From c087d343fb0b1aa6583a867ff24d4801156da91a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 05:26:44 +0000 Subject: [PATCH] =?UTF-8?q?docs(spec):=20fix=20QA=20field/capture=20path?= =?UTF-8?q?=20convention=20=E2=80=94=20no=20body.=20prefix=20(#7365)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestAssertionSchema.field's describe() text and TestStepSchema.capture's sibling describe() both taught a `body.*` path convention that never matched the runtime: TestRunner resolves both against `result` directly, the value HttpTestAdapter.handleResponse returns (the parsed response body itself, no `body` wrapper). Executes the 2026-08-11 maintainer ruling on #7365 (comment 5248467805): docs follow the adapter, root- relative paths, no stored-suite compatibility to preserve since body.* never worked. Acceptance face unchanged - field and capture keep their original Zod types, only description text moves. Regenerates content/docs/references/qa/testing.mdx via `pnpm --filter @objectstack/spec gen:docs`; check:docs reports all 231 files in sync. Adds a patch changeset for @objectstack/spec, following the #7444 precedent for describe/TSDoc-only spec docs fixes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_016R9de1FqP7NvwKvqXi92Gh --- .../qa-assertion-field-path-body-root.md | 41 +++++++++++++++++++ content/docs/references/qa/testing.mdx | 4 +- packages/spec/src/qa/testing.zod.ts | 4 +- 3 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 .changeset/qa-assertion-field-path-body-root.md diff --git a/.changeset/qa-assertion-field-path-body-root.md b/.changeset/qa-assertion-field-path-body-root.md new file mode 100644 index 0000000000..097408269f --- /dev/null +++ b/.changeset/qa-assertion-field-path-body-root.md @@ -0,0 +1,41 @@ +--- +"@objectstack/spec": patch +--- + +docs(spec): fix the QA `field`/`capture` path convention — no `body.` prefix (#7365) + +`TestAssertionSchema.field`'s `.describe()` text read `'Field path in the +result to check (e.g. "body.data.0.status")'`, and `TestStepSchema.capture`'s +sibling `.describe()` repeated the same `body.*` example. Neither convention +ever matched the runtime: `TestRunner.assert`/`runStep` resolve both paths +against `result` directly — the value `HttpTestAdapter.handleResponse` +returns, which is the parsed response body itself with no `body` wrapper (nor +does the platform's own response envelope, `data`/`meta`, ever nest under a +`body` key). A suite written to the documented convention resolved every path +to `undefined`. + +Filed as #7365 (observation-class finding from #7256's blast radius): with +`equals`-class operators a `body.*` path already failed loudly, so an author +worked the real convention out by trial; with `contains`, `undefined` fell out +of the switch and the assertion silently passed, so a `body.*` `contains` +reported green forever. #7256 (PR #7348) turned that silent pass into a loud +failure, which is correct, but it meant an author following the schema's own +example now hits a error that never says the *documentation* is wrong. + +Maintainer ruling, 2026-08-11 (issue comment 5248467805): "docs follow the +adapter" — fix the `describe()` text (and the regenerated reference) to the +real convention, root-relative, no `body.` wrapper. `body.*` never worked, so +there is no stored-suite compatibility to preserve. The acceptance face is +unchanged — `field` and `capture` both stay their original Zod types; only the +description text moves. + +Both faces now read, in the file's existing one-sentence-plus-example style: + +- `field`: `'Field path in the result to check, resolved against the parsed + response body root — no "body." prefix (e.g. "data.0.status")'` +- `capture`: `'Map result fields to context variables, paths resolved against + the response body root (e.g. { "newId": "data.id" })'` + +`content/docs/references/qa/testing.mdx` is regenerated to match +(`pnpm --filter @objectstack/spec gen:docs`); `check:docs` reports all 231 +generated files in sync. diff --git a/content/docs/references/qa/testing.mdx b/content/docs/references/qa/testing.mdx index 0fbadc75d1..65ecec928d 100644 --- a/content/docs/references/qa/testing.mdx +++ b/content/docs/references/qa/testing.mdx @@ -63,7 +63,7 @@ A test assertion that validates the result of a test action | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **field** | `string` | ✅ | Field path in the result to check (e.g. "body.data.0.status") | +| **field** | `string` | ✅ | Field path in the result to check, resolved against the parsed response body root — no "body." prefix (e.g. "data.0.status") | | **operator** | `Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'error'>` | ✅ | Comparison operator to use | | **expectedValue** | `any` | ✅ | Expected value to compare against | @@ -126,7 +126,7 @@ A single step in a test scenario, consisting of an action and optional assertion | **description** | `string` | optional | Human-readable description of what this step tests | | **action** | `{ type: Enum<'create_record' \| 'update_record' \| 'delete_record' \| 'read_record' \| … +4 more>; target: string; payload?: Record; user?: string }` | ✅ | The action to execute in this step | | **assertions** | `{ field: string; operator: Enum<'equals' \| 'not_equals' \| 'contains' \| 'not_contains' \| 'is_null' \| 'not_null' \| … +5 more>; expectedValue: any }[]` | optional | Assertions to validate after the action completes | -| **capture** | `Record` | optional | Map result fields to context variables: `{ "newId": "body.id" }` | +| **capture** | `Record` | optional | Map result fields to context variables, paths resolved against the response body root (e.g. `{ "newId": "data.id" }`) | --- diff --git a/packages/spec/src/qa/testing.zod.ts b/packages/spec/src/qa/testing.zod.ts index 0c4982241f..198a4236e9 100644 --- a/packages/spec/src/qa/testing.zod.ts +++ b/packages/spec/src/qa/testing.zod.ts @@ -42,7 +42,7 @@ export const TestAssertionTypeSchema = lazySchema(() => z.enum([ ]).describe('Comparison operator for test assertions')); export const TestAssertionSchema = lazySchema(() => z.object({ - field: z.string().describe('Field path in the result to check (e.g. "body.data.0.status")'), + field: z.string().describe('Field path in the result to check, resolved against the parsed response body root — no "body." prefix (e.g. "data.0.status")'), operator: TestAssertionTypeSchema.describe('Comparison operator to use'), expectedValue: z.unknown().describe('Expected value to compare against') }).describe('A test assertion that validates the result of a test action')); @@ -55,7 +55,7 @@ export const TestStepSchema = lazySchema(() => z.object({ action: TestActionSchema.describe('The action to execute in this step'), assertions: z.array(TestAssertionSchema).optional().describe('Assertions to validate after the action completes'), // Capture outputs to variables for subsequent steps - capture: z.record(z.string(), z.string()).optional().describe('Map result fields to context variables: { "newId": "body.id" }') + capture: z.record(z.string(), z.string()).optional().describe('Map result fields to context variables, paths resolved against the response body root (e.g. { "newId": "data.id" })') }).describe('A single step in a test scenario, consisting of an action and optional assertions')); export const TestScenarioSchema = lazySchema(() => z.object({