Observation-class finding, measured while running the blast radius for #7256 (PR #7348). Filed unassigned, no pm:queue — for triage to grade. Deliberately not folded into #7256's PR: the fix there is one else branch in packages/core/src/qa/runner.ts, and this is a mismatch between packages/core/src/qa/http-adapter.ts and the describe() text in packages/spec/src/qa/testing.zod.ts — a different file and a different call (change the adapter, or change the convention).
The mismatch
TestAssertionSchema.field documents its own convention, and the generated reference repeats it:
field: z.string().describe('Field path in the result to check (e.g. "body.data.0.status")')
TestRunner.assert resolves that path against whatever the adapter returned. HttpTestAdapter.handleResponse returns the parsed response body itself:
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json(); // ← the body, not { body: … }
}
return response.text();
There is no body key anywhere in the value the runner sees, and none in the platform's own response envelope either (ResponseEnvelopeConfigSchema wraps into data + meta, never body). So a suite written to the documented convention — field: "body.data.0.status", field: "body.status" — resolves every assertion path to undefined.
Why it surfaced now, and why it is worth a card
Until #7256 this was half-invisible. With equals/not_equals/not_null a body.* path already failed loudly, so an author hit it on their first run and worked the convention out by trial. With contains the undefined fell out of the switch and the assertion passed, so a body.* contains reported ✅ forever and nobody learned anything.
Measured against a stand-in server returning {"ok":true,"data":{"total":0},"name":"acme corp","tags":["acme","globex"]}, same fixture, same HttpTestAdapter:
| scenario |
base f3f855ac |
with #7256's fix |
contains on body.data.items (the documented convention) |
✅ (asserting nothing) |
❌ got undefined |
contains on data.items (path absent) |
✅ (asserting nothing) |
❌ got undefined |
contains on data.total (a number) |
✅ (asserting nothing) |
❌ got number |
contains on tags (a real array) |
✅ |
✅ |
contains on name (a real string) |
✅ |
✅ |
So #7256 makes the mismatch visible rather than causing it — the documented-convention row is a test that was never running. That is the right outcome, but the author who now sees red is following the schema's own example, and the error will tell them the path did not resolve without telling them the documentation is what's wrong.
The call, which is a real choice
- Fix the convention — correct the
describe() example (and the generated reference) to the shape the adapter actually returns (data.0.status). Cheapest, and makes the docs true today. Touches packages/spec (domain:spec-surface by the accept-face test: describe text only, no accept/reject change).
- Fix the adapter — have
HttpTestAdapter return { status, headers, body } so body.* and status both work, which is the shape the field example implies and what most HTTP test runners hand back. Strictly better for authors (an assertion on the status code is impossible today), but it is a behaviour change to a published adapter and would break any suite written against the current shape.
Option 2 is the better surface and option 1 is the smaller change; picking between them is a protocol call, not a cleanup, which is why this is filed rather than fixed.
Dedup
Searched open issues and PRs for HttpTestAdapter, the field path convention and the body prefix before filing — nothing names this. #7256 names the contains fall-through only.
Observation-class finding, measured while running the blast radius for #7256 (PR #7348). Filed unassigned, no
pm:queue— for triage to grade. Deliberately not folded into #7256's PR: the fix there is oneelsebranch inpackages/core/src/qa/runner.ts, and this is a mismatch betweenpackages/core/src/qa/http-adapter.tsand thedescribe()text inpackages/spec/src/qa/testing.zod.ts— a different file and a different call (change the adapter, or change the convention).The mismatch
TestAssertionSchema.fielddocuments its own convention, and the generated reference repeats it:TestRunner.assertresolves that path against whatever the adapter returned.HttpTestAdapter.handleResponsereturns the parsed response body itself:There is no
bodykey anywhere in the value the runner sees, and none in the platform's own response envelope either (ResponseEnvelopeConfigSchemawraps intodata+meta, neverbody). So a suite written to the documented convention —field: "body.data.0.status",field: "body.status"— resolves every assertion path toundefined.Why it surfaced now, and why it is worth a card
Until #7256 this was half-invisible. With
equals/not_equals/not_nullabody.*path already failed loudly, so an author hit it on their first run and worked the convention out by trial. Withcontainstheundefinedfell out of the switch and the assertion passed, so abody.*containsreported ✅ forever and nobody learned anything.Measured against a stand-in server returning
{"ok":true,"data":{"total":0},"name":"acme corp","tags":["acme","globex"]}, same fixture, sameHttpTestAdapter:f3f855accontainsonbody.data.items(the documented convention)got undefinedcontainsondata.items(path absent)got undefinedcontainsondata.total(a number)got numbercontainsontags(a real array)containsonname(a real string)So #7256 makes the mismatch visible rather than causing it — the documented-convention row is a test that was never running. That is the right outcome, but the author who now sees red is following the schema's own example, and the error will tell them the path did not resolve without telling them the documentation is what's wrong.
The call, which is a real choice
describe()example (and the generated reference) to the shape the adapter actually returns (data.0.status). Cheapest, and makes the docs true today. Touchespackages/spec(domain:spec-surfaceby the accept-face test: describe text only, no accept/reject change).HttpTestAdapterreturn{ status, headers, body }sobody.*andstatusboth work, which is the shape thefieldexample implies and what most HTTP test runners hand back. Strictly better for authors (an assertion on the status code is impossible today), but it is a behaviour change to a published adapter and would break any suite written against the current shape.Option 2 is the better surface and option 1 is the smaller change; picking between them is a protocol call, not a cleanup, which is why this is filed rather than fixed.
Dedup
Searched open issues and PRs for
HttpTestAdapter, thefieldpath convention and thebodyprefix before filing — nothing names this. #7256 names thecontainsfall-through only.