From 2bfdcbab856ce0768536262a4b398c39aad83463 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 09:56:12 +0000 Subject: [PATCH 1/2] test(runtime): the dispatcher envelope check uses the shared predicate (#4090 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `domain-handler-registry.test.ts` hand-rolled "no key beside the envelope's own may hold the payload" for #4038. #4090 extracted that rule into `envelopeViolations` so it stops having two definitions; this is the caller that motivated the extraction, now using it. Leaving the local copy would have recreated the exact failure this line has been closing — one rule, two places, and only one of them updated next time. It also was not the same rule: the local set allowed any body whose top-level keys were success/data/meta, so it passed a success body with NO `data` at all, and one carrying an `error` alongside `success: true`. The shared predicate rejects both. Verified the swap is a strict improvement rather than a refactor, by breaking the producer two ways and checking the suite catches each: { success: true, data: link, link } → caught (the old check caught this too) { success: true } → caught (the old check PASSED this) runtime 914 passed. All ten gates in the TypeScript Type Check job pass, plus the six `check:*` guards. Note for the next person touching spec: after restarting a branch onto a newer main, `check:api-surface` and `check:i18n-coverage` both fail against a stale `packages/spec/dist` — the first reports other people's exports as added/removed, the second rejects an example config for a chart type the fresh spec allows. Neither is a real failure; rebuilding spec clears both. They read as "my change broke something" and are not. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z --- .../runtime/src/domain-handler-registry.test.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/domain-handler-registry.test.ts b/packages/runtime/src/domain-handler-registry.test.ts index 5d46434709..0798c50983 100644 --- a/packages/runtime/src/domain-handler-registry.test.ts +++ b/packages/runtime/src/domain-handler-registry.test.ts @@ -14,6 +14,7 @@ */ import { describe, it, expect, vi } from 'vitest'; +import { envelopeViolations } from '@objectstack/spec/api'; import { HttpDispatcher } from './http-dispatcher.js'; import { DomainHandlerRegistry } from './domain-handler-registry.js'; import type { DomainHandler } from './domain-handler-registry.js'; @@ -391,9 +392,15 @@ describe('HttpDispatcher extracted domains (PR-4: share-links)', () => { }); it('every success body carries its payload under `data` and nowhere else', async () => { - // The general form of the two assertions above, over all four success - // routes: no key beside the envelope's own may hold the payload. - const ENVELOPE_KEYS = new Set(['success', 'data', 'meta']); + // The general form of the two assertions above, over all the success + // routes — and `envelopeViolations` (#4090) is now where that general form + // lives. This test hand-rolled it first, for #4038; extracting it into the + // spec meant the same rule stopped having two definitions that could drift + // apart, which is the failure this whole line has been closing. + // + // The shared one is also stricter than what stood here: the local set + // allowed any body whose keys were `success`/`data`/`meta`, so it passed a + // success body with no `data` at all and one carrying an `error`. const shareLinks = { createLink: vi.fn().mockResolvedValue(LINK), listLinks: vi.fn().mockResolvedValue([LINK]), @@ -407,9 +414,7 @@ describe('HttpDispatcher extracted domains (PR-4: share-links)', () => { ]; for (const body of bodies) { expect(body?.success).toBe(true); - for (const key of Object.keys(body ?? {})) { - expect(ENVELOPE_KEYS.has(key), `body carries a non-envelope top-level key: ${key}`).toBe(true); - } + expect(envelopeViolations(body), `not the declared envelope: ${JSON.stringify(body)}`).toEqual([]); } }); From ac3ac0753b38267664597328a5adea819f79e50a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 09:58:12 +0000 Subject: [PATCH 2/2] chore: add the empty changeset this PR needs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check Changeset requires one on every PR; an empty-frontmatter changeset is the sanctioned form for a change that releases nothing, and the gate's own error message says so. Second time this session — #4009 was a comment-only PR that failed the same way. I fixed that one and did not draw the rule, which is that "this releases nothing" is a reason to write an EMPTY changeset, not a reason to skip the file. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CYbS3kS8xzsHNXFTzp4e2z --- .../dispatcher-envelope-shared-predicate.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/dispatcher-envelope-shared-predicate.md diff --git a/.changeset/dispatcher-envelope-shared-predicate.md b/.changeset/dispatcher-envelope-shared-predicate.md new file mode 100644 index 0000000000..2de0f29483 --- /dev/null +++ b/.changeset/dispatcher-envelope-shared-predicate.md @@ -0,0 +1,17 @@ +--- +--- + +`domain-handler-registry.test.ts` now uses `envelopeViolations` instead of its own +copy of the rule. Deliberately empty frontmatter: test-only, this releases nothing. + +That test hand-rolled "no key beside the envelope's own may hold the payload" for +#4038. #4090 promoted the rule into the spec so it would stop having two +definitions; leaving the local copy would have recreated the exact failure this +line has been closing — one rule, two places, only one updated next time. + +The two were also not equivalent. The local set allowed any body whose top-level +keys were `success` / `data` / `meta`, so it passed a success body with no `data` +at all and one carrying an `error` beside `success: true`. The shared predicate +rejects both, which makes this a strict tightening rather than a refactor: +injecting `{ success: true }` into the producer now fails the suite, and used to +pass it.