API Mock and Fixture Drift Gates: Three-Party Contracts, Recorded Lineage, and Bidirectional Verification
Mock tests all green, the real API may still have changed enums, defaults, or error semantics. This article brings specs, producer implementations, and test doubles into one versioned manifest, using bidirectional consistency and counterexamples to block "self-consistent but wrong" frontend tests.
Key Points
- Topic Focus: React、TypeScript、DevOps
- Suitable for: indie hackers, full-stack engineers, AI application developers
- Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
The frontend repo froze the /orders success response as a JSON; six months later the API split status: pending into awaiting_payment, but the Mock still returns the old value and component tests stay all green. Another kind of drift is more subtle: OpenAPI marks a field nullable, but the real service always omits it; the team only validates schema and still can't discover that defaults, sorting, and error-code semantics have diverged. REST API versioning discusses how interfaces evolve; this article governs whether test doubles still represent some explicit version.
OpenAPI 3.1's Schema Object is based on the JSON Schema 2020-12 dialect, but a valid schema doesn't automatically prove examples cover all branches, nor that the implementation obeys business invariants. Mock, spec, and producer must each be evidenced separately; you can't let the Mock be generated from the spec and then verify itself with the same generator and claim the contract holds. API design comparison helps pick a protocol; after landing, you still need real consumer scenarios and producer-combination validation.
Pin contractSet.v1's four digests
Each release of test doubles generates web.apiMockContractSet.v1. The manifest pins apiId, OpenAPI and dialect, producer and Mock artifacts, and the fixture bundle, and must also pin the versions and digests of the parser, independent verifier, normalizer, consumer artifacts, and the business oracle bundle. Each scenario has a stable scenarioId, listing request preconditions, response branches, latency behavior, sensitive-field policy, and oracle.
schema: web.apiMockContractSet.v1
apiId: orders.v3
specDigest: sha256:31d...
producerArtifact: sha256:901...
mockEngine: [email protected]
fixtureBundle: sha256:8ca...
toolchain: { parser: sha256:12a..., verifier: sha256:44b... }
normalizer: sha256:77c...
consumers: [web@sha256:51d...]
oracleBundle: sha256:93e...
scenarios: [order.paid, order.missing, order.conflict]
Spec text or semantic changes must produce a new specDigest; merely reordering keys can't manufacture a version. Fixtures are immutable content; correcting an old sample requires publishing a new bundle and recording supersedes, and overwriting the file while keeping the original version number is forbidden. When the reader doesn't support the dialect, references can't be resolved, or digests don't match, it returns UNRESOLVED_CONTRACT; it can't skip validation and continue starting the Mock.
Three independent verification edges
The first edge: a verifier that is a different implementation from the Mock generator and independently published performs spec conformance on the real producer. The second sends the same request into producer and Mock and compares results after a fixed normalizer; time and trace IDs can be ignored as declared, but sorting, amount units, and missing/null values can't be smoothed over. The third lets the real consumer artifacts in the manifest run against each of the two separately. Beyond structural validation, an independent business oracle also checks amount conservation, state transitions, cursor uniqueness, and that errors have no side effects; you can't just let parser and generator be self-consistent with each other.
The compatibility matrix must at least cover previous consumer/current consumer and previous producer/current producer; break results enter manual migration and don't auto-refresh snapshots. Agent API contract compatibility gate applies to Agents changing schemas; here it additionally requires the producer artifact to have exclusive database, queue, and email/payment sinks per scenario; the network is off by default, external actions only write to assertable isolated sinks, and after completion they are cleared by scenario ID, preventing contract tests from actually sending mail, charging, or polluting the next case.
When recording fixtures, first redact via a controlled proxy with a field allowlist, then write to CAS; request Authorization, Cookie, free text, and production identifiers are not saved by default. The manifest records the source environment category, recording tool, sanitizer version, and deleted field counts, but doesn't carry real tenants. Dynamic data is replaced with explicit generators, preserving type, boundaries, and inter-field relationships. Expiry time isn't an auto-invalidation basis: successful re-validation can extend the acceptance window, while failure freezes the bundle for old-version replay. Agent replay evaluation also depends on fixture lineage, but the recipient here is the API consumer, not the Agent decision trajectory.
Attack the gate with "legal but not equivalent"
The failure suite injects unknown enums, nullable/absent swaps, amount changing from cents to yuan, 200 error objects, duplicate cursors, media-type changes, truncated JSON, and connection drops. Then make the Mock accept disabled fields, ignore scope, and mutate the normalizer one by one: delete amounts, collapse nulls, reorder arrays, or expand ignore paths; simultaneously let the producer try to write to non-isolated sinks. The system must kill these mutations via the independent verifier or business oracle and locate the spec, producer, mock, normalizer, or sink edge.
The merge gate requires: 100% scenario schema and required error-branch coverage; current/previous combos all passing; 100% kill rate across 12 failure classes and verifier/oracle/normalizer mutations; zero undeclared ignores; 100% consistency rate of normalized digests between Mock and producer; zero writes outside isolated sinks and zero sensitive-field leaks. Probabilistic interfaces must declare metrics, sample sizes, and intervals; you can't use literal equality to fake stability.
Release first double-runs old and new contract sets in CI, reporting only diffs; then lets a few consumers explicitly opt into the new alias; finally promotes the default generation. If unknown-enum errors increase, double-run diffs exceed the approved baseline, or consumer failure rate rises above 1%, the alias CAS reverts to the old bundle and old Mock engine; the new bundle stays immutable for diagnosis. Migration can't rely on deleting failing snapshots; property testing can supplement boundary generation, but still must bind to the scenario and spec digest here to prove it's testing the current API.
Related
Related Content
Article
Is Next.js App Router a Good Fit for a Technical Community?
Technical trends, engineering practices, and independent development perspectives centered on Next.js and React.
Article
SEO Design for a Content Community with Next.js
Technical trends, engineering practices, and an indie developer's perspective on Next.js and TypeScript.
Article
The Engineering Value of TypeScript in AI Application Frontends
Technical trends, engineering practices, and an independent developer perspective on TypeScript and React.
Article
What Value Do Server Components Offer Content Sites?
Technical trends and engineering practices around Next.js and React, from an independent developer's perspective.