From 427b3dc935b736728785c865f30bb030dc2664e9 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 14 Aug 2026 23:51:01 +0200 Subject: [PATCH 1/2] =?UTF-8?q?test:=20#265=20=E2=80=94=20a=20proof=20that?= =?UTF-8?q?=20drove=20a=20copy=20of=20its=20subject,=20and=20a=20boundary?= =?UTF-8?q?=20that=20was=20wrong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C-89's two orphaned deferrals, both discharged. THE PROOF DROVE A COPY. test_the_drift_check_would_catch_a_rename rebuilt the gated check's comparison with its own comprehensions instead of calling it, so blanking the real check's assertions left the proof green — a proof of a reimplementation. Its two siblings had already been repaired the same way via _describe_changes, which makes this a second incident rather than a guessed abstraction. The comparison is now _name_and_class_drift, called by both. Mutation-proven: making it return ([], {}) fails the proof for both partners, where before it passed. It also gained an assertion that the report names BOTH sides of a mismatch — what this package expects and what the registry declares — because a reader who cannot tell which one moved cannot act on it. THE RELOCATION IS ANSWERED "no", with the reason recorded rather than deferred a third time. The coordinate-value scan STAYS in test_env_declaration.py. Its subject is _EXPECTED_NAMES, derived from _PARTNER_ENV — the declaration of what each partner reads, which is the substance of that module. Moving a guard away from the declaration it guards so a filename reads better trades a real coupling for a filing convenience, and would mean exporting a private name from one test module into another. What was actually misfiled moved instead. registry_at / registry_current / rows are the shared READER, not this package's environment declarations, and their refusal tests plus _scratch_repo are now tests/test_seam_registry.py — 115 lines out, no shared private state left behind. test_env_declaration.py is 1406 lines against 1503 before; the split removed more than that and the shared comparison put some back. Same 457 tests before and after the move, which is the point: a file split that changes the count has moved something it should not have. Suite 457 passed / 1 skipped / 39 xfailed, ruff clean. Closes #265. Co-Authored-By: Claude Opus 5 (1M context) --- reports/technical_risk_register.md | 8 +- tests/test_env_declaration.py | 159 ++++++----------------------- tests/test_seam_registry.py | 140 +++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 129 deletions(-) create mode 100644 tests/test_seam_registry.py diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 6e1f082..6788ad7 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -370,7 +370,13 @@ What actually keeps it open is neither of those — it is the two deferrals belo - Deferral 1's trigger is *"when #243 finishes touching `tests/test_env_declaration.py`"*. #243 finished. The leak guards are still in that file; `tests/test_redaction_guard.py` is only cross-referenced. - Deferral 2 was *"routed to #243"* — and #243 closed without it. `test_the_drift_check_would_catch_a_rename` still rebuilds its subject's comparison with its own comprehension rather than driving the checked function. -Both were unowned, which is worse than deferred — **now filed as #265**, with acceptance criteria and the reason each is not urgent. That issue closing is what closes this entry: its stated condition was already met by #243. *(An earlier draft of this amendment named the problem and left it there, which under ADR-014 §4 converts two compliant deferrals into two non-compliant items. Naming is not rehoming.)* +Both were unowned, which is worse than deferred — filed as **#265**, and **both discharged 2026-08-14**. *(An earlier draft of this amendment named the problem and left it there, which under ADR-014 §4 converts two compliant deferrals into two non-compliant items. Naming is not rehoming.)* + +**Deferral 2 is fixed as filed.** The comparison the gated check ran inline is now `_name_and_class_drift`, called by both it and the proof, so the proof drives its subject instead of a copy of it. Mutation-proven: making that function return `([], {})` now fails the proof for both partners, where before it stayed green. It also gained an assertion that the report names *both* sides of a mismatch — what this package expects and what the registry declares — since a reader who cannot tell which moved cannot act on it. + +**Deferral 1 is answered "no", with the reason recorded rather than deferred a third time.** The coordinate-value scan **stays in `tests/test_env_declaration.py`**. Its subject is `_EXPECTED_NAMES`, derived from `_PARTNER_ENV` — the declaration of what each partner reads, which is the substance of that module. Moving a guard away from the declaration it guards, so that a filename reads better, trades a real coupling (CCP) for a filing convenience, and would require exporting a private name from one test module into another. + +What *was* misfiled moved instead: `registry_at` / `registry_current` / `rows` are the shared **reader**, not this package's environment declarations, and their five refusal tests plus `_scratch_repo` are now `tests/test_seam_registry.py`. That is the boundary that was actually wrong: **115 lines out**, no shared private state left behind. `test_env_declaration.py` is 1406 lines against 1503 before this change — the split removed more than that and the shared comparison above put some back. **Two deferrals, both with triggers (ADR-014 §4).** diff --git a/tests/test_env_declaration.py b/tests/test_env_declaration.py index 9632514..f772b1f 100644 --- a/tests/test_env_declaration.py +++ b/tests/test_env_declaration.py @@ -35,15 +35,10 @@ from tests.seam_registry import ( ABSENT as _ABSENT, - REGISTRY_RELPATH, REGISTRY_RELPATH as _REGISTRY_RELPATH, - RegistryReadError, RegistryReadError as _RegistryReadError, - registry_at, registry_at as _registry_at, - registry_current, registry_current as _registry_current, - rows, rows as _rows, ) from tests.conftest import ( @@ -629,6 +624,26 @@ def _declared_classes(registry: dict) -> dict[str, str]: return {n: row[1] for n, row in _rows(registry, _CONSUMED_TABLES).items()} +def _name_and_class_drift(expected_class: dict, declared: dict) -> tuple[list, dict]: + """What this package expects vs what the registry declares. + + Returns ``(names the registry does not carry, {name: (expected, declared)})``. + + Extracted 2026-08-14 (issue #265) so the gated check below and the ungated proof of + it further down run the **same** comparison. They did not: the proof rebuilt this + with its own comprehensions, so blanking the assertions here left it green. Its two + siblings had already been repaired the same way via ``_describe_changes`` — a second + incident, not a guessed abstraction. + """ + missing = sorted(n for n in expected_class if n not in declared) + misclassified = { + n: (expected, declared[n]) + for n, expected in expected_class.items() + if n in declared and declared[n] != expected + } + return missing, misclassified + + @pytest.mark.parametrize("partner", _PARTNERS) def test_every_declared_name_exists_in_the_registry_with_the_class_we_treat_it_as(partner): """C-57: a rename or reclassification upstream must not be silent here.""" @@ -636,17 +651,12 @@ def test_every_declared_name_exists_in_the_registry_with_the_class_we_treat_it_a declared = _declared_classes(_registry_current(repo)) _, _, expected_class = _PARTNER_ENV[partner] - missing = sorted(n for n in expected_class if n not in declared) + missing, misclassified = _name_and_class_drift(expected_class, declared) assert not missing, ( f"[{partner}] names this package requires are absent from the Appwrite Seam " f"Contract's registry: {missing}. Either the registry retired them or this " "module invented them; the registry is the authority." ) - misclassified = { - n: (expected, declared[n]) - for n, expected in expected_class.items() - if declared[n] != expected - } assert not misclassified, ( f"[{partner}] class mismatch (expected, registry) {misclassified}. Class is " "DECLARED by the registry, never inferred from a name's prefix — a coordinate " @@ -1140,17 +1150,20 @@ def test_the_drift_check_would_catch_a_rename(partner): "not the mutation this test believes it is" ) assert "APPWRITE_DATASTORE_PROJECT_ID" not in declared, "fixture should omit it" - missing = sorted(n for n in expected_class if n not in declared) - assert missing, "the detector reported no missing names against a registry that omits most" - mismatched = [ - n for n, expected in expected_class.items() - if n in declared and declared[n] != expected - ] - assert canary in mismatched, ( + # The gated check's own comparison, not a copy of it (issue #265). This rebuilt the + # logic with its own comprehensions until 2026-08-14, which made it a proof of a + # reimplementation: blank the real check's assertions and it stayed green. + missing, misclassified = _name_and_class_drift(expected_class, declared) + assert missing, "the detector reported no missing names against a registry that omits most" + assert canary in misclassified, ( f"[{partner}] a target reclassified as a secret went unnoticed — that is the " "case where getting it wrong leaks or hides a value" ) + assert misclassified[canary] == ("target", "secret"), ( + "the detector must report BOTH sides of the mismatch — what this package " + "expects and what the registry declares — or a reader cannot tell which moved" + ) def _docstring_nodes(tree: ast.AST) -> set[int]: @@ -1391,113 +1404,3 @@ def test_the_scan_understands_every_assignment_form_this_repo_writes(): "document that introduced it — that is the stopping rule, and it is why the " "form list is derived from this repository's own corpus rather than invented." ) - - -def _scratch_repo(tmp_path: Path): - """A throwaway git repo whose registry differs on `main`, on `origin/main`, and on disk. - - `-c` rather than `git config`: a contributor's global `commit.gpgsign` or - `core.hooksPath` would otherwise reach in and either fail opaquely or block on - pinentry with no timeout. - """ - def git(*args): - return subprocess.run( - ["git", "-C", str(tmp_path), "-c", "commit.gpgsign=false", - "-c", "core.hooksPath=/dev/null", *args], - capture_output=True, text=True, check=True, timeout=30, - ) - - target = tmp_path / REGISTRY_RELPATH - target.parent.mkdir(parents=True) - - def edition(marker: str) -> str: - return f'[meta]\nversion = "{marker}"\n\n[connection.X]\nclass = "connection"\n' - - git("init", "-q", "-b", "main") - git("config", "user.email", "t@t") - git("config", "user.name", "t") - target.write_text(edition("on-main")) - git("add", "-A") - git("commit", "-q", "-m", "main") - - # a remote-tracking ref that is AHEAD of main, so preferring one over the other shows - git("checkout", "-q", "-b", "upstream") - target.write_text(edition("on-origin-main")) - git("add", "-A") - git("commit", "-q", "-m", "origin") - git("update-ref", "refs/remotes/origin/main", "HEAD") - git("checkout", "-q", "main") - - # and a dirty working tree, which is what #196 was about - target.write_text(edition("in-the-working-tree")) - return tmp_path - - -def test_registry_current_reads_origin_main_not_the_working_tree(tmp_path): - """The reason `tests/seam_registry.py` exists, and until now the only untested part. - - A sibling clone sits on whatever branch its own agent last worked on. Comparing - against that grades this repository on unreviewed content — issue #196, which cost a - withdrawn pull request. Replacing this function with a working-tree or `HEAD` read - used to leave the whole suite green. - """ - repo = _scratch_repo(tmp_path) - assert registry_current(repo)["meta"]["version"] == "on-origin-main", ( - "registry_current read something other than origin/main. A working-tree read is " - "#196 verbatim; a bare `main` read misses that the sibling's remote has moved." - ) - - -def test_registry_current_refuses_a_repo_with_neither_ref(tmp_path): - """No `origin/main` and no `main` must say so, not return an empty registry.""" - subprocess.run(["git", "init", "-q", str(tmp_path)], - capture_output=True, text=True, check=True, timeout=30) - with pytest.raises(RegistryReadError, match="neither origin/main nor main"): - registry_current(tmp_path) - - -def test_registry_at_refuses_a_commit_whose_registry_is_missing_or_unparseable(tmp_path): - """`git show` failing, and a blob that is not TOML — two refusal branches nothing reached.""" - def git(*args): - return subprocess.run( - ["git", "-C", str(tmp_path), "-c", "commit.gpgsign=false", - "-c", "core.hooksPath=/dev/null", *args], - capture_output=True, text=True, check=True, timeout=30, - ) - git("init", "-q", "-b", "main") - git("config", "user.email", "t@t") - git("config", "user.name", "t") - - (tmp_path / "unrelated.txt").write_text("no registry here\n") - git("add", "-A") - git("commit", "-q", "-m", "no registry") - absent = git("rev-parse", "--short", "HEAD").stdout.strip() - - target = tmp_path / REGISTRY_RELPATH - target.parent.mkdir(parents=True) - target.write_text("this is not toml = = =\n") - git("add", "-A") - git("commit", "-q", "-m", "not toml") - garbage = git("rev-parse", "--short", "HEAD").stdout.strip() - - with pytest.raises(RegistryReadError, match="cannot read the registry"): - registry_at(tmp_path, absent) - with pytest.raises(RegistryReadError, match="did not parse as TOML"): - registry_at(tmp_path, garbage) - - -def test_rows_refuses_a_section_whose_entries_are_not_tables(): - """`[test_environment]` on the live registry is scalars, not sub-tables. - - Nothing breaks today because that table is IGNORED — but the partition check's own - remediation message tells a maintainer to classify a new table CONSUMED, and doing - that for one written this way used to return an `AttributeError` from a dict - comprehension. Register C-91. - """ - scalars = {"test_environment": {"status": "none", "fact": "a sentence"}} - with pytest.raises(RegistryReadError, match=r"\[test_environment\]\.(status|fact) is a bare str"): - rows(scalars, ("test_environment",)) - - # and the ordinary shape still works, or the refusal above proves nothing - tables = {"target": {"APPWRITE_X": {"class": "target", "value": "v"}}} - assert rows(tables, ("target",)) == {"APPWRITE_X": ("target", "target", "v")} diff --git a/tests/test_seam_registry.py b/tests/test_seam_registry.py new file mode 100644 index 0000000..039b327 --- /dev/null +++ b/tests/test_seam_registry.py @@ -0,0 +1,140 @@ +"""Refusals of the shared seam-registry reader (`tests/seam_registry.py`). + +These moved out of `tests/test_env_declaration.py` on 2026-08-14 (issue #265). Their +subject is the *reader* — `registry_at`, `registry_current`, `rows` — not what this +package declares about its environment, and a 1503-line module that had become the home +for both was the clearest signal in the repo that a boundary was wrong. + +What deliberately did **not** move with them: the coordinate-value scan. Its subject is +`_EXPECTED_NAMES`, derived from the partner declarations that are the substance of +`test_env_declaration.py`, and separating a guard from the declaration it guards to make +a filename read better trades a real coupling for a filing convenience. The reasoning is +in register C-89. + +Every refusal here is a real failure someone hit: an empty pin, a commit this clone does +not have, a ref that is not a commit, a registry that will not parse, and a section whose +rows are scalars rather than tables (C-91). +""" + +import subprocess +from pathlib import Path + +import pytest + +from tests.seam_registry import ( + REGISTRY_RELPATH, + RegistryReadError, + registry_at, + registry_current, + rows, +) + + +def _scratch_repo(tmp_path: Path): + """A throwaway git repo whose registry differs on `main`, on `origin/main`, and on disk. + + `-c` rather than `git config`: a contributor's global `commit.gpgsign` or + `core.hooksPath` would otherwise reach in and either fail opaquely or block on + pinentry with no timeout. + """ + def git(*args): + return subprocess.run( + ["git", "-C", str(tmp_path), "-c", "commit.gpgsign=false", + "-c", "core.hooksPath=/dev/null", *args], + capture_output=True, text=True, check=True, timeout=30, + ) + + target = tmp_path / REGISTRY_RELPATH + target.parent.mkdir(parents=True) + + def edition(marker: str) -> str: + return f'[meta]\nversion = "{marker}"\n\n[connection.X]\nclass = "connection"\n' + + git("init", "-q", "-b", "main") + git("config", "user.email", "t@t") + git("config", "user.name", "t") + target.write_text(edition("on-main")) + git("add", "-A") + git("commit", "-q", "-m", "main") + + # a remote-tracking ref that is AHEAD of main, so preferring one over the other shows + git("checkout", "-q", "-b", "upstream") + target.write_text(edition("on-origin-main")) + git("add", "-A") + git("commit", "-q", "-m", "origin") + git("update-ref", "refs/remotes/origin/main", "HEAD") + git("checkout", "-q", "main") + + # and a dirty working tree, which is what #196 was about + target.write_text(edition("in-the-working-tree")) + return tmp_path + + +def test_registry_current_reads_origin_main_not_the_working_tree(tmp_path): + """The reason `tests/seam_registry.py` exists, and until now the only untested part. + + A sibling clone sits on whatever branch its own agent last worked on. Comparing + against that grades this repository on unreviewed content — issue #196, which cost a + withdrawn pull request. Replacing this function with a working-tree or `HEAD` read + used to leave the whole suite green. + """ + repo = _scratch_repo(tmp_path) + assert registry_current(repo)["meta"]["version"] == "on-origin-main", ( + "registry_current read something other than origin/main. A working-tree read is " + "#196 verbatim; a bare `main` read misses that the sibling's remote has moved." + ) + + +def test_registry_current_refuses_a_repo_with_neither_ref(tmp_path): + """No `origin/main` and no `main` must say so, not return an empty registry.""" + subprocess.run(["git", "init", "-q", str(tmp_path)], + capture_output=True, text=True, check=True, timeout=30) + with pytest.raises(RegistryReadError, match="neither origin/main nor main"): + registry_current(tmp_path) + + +def test_registry_at_refuses_a_commit_whose_registry_is_missing_or_unparseable(tmp_path): + """`git show` failing, and a blob that is not TOML — two refusal branches nothing reached.""" + def git(*args): + return subprocess.run( + ["git", "-C", str(tmp_path), "-c", "commit.gpgsign=false", + "-c", "core.hooksPath=/dev/null", *args], + capture_output=True, text=True, check=True, timeout=30, + ) + git("init", "-q", "-b", "main") + git("config", "user.email", "t@t") + git("config", "user.name", "t") + + (tmp_path / "unrelated.txt").write_text("no registry here\n") + git("add", "-A") + git("commit", "-q", "-m", "no registry") + absent = git("rev-parse", "--short", "HEAD").stdout.strip() + + target = tmp_path / REGISTRY_RELPATH + target.parent.mkdir(parents=True) + target.write_text("this is not toml = = =\n") + git("add", "-A") + git("commit", "-q", "-m", "not toml") + garbage = git("rev-parse", "--short", "HEAD").stdout.strip() + + with pytest.raises(RegistryReadError, match="cannot read the registry"): + registry_at(tmp_path, absent) + with pytest.raises(RegistryReadError, match="did not parse as TOML"): + registry_at(tmp_path, garbage) + + +def test_rows_refuses_a_section_whose_entries_are_not_tables(): + """`[test_environment]` on the live registry is scalars, not sub-tables. + + Nothing breaks today because that table is IGNORED — but the partition check's own + remediation message tells a maintainer to classify a new table CONSUMED, and doing + that for one written this way used to return an `AttributeError` from a dict + comprehension. Register C-91. + """ + scalars = {"test_environment": {"status": "none", "fact": "a sentence"}} + with pytest.raises(RegistryReadError, match=r"\[test_environment\]\.(status|fact) is a bare str"): + rows(scalars, ("test_environment",)) + + # and the ordinary shape still works, or the refusal above proves nothing + tables = {"target": {"APPWRITE_X": {"class": "target", "value": "v"}}} + assert rows(tables, ("target",)) == {"APPWRITE_X": ("target", "target", "v")} From 7845048ef0911ae613f800882540333e6cc0e800 Mon Sep 17 00:00:00 2001 From: Polichinl Date: Fri, 14 Aug 2026 23:54:27 +0200 Subject: [PATCH 2/2] docs: two of my own claims in this change were wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Focused verification of a test-only change — lighter than the full multi-agent gate the last two stories got, because nothing under views_postprocessing/ moved, the test count is identical before and after, and both new behaviours are mutation-proven. Saying which gate ran, rather than implying the same one. It still found two false claims, both mine, both the measure-don't-recall rule. "115 LINES OUT" was invented. Measured: the moved block is 109 lines of test code, and test_env_declaration.py went 1503 -> 1406, a net 97, because the shared comparison put some back. No command had produced 115. "ITS TWO SIBLINGS HAD ALREADY BEEN REPAIRED THE SAME WAY VIA _describe_changes" is not what happened. Reading the sibling's own docstring: the rotation proof CALLING _describe_changes was part of the defect, not the repair — it drives "the helper underneath" rather than the check. The actual repair was a new test driving the real check under monkeypatch. That matters because it was load-bearing: I used it as the WET-before-DRY licence for extracting a shared function, and a precedent that does not exist cannot license anything. The extraction is still right, for a reason now stated honestly — the comparison is a pure function of two dicts and both callers want exactly it, so sharing it is the same guarantee with less machinery than monkeypatching a gated check. What justifies it is that this is the second time the pattern has bitten, not a rule about line counts. Also verified and true: the new module imports nothing private from the old one, and nothing anywhere else referenced the moved tests by name. Suite 457 passed / 1 skipped / 39 xfailed, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) --- reports/technical_risk_register.md | 4 ++-- tests/test_env_declaration.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/reports/technical_risk_register.md b/reports/technical_risk_register.md index 6788ad7..e6ef47e 100644 --- a/reports/technical_risk_register.md +++ b/reports/technical_risk_register.md @@ -372,11 +372,11 @@ What actually keeps it open is neither of those — it is the two deferrals belo Both were unowned, which is worse than deferred — filed as **#265**, and **both discharged 2026-08-14**. *(An earlier draft of this amendment named the problem and left it there, which under ADR-014 §4 converts two compliant deferrals into two non-compliant items. Naming is not rehoming.)* -**Deferral 2 is fixed as filed.** The comparison the gated check ran inline is now `_name_and_class_drift`, called by both it and the proof, so the proof drives its subject instead of a copy of it. Mutation-proven: making that function return `([], {})` now fails the proof for both partners, where before it stayed green. It also gained an assertion that the report names *both* sides of a mismatch — what this package expects and what the registry declares — since a reader who cannot tell which moved cannot act on it. +**Deferral 2 is fixed as filed.** The comparison the gated check ran inline is now `_name_and_class_drift`, called by both it and the proof, so the proof drives its subject instead of a copy of it. Mutation-proven: making that function return `([], {})` now fails the proof for both partners, where before it stayed green. *(The sibling defects were repaired by driving the real check under `monkeypatch` instead; here the comparison is a pure function of two dicts that both callers want whole, so sharing it is the same guarantee with less machinery.)* It also gained an assertion that the report names *both* sides of a mismatch — what this package expects and what the registry declares — since a reader who cannot tell which moved cannot act on it. **Deferral 1 is answered "no", with the reason recorded rather than deferred a third time.** The coordinate-value scan **stays in `tests/test_env_declaration.py`**. Its subject is `_EXPECTED_NAMES`, derived from `_PARTNER_ENV` — the declaration of what each partner reads, which is the substance of that module. Moving a guard away from the declaration it guards, so that a filename reads better, trades a real coupling (CCP) for a filing convenience, and would require exporting a private name from one test module into another. -What *was* misfiled moved instead: `registry_at` / `registry_current` / `rows` are the shared **reader**, not this package's environment declarations, and their five refusal tests plus `_scratch_repo` are now `tests/test_seam_registry.py`. That is the boundary that was actually wrong: **115 lines out**, no shared private state left behind. `test_env_declaration.py` is 1406 lines against 1503 before this change — the split removed more than that and the shared comparison above put some back. +What *was* misfiled moved instead: `registry_at` / `registry_current` / `rows` are the shared **reader**, not this package's environment declarations, and their five refusal tests plus `_scratch_repo` are now `tests/test_seam_registry.py`. That is the boundary that was actually wrong: **109 lines of test code moved out**, no shared private state left behind — the new module imports only from `tests/seam_registry.py`. `test_env_declaration.py` is 1406 lines against 1503 before this change — the split removed more than that and the shared comparison above put some back. **Two deferrals, both with triggers (ADR-014 §4).** diff --git a/tests/test_env_declaration.py b/tests/test_env_declaration.py index f772b1f..8964300 100644 --- a/tests/test_env_declaration.py +++ b/tests/test_env_declaration.py @@ -631,9 +631,15 @@ def _name_and_class_drift(expected_class: dict, declared: dict) -> tuple[list, d Extracted 2026-08-14 (issue #265) so the gated check below and the ungated proof of it further down run the **same** comparison. They did not: the proof rebuilt this - with its own comprehensions, so blanking the assertions here left it green. Its two - siblings had already been repaired the same way via ``_describe_changes`` — a second - incident, not a guessed abstraction. + with its own comprehensions, so blanking the assertions here left it green. + + The two sibling defects were repaired differently — by adding a test that drives the + real check under ``monkeypatch`` (``test_the_drift_check_fires_when_a_row_this_partner_reads_rotates``). + That works there because the check reads a registry the test can substitute. Here the + comparison is a pure function of two dicts and both callers want exactly it, so + sharing the function is the same guarantee with less machinery. Extracting is + justified by *this* being the second time the pattern has bitten, not by a rule about + line counts. """ missing = sorted(n for n in expected_class if n not in declared) misclassified = {