Uh oh!
There was an error while loading. Please reload this page.
test(cloud-connection): envelope conformance for the plugin-route door, and a third surface for check:route-envelope - #9369
Merged
os-zhuang merged 1 commit intoAug 17, 2026
Conversation
…r + teach the gate its third surface
The `plugin-route` door — a plugin mounting its own Hono routes and answering
refusals with `c.json({ success: false, error: { … } })` — had no envelope
conformance coverage of any kind. Its bodies pass through neither the
dispatcher's `errorFromThrown` nor packages/rest's responders, so neither the
central narrowing nor the central suites ever saw them.
Both halves, per triage direction 3:
- A conformance suite in packages/cloud-connection drives the four plugins'
real error exits through their mounted routes and parses each emitted body
against BaseResponseSchema / envelopeViolations / ApiErrorSchema from
@objectstack/spec/api, rather than reading one field off it.
- scripts/check-route-envelope.mjs gains a third surface: plugin-mounted Hono
routes, discovered by parsing rather than by filename, counting the bodies
that depart from the declared envelope. Write-site totals are reported and
never pinned (the #7295 lesson); every violation counter ticks down only.
What the new surface found and this fixes, all local to an error exit:
- Eleven refusals on /api/v1/cloud-connection/* emitted `error: { code }` with
no `message`. ApiErrorSchema requires it, so `body.error.message` read
`undefined` — the #3843 class. The Console had already grown the
consumer-side accommodation that produces, displaying
`body?.error?.message ?? body?.error?.code`.
- /bind/poll stamped the upstream RFC 8628 spelling (`expired_token`, …)
straight into the closed ADR-0112 `code` slot, and carried no `message`.
The verbatim spelling now rides `declaredCode`, the open producer-authored
channel ADR-0112 declares for exactly this; `code` carries the registered
member.
The remaining 20 non-conforming bodies the surface found across five other
packages are filed as #9364 and recorded as ratchets — measured, pinned, and
not blessed. Two of them are cross-repo breaking wire changes, not
conformance tidying.
Co-Authored-By: Claude <noreply@anthropic.com>os-zhuang
marked this pull request as ready for review
August 17, 2026 17:31
This was referenced Aug 17, 2026
Merged
Uh oh!
There was an error while loading. Please reload this page.
os-zhuang
deleted the
claude/issue-9267-plugin-route-envelope-conformance
branch
August 17, 2026 17:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#9267
The
plugin-routedoor — a plugin mounting its own Hono routes and answering refusals with its ownc.json({ success: false, error: { … } })— had no envelope conformance coverage of any kind. Its bodies pass through neither the dispatcher'serrorFromThrownnorpackages/rest's responders, so neither the central narrowing nor the central conformance suites ever saw them.Triage's direction 3, both halves, because the two catch different things — and this branch measured exactly how differently (see the reverse verification below).
1. The suite —
packages/cloud-connection/src/error-envelope.conformance.test.ts18 cases driving the four plugins' real error exits through the routes they actually mount (
start()+kernel:ready, the kernel's own lifecycle), following the #3687 shapeservice-storage/service-i18nuse. Each emitted body is then parsed against the declared contract rather than read one field at a time:BaseResponseSchema.safeParse— it parses as an envelope at all.envelopeViolations— it is the declared envelope.safeParsealone passes{ success: true }with no payload and passes a payload duplicated into a stray top-level key (The /share-links dispatcher domain emitsdataAND a legacylink/linksbeside it — a producer-side shim that outlived its readers #4038 / fix(runtime)!: the /share-links dispatcher domain stops emitting a duplicate link/links beside data (#4038) #4049).ApiErrorSchema.safeParse(body.error)— the nested error is the declared error, including thatcodeis a member of the closed ADR-0112 vocabulary. An invented spelling fails here rather than reaching a wire nobody audits.Status and code are asserted as the case's identity — proof the exit under test is the one that ran — never as the conformance claim. The pre-existing tests at this seam assert
res.payload.error.code/details.findings, which is precisely the style that cannot notice a body drifting offBaseResponseSchema.2. The gate — a third surface in
scripts/check-route-envelope.mjsSurfaces 1 and 2 cannot see these files: they neither write to a response object nor return
{ status, body }for a central sender.cloud-connectionappeared nowhere in the gate.Discovery is by behaviour, not by filename. The scan parses every non-test file under
packages/and keeps the ones that actually write a Hono context response. That is the lesson of this gate's own history — surface 1 discovers by the*-routes.tsconvention, which is howrest-server.tssat unaudited for as long as the gate existed (#7295), and the same gap #8884 closed. A plugin mounting Hono routes follows no convention at all, so a name-based surface would need hand-extending for every new plugin, and the one nobody remembers to add is the one that drifts. A discovered file absent from the table is an error, never a default.What is counted, and what is deliberately not. The
rest-server.tsruling (#7295, 2026-08-10) applies in full: these files are hot —marketplace-install-local-plugin.tschanged twice in one day — so the total write-site count is reported and never pinned. What is pinned is the count of bodies that depart from the envelope; those move only when a non-conforming body is added or removed, and every one ticks DOWN only.Six counters, each earned by a shape measured in the tree:
unenveloped,errorWithoutMessage,errorCodeNotString,strayKeys, plus the two known dialectsstringError(#3675) andsiblingCode(#7035). Relayed bodies —c.json(upstreamBody, status)— are invisible to all six on purpose: this gate governs the bodies the repo builds, not the bytes it passes through. 16 new self-test cases pin the counters in both directions, including three negatives.3. What the new surface found, and what I did about each
It went red on existing code immediately. Per the card, that red is the finding.
Fixed here — small and local to a plugin's error exit, in
cloud-connection-plugin.ts:error: { code }with nomessage.ApiErrorSchema.messageis required, sobody.error.messagereadundefinedon every one. The Console had already grown the consumer-side accommodation that produces —CloudConnectionPaneldisplaysbody?.error?.message ?? body?.error?.code, i.e. it shows a machine code to a human because the readable half was never sent. No status and no code changed; the eightENVIRONMENT_NOT_FOUNDsites now share one constant rather than repeating a literal./bind/pollstamped the upstream RFC 8628 spelling into the closedcodeslot (expired_token,access_denied, …) and carried nomessage— so that body failed its own contract twice. The verbatim spelling now ridesdeclaredCode, the open producer-authored channel ADR-0112 declares for exactly this case, whilecodecarries the registeredDEVICE_CODE_FAILED. Nothing is lost; the changeset carries the before/after.Not fixed — filed as #9364, and recorded in the gate as ratchets so none of it can get worse: 20 non-conforming bodies across
plugin-hono-server(adapter refusals in the pre-#3675 dialect, plus 9 barecurrent-userpayloads),plugin-auth(/bootstrap-status),adapters/hono(two bare{ data }discovery bodies and anerrorJsonwriting the HTTP status intoerror.code),cli/serve.ts, and this package's ownruntime-config-plugin.ts. Two of those are cross-repo breaking wire changes rather than conformance tidying —/api/v1/runtime/configis read bare by the Console SPA before first paint (objectuiapp-shell/src/runtime-config.tsreadsbody.cloudUrl/body.features/body.brandingoff the top level) — which is well outside "small and local to an error exit".runtime-config-plugin.ts's state is additionally pinned by a suite case that records the drift explicitly and goes red the day someone fixes it.Nothing was weakened to make the gate pass: no threshold lowered, no ignore added, and the surface is repo-wide rather than scoped to this card's package.
Reverse verification — measured, from the committed state
Reverting only the
messagefix and re-running both halves:errorWithoutMessage: found 11, declared 0, lines 214, 266, 321, 324, 325, 399, 402, 444, 496, 558, 595That is the card's own thesis, measured: a suite catches what it drives; a structural count catches the branch nobody drove. Neither half alone would have found this. The file was restored from the commit and proved byte-identical (
git hash-object=git rev-parse HEAD:PATH), and the numbers above come from that committed state.Verification
Run against the final commit,
85a937a5e:pnpm --filter @objectstack/cloud-connection test— 23 files, 191 tests, all passing (18 of them new).node scripts/check-route-envelope.mjs— green across all three surfaces: 10 route modules, 16 dispatcher domains, and 11 plugin-route modules / 161 hand-built bodies (5 conformant, 6 ratcheted).node scripts/check-route-envelope.mjs --self-test— passes, including the 16 new cases.node scripts/pm/dispatch-gates.mjsand run:check:route-envelope,check:nul-bytes,check:engine-double-contract,check:where-matcher,check:query-options-erasure,check:cross-package-test-inputs,check:slot-lookup,check:error-code-casing,check:type-check-coverage,check:type-check-debt --re-measure(on a built closure), andscripts/docs-audit/check-affected-docs.mjs— all green.The derivation named four convention-triggered families the dispatch brief did not, because this PR adds a test file; they are in the list above.
Generated by Claude Code