Make every port notice a fixture nothing decodes - #49
Merged
Merged
Conversation
With `items` empty the fixture proved the page envelope decodes and never decoded an Asset through it, so a port whose page carried the wrong element type would have passed in all five languages. All five already model the items as assets, so nothing failed — the fixture now holds them to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TypeScript has asserted since #39 that every fixture in `models.json` has a schema to decode it. Rust, Python, Swift and Kotlin hand-maintained their lists with nothing holding them up against the file, so a thirty-seventh fixture would have been skipped in silence in four languages — the mechanism that let TypeScript drift to 26 of 36 without anyone noticing. Each port now asserts coverage and names what is missing. Python's map was already a dict, so it only needed the assertion; Rust, Swift and Kotlin turn their run of calls into a table keyed by fixture name, which the guard reads. Each was proved to bite by adding a fixture with no registered type and watching that port's suite fail naming it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 16, 2026
Closed
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.
Closes #48.
Since #39 the TypeScript suite has asserted that every fixture in
models.jsonhas a schema to decode it. The other four ports hand-maintained their lists with
nothing holding them up against the file, so a thirty-seventh fixture would have
been silently unchecked in Rust, Python, Swift and Kotlin — the same mechanism
that let TypeScript drift to 26 of 36 without anyone noticing.
What changed
conformance/models.json:assetPagenow carries one real asset. Withitemsempty the fixture proved the envelope decodes and never decoded an
Assetthroughit, so a port whose page held the wrong element type passed everywhere. All five
already type the items as assets, so nothing failed — the fixture now holds them to it.
Python's map was already a dict, so it only needed the assertion; Rust, Swift and
Kotlin turn their run of
checkcalls into a table keyed by fixture name, which theguard reads.
$commentandversiondescribe the file rather than a model, and areskipped in all five the way TypeScript already skipped them.
Proved to bite
A guard that cannot fail is worse than none, so each was run against a fixture
(
assetSidecar) added tomodels.jsonwith no registered type, then the fixture removed:fixtures in the contract with no type to decode them: ["assetSidecar"]AssertionError: fixtures in the contract with no model to decode them: ['assetSidecar']FAIL every fixture in the contract has a type to decode it (1)—fixtures in the contract with no type to decode them: expected [], got ["assetSidecar"]AssertionFailedError: fixtures in the contract with no type to decode them ==> expected: <[]> but was: <[assetSidecar]>Additive?
Yes — additive, and in substance a test-only change. Nothing on the wire moves: no
production code is touched, no endpoint, request field or response field is added or
retyped. The
assetPagepayload gains an item, which changes what every port mustdecode in its own test run — but it asks for nothing a server has to send, and an
AssetPagecarrying assets is what the contract always said. The one way this couldbreak a consumer is if a port had modelled the page's items as something other than
Asset; all five were checked and none had, and that disagreement is exactly what theitem exists to catch from now on.
One limit, carried over
items.0.duration: nullinherits the limitation #47 wrote down: every port's walkerreads an absent key as null, so a null assert cannot tell a dropped field from a carried
one. The other nine
items.0.*asserts are non-null and do catch a dropped field, so theitem is exercised either way — but the null one is a weaker check than it looks.
Verified
typescript:bun install && bun run verify— 201 pass, 0 failrust:cargo test && cargo clippy --all-targets && cargo fmt --check— 18 conformance tests pass, clippy clean, fmt cleanpython:uv run pytest && uv run ruff check && uv run ruff format --check .— 55 passed, cleanswift:swift run ImogenSDKConformance— 258 checks passedkotlin:./gradlew build --rerun-tasks— BUILD SUCCESSFULFound, not fixed
testtask does not declareconformance/as an input (it reads the filesthrough
user.dirat run time), so a contract-only change leaves./gradlew buildUP-TO-DATEand the suite does not re-run locally. CI starts clean, so it only bites adeveloper on loki;
--rerun-tasksworks around it. Nit-to-significant, not filed.🤖 Generated with Claude Code