test(api): parse real responses against the schemas that publish them

Response drift was structurally undetectable. `openapi:check` proves the
YAML matches the registry and the route-coverage guard proves the verb is
listed; both pass when the registered schema disagrees with what
`apiSuccess()` sends, and the coverage guard says so in its own doc
comment. That is how `/api/sync/changes` shipped five domains against a
three-domain contract and how the error envelope went years without
declaring the field the multi-issue 422 was built for.

Two bodies, the two that were broken: a full `/api/sync/changes` page
with live and soft-deleted rows in all five domains, its `cursorExpired`
short-circuit (a second shape under the same schema), and a 422 carrying
more than one issue.

`safeParse` alone would not have caught either finding, because Zod
strips undeclared keys instead of refusing them — a schema short of the
wire parses cleanly. So the parsed value is compared back to the raw
body; a strip means the contract is missing something the server sends.
Removing `cycleDays` / `cycles` from the sync schema, or `details` from
the envelope, fails all three cases.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 04:08:52 +02:00
1 parent 0cd996e7de
commit 4993352cc9
2 files changed
+307 -1

No files matched your search

+9 -1
View File
@@ -236,7 +236,15 @@ const syncChangesQuery = z
})
.meta({ id: "SyncChangesQuery" });
const syncChangesResponse = z
/**
* Exported so a test can `safeParse` a real `/api/sync/changes` body against
* it. Nothing else checks a response SHAPE against its published schema —
* `openapi:check` proves the YAML matches the registry, and the route-coverage
* guard proves the path is listed, so a schema that disagrees with what
* `apiSuccess()` sends passes both. That is how this endpoint went five
* domains wide against a three-domain contract.
*/
export const syncChangesResponse = z
.object({
serverNow: z.iso.datetime({ offset: true }),
cursor: z