test(807): make "threaded through every branch" a checked property
Build ErsatzTV Image / CI toolchain image resolves (pull_request) Successful in 11s
PR Gates / CI image pin matches docker/ci (pull_request) Successful in 15s
Build ErsatzTV Image / Delimiter ban (release path) (pull_request) Successful in 40s
PR Gates / Fix proofs (Proves trailers) (pull_request) Successful in 10s
PR Gates / Docs update reminder (pull_request) Successful in 45s
PR Gates / decisions lifecycle (pull_request) Successful in 23s
PR Gates / Script lint and tests (ruff + pytest) (pull_request) Successful in 6m0s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m49s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 6m11s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Skipped
Review verdict / Set review-verdict status (pull_request_target) Successful in 9s
review-verdict/h10 Review-verdict: MERGEABLE @ dafa564 (base: main)
Build ErsatzTV Image / Functional E2E (curl + UI contracts) (pull_request) Successful in 5m45s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 12s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 8s

`_resolve` returns three sets and the third has to reach every composition site.
That was a claim in a docstring; each site is a separate opportunity to forget,
and forgetting is silent — no error, just a smaller population.

Ten sites enumerated and asserted: top level, `allOf` arm, `if`, `then`, `else`,
`dependentSchemas`, `oneOf` arm, `anyOf` arm, `items`, `prefixItems`. All ten
currently thread it. Witnessed load-bearing by dropping the threading at three of
them — `allOf` reddens 1 case, `oneOf`/`anyOf` 2, `items` 2.

The value is not today's ten passes; it is that a NEW composition keyword has one
visible place to be added to, instead of the omission being spread across the
resolver where nothing would report it.

956 passed 2 skipped, ruff check 0, ruff format 0.
This commit is contained in:
2026-08-23 01:36:32 +02:00
parent 424ffb573b
commit dafa56452c
@@ -665,6 +665,43 @@ def test_composition_is_resolved_the_way_JSON_Schema_means_it(case: str, schema:
assert sorted(_optional_of(schema)) == expected, case
_NESTED_PROBE = {
"properties": {"outer": {"properties": {"req": {}, "opt": {}}, "required": ["req"]}},
"required": ["outer"],
}
@pytest.mark.parametrize(
("site", "schema"),
[
("top level", _NESTED_PROBE),
("allOf arm", {"allOf": [_NESTED_PROBE]}),
("if", {"if": _NESTED_PROBE}),
("then", {"then": _NESTED_PROBE}),
("else", {"else": _NESTED_PROBE}),
("dependentSchemas", {"dependentSchemas": {"trigger": _NESTED_PROBE}}),
("oneOf arm", {"oneOf": [_NESTED_PROBE]}),
("anyOf arm", {"anyOf": [_NESTED_PROBE]}),
("items", {"type": "array", "items": _NESTED_PROBE}),
("prefixItems", {"type": "array", "prefixItems": [_NESTED_PROBE]}),
],
)
def test_a_nested_inline_object_survives_EVERY_composition_site(site: str, schema: dict) -> None:
"""`_resolve` returns three sets, and the third has to be threaded through every branch.
A composition site that unions `properties` and `required` but forgets `nested_optional` loses
the nested member silently — no error, just a smaller population — and each site is a separate
opportunity to forget. Enumerating the sites here is what makes "threaded through every branch"
a checked property instead of a claim in a docstring; a NEW composition keyword must be added
to this list, and if it is not, the omission is at least visible in one place rather than
spread across the resolver.
"""
assert "outer.opt" in _optional_of(schema), (
f"a nested inline object under `{site}` lost its optional member — `nested_optional` is "
"not threaded through that branch of `_resolve`"
)
def test_a_ref_ARM_is_covered_by_the_component_walk_not_by_this_one() -> None:
"""The two walks COMPOSE; neither alone covers an inline body with a `$ref` arm.