test(e2e-prod): response-schema validation — suite records, gate validates - #802
Merged
Conversation
…dates
Nothing validated live response bodies against api/openapi.yaml: the suites
assert hand-picked fields, and harness/coverage.ts records 2xx status pairs
only. A dropped required field, a wrong type, or an error path returning a
bare string instead of the envelope all shipped invisibly.
The suite stays zero-dependency (the ops pipeline runs it with no npm
install), so validation is split:
- harness/responses.ts records EVERY ApiClient response — all statuses, not
just 2xx, since every op documents `default` → ErrorEnvelope, making each
401/404/422 the suites already provoke a free error-contract check — as
{method, path, status, kind, body} samples in reports/response-samples/
(per-pid shards, same flush/pretest-clean pattern as coverage.ts). The
coverage channel's 2xx-only filter is untouched: coverage answers "did
the op run?", this answers "was the body well-shaped?".
- response_schema_gate.py (needs jsonschema next to the existing pyyaml)
maps each sample to its operationId with coverage_gate's matcher, resolves
the response schema for the exact status (falling back to `default`),
resolves $refs against components, and validates under Draft 2020-12.
Extra fields are fine (responses are deliberately additionalProperties:
true); missing required fields, wrong types, empty/non-JSON bodies where
JSON is documented, and undocumented statuses are violations. House gate
contract: exit 2 on no-shards/inconclusive, 1 on violations, 0 clean;
empty ALLOWLIST with a stale-entry check.
Not wired into the release pipeline yet — this lands the mechanism and the
measurement; the blocking decision follows the first measured violation set.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 free
to 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.
What
Closes the audited conformance gap: nothing validated live response bodies against
api/openapi.yaml—harness/client.tschecks status codes, suites assert hand-picked fields, andharness/coverage.tsrecords 2xx pairs only. A dropped required field, a wrong type, or an error path returning a bare string instead of the envelope all shipped invisibly.The suite is deliberately zero-dependency (the ops release pipeline runs it with no
npm install), so validation cannot happen in-process. The split: the suite records, the gate validates.harness/responses.ts(new) — records everyApiClientresponse as a{method, path, status, contentType, kind, body}sample inreports/response-samples/(per-pid shards, sameexit-flush +pretest-clean pattern ascoverage.ts). Records all statuses, not just 2xx: every op documentsdefault→ErrorEnvelope, so each 401/404/422 the suites already provoke is a free error-contract check. The coverage channel's 2xx-only filter is untouched — coverage answers "did the op run?", this answers "was the body well-shaped?".response_schema_gate.py(new) — maps each sample to its operationId withcoverage_gate.py's matcher, picks the response schema for the exact status (falling back todefault), resolves$refs againstcomponents, validates under Draft 2020-12 (the spec is OpenAPI 3.1). Needsjsonschemaalongside the existingpyyaml. Extra fields are not violations (responses are deliberatelyadditionalProperties: true, andformatstays annotation-only); missing required fields, wrong types, empty/non-JSON bodies where JSON is documented, and undocumented statuses are. House gate contract: exit 2 on no-shards/inconclusive (never 0), 1 on violations, 0 clean; emptyALLOWLISTwith the stale-entry check.harness/client.ts— onerecordResponse(...)call after body parse.package.json—coverage:gate:responsesscript;pretestclears the new shard dir.test_gates.py— 6 new subprocess tests (skip cleanly withoutjsonschema), including explicit proof the gate can FAIL: missing required field, wrong type, and non-JSON body each exit 1; extra fields exit 0; no shards exit 2.Measured against live staging (partial run, 2026-08-02)
Suites 01–13 against
api-staging.e2a.dev(run cut short deliberately; a Free-plan account capped agent-heavy paths). 379 samples, 13 shards, 28/72 operations sampled (274× 2xx + 105× 4xx), 360 valid, 1 violating sample:Root cause (verified by curl + config): Caddy URI-normalizes
..%2F..so the path escapes/v1/*and hits the api-host allowlist catch-all (respond "Not Found" 404— opsCaddyfile.staging/Caddyfile), so the request never reaches the app. The server's ownrouteNotFoundcorrectly returns the JSON envelope for real/v1paths (control: bogus agent → JSONnot_found+x-request-id). The attribution togetAgentis a recorder artifact (the client-side pathname keeps%2Fencoded, so it maps as one segment); the finding itself — the hosted edge answers some/v1-looking requests with a non-envelope plain-text 404 — is real, and is an edge-config question for e2a-ops, not a server bug.Every app-generated response sampled — including all 105 error responses — conformed. Not sampled (suites 14+ not reached): templates, webhooks, reviews (mostly), api-keys, contacts, suppressions, trash, attachments, starter templates.
Not in this PR
e2a-opsPR (pip install jsonschemanext to the existingpyyamlstep + runningcoverage:gate:responses). The measured violation rate (1 infra-layer finding, 0 app-layer, across 379 samples) suggests it can block with at most one justified allowlist entry once the remaining 44 ops have been measured on a full staging-gate run.Verification
npm run typecheckclean;node --test harness/*.test.ts18/18.python3 -m unittest test_gates -v12/12 (venv with pyyaml + jsonschema).🤖 Generated with Claude Code