Skip to content

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 into
mainfrom
claude/issue-9267-plugin-route-envelope-conformance
Aug 17, 2026
Merged

test(cloud-connection): envelope conformance for the plugin-route door, and a third surface for check:route-envelope#9369
os-zhuang merged 1 commit into
mainfrom
claude/issue-9267-plugin-route-envelope-conformance

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#9267

The plugin-route door — a plugin mounting its own Hono routes and answering refusals with its own 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 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.ts

18 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 shape service-storage / service-i18n use. Each emitted body is then parsed against the declared contract rather than read one field at a time:

  1. BaseResponseSchema.safeParse — it parses as an envelope at all.
  2. envelopeViolations — it is the declared envelope. safeParse alone passes { success: true } with no payload and passes a payload duplicated into a stray top-level key (The /share-links dispatcher domain emits data AND a legacy link/links beside 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).
  3. ApiErrorSchema.safeParse(body.error) — the nested error is the declared error, including that code is 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 off BaseResponseSchema.

2. The gate — a third surface in scripts/check-route-envelope.mjs

Surfaces 1 and 2 cannot see these files: they neither write to a response object nor return { status, body } for a central sender. cloud-connection appeared 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.ts convention, which is how rest-server.ts sat 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.ts ruling (#7295, 2026-08-10) applies in full: these files are hot — marketplace-install-local-plugin.ts changed 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 dialects stringError (#3675) and siblingCode (#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:

  • 11 error exits emitted error: { code } with no message.ApiErrorSchema.message is required, so body.error.message read undefined on every one. The Console had already grown the consumer-side accommodation that produces — CloudConnectionPanel displays body?.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 eight ENVIRONMENT_NOT_FOUND sites now share one constant rather than repeating a literal.
  • /bind/poll stamped the upstream RFC 8628 spelling into the closed code slot (expired_token, access_denied, …) and carried no message — so that body failed its own contract twice. The verbatim spelling now rides declaredCode, the open producer-authored channel ADR-0112 declares for exactly this case, while code carries the registered DEVICE_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 bare current-user payloads), plugin-auth (/bootstrap-status), adapters/hono (two bare { data } discovery bodies and an errorJson writing the HTTP status into error.code), cli/serve.ts, and this package's own runtime-config-plugin.ts. Two of those are cross-repo breaking wire changes rather than conformance tidying — /api/v1/runtime/config is read bare by the Console SPA before first paint (objectui app-shell/src/runtime-config.ts reads body.cloudUrl / body.features / body.branding off 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 message fix and re-running both halves:

halfcaught
conformance suite4 of 11 — the exits it drives
gate, surface 311 of 11 — errorWithoutMessage: found 11, declared 0, lines 214, 266, 321, 324, 325, 399, 402, 444, 496, 558, 595

That 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 test23 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.
  • Gate families re-derived from the actual changed paths with node scripts/pm/dispatch-gates.mjs and 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), and scripts/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

…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>
@github-actionsgithub-actionsBot added size/xl documentation Improvements or additions to documentation tests tooling labels Aug 17, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 17, 2026 17:31
@os-zhuang
os-zhuang added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit d2e6b1dAug 17, 2026
25 of 26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9267-plugin-route-envelope-conformance branch August 17, 2026 17:59
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-route door: no envelope guard and no schema parse — cloud-connection's hand-built error bodies are outside every conformance surface

2 participants

@os-zhuang@claude