"""Guard: object retirement is declared once, and every derived marker agrees (issue #4881). Retirement used to be recorded in four hand-maintained places — a label suffix, a prose description, a `Legacy_Table_Retired_Guard` validation rule, and a list in the MCP server — with nothing binding them together. They drifted, and the drift was invisible: an audit of `PoC__c` checked the guard rule, the row count, the active business rules and the UI wiring, got four signals reading "live", and concluded wrong. The object's own label said `(Legacy)`. `lifecycleStatus` on the object spec is now the single declaration. These assertions are what stops the other markers drifting away from it again. Two properties matter more than the individual checks: 1. **Both directions.** Every declared object must carry its markers, AND every object carrying a marker must be declared. A one-way check passes on exactly the shape that shipped: `Opportunity_Product__c` had the guard rule and no suffix, so "declared objects have their markers" alone would have found nothing to say about it. 2. **No second copy of the list.** Nothing here enumerates which objects are retired — that would make this file the fifth hand-maintained copy and recreate the problem one layer up. The declarations are read from the pack; only the *rules* relating a declaration to its derived markers live here. Retiring another object requires no edit to this file. The two states are not two spellings of one state, and the difference is not about writes — writes are blocked in both. `deprecated` means the historical records still live here and lookups belong here, so the read path and UI stay open and no guard rule is written. `retired` means the data has moved to the superseding object, so reads go there and this object drops out of discovery. The objective test is whether the migration has completed. """ import json import os import re import subprocess import unittest from copy import deepcopy from pathlib import Path ROOT = Path(__file__).resolve().parents[1] L2_ROOT = ROOT / "packs" / "Salesforce-PingCAP-L2" FINAL_ROOT = ROOT / "packs" / "NOT-EDIT-final-merged-packs" MCP_SERVER = ROOT / "agent-tools" / "nexuscrm-connect" / "mcp" / "server.mjs" GUARD_VR_NAME = "Legacy_Table_Retired_Guard" # The rules, not the data. A state's markers are fully determined by these. SUFFIX = {"deprecated": "(Legacy)", "retired": "(Legacy — Retired)"} NEEDS_GUARD_VR = {"deprecated": False, "retired": True} # Listing visibility is the third derived behaviour, but it is a runtime decision inside the MCP # server rather than a fact in the pack, so it is asserted there — see the "deprecated objects stay # VISIBLE but are closed to writes" check in server.mjs's selftest. # Generated description prefix. The issue's objection to prose — "unreliable for # an exact match" — holds only while it is hand-written; anchored and generated # from the declaration it is exact. # # It is kept readable rather than machine-shaped (`[lifecycle:retired]`), but NOT # for the reason this comment used to give. It claimed the prefix "renders # verbatim to admins on the object detail page". It does not, and never did: # Setup's object detail shows `-` for every object, including ones with a # description, because the page reads describe — which has no object-level # description field, matching Salesforce — and the response adapter would drop # the field even if it were there (app-packs#604). # # The audience that does read this is the one the issue is about: whoever reaches # the object through SOQL, a direct database query, or the pack files. Readable # wording is for them. # The trailing `(?:\s|$)` rather than a literal space: a description consisting of nothing but the # generated prefix is the natural output for an object with no extra prose, and requiring a space # after it would make that parse as "no declaration" — failing safe, but with a message pointing at # the wrong thing, and it would bite the generator in PR 2. PREFIX_RE = re.compile( r"^(?PDEPRECATED|RETIRED) — superseded by (?P\S+) " r"\((?P\d{4}-\d{2}-\d{2})\)\. (?P[^\n]+?\.)(?:\s|$)" ) INSTRUCTION = { "deprecated": "Historical lookups belong here; do not create or update.", # Formatted with the declared successor: naming it is the whole point of the # retired state, and a fixed string would let the successor drift silently. "retired": "Read from {superseded_by} instead; do not create or update here.", } # Anything a human or a grep would read as a retirement mark. Used only by the reverse checks, to # catch an object that carries a mark without a declaration. Both are case-insensitive: the label # one was not, so `Foo (legacy)` walked past it while `Foo (Legacy)` was caught — an asymmetry with # no reason behind it, since the point is to notice what a reader would notice. MARKED_LABEL_RE = re.compile(r"[(\[]\s*(?:Legacy|Retired|Deprecated)\b", re.IGNORECASE) MARKED_DESCRIPTION_RE = re.compile(r"^\s*(?:RETIRED|DEPRECATED|Retired by)\b", re.IGNORECASE) def load_json(path: Path): return json.loads(path.read_text()) def objects_in(root: Path) -> dict: """Every object spec in either pack layout, keyed by apiName. L2 keeps one file per object; the build merges them into a single standard_objects_spec.json list. """ per_object_dir = root / "objects" merged = per_object_dir / "standard_objects_spec.json" if merged.exists(): return {obj["apiName"]: obj for obj in load_json(merged)} out = {} for path in sorted(per_object_dir.glob("*.json")): spec = load_json(path) for obj in spec if isinstance(spec, list) else [spec]: out[obj["apiName"]] = obj return out def guard_rule_objects(root: Path) -> set: rules = load_json(root / "validation" / "standard_validation_rules.json")[ "validationRules" ] # Match on name, not id: the build rewrites L2 ids with a `vrule_` prefix, so # an id comparison passes on L2 and silently finds nothing in the final pack. return {r["objectApiName"] for r in rules if r["name"] == GUARD_VR_NAME} def mcp_object_lifecycle() -> dict: """The DERIVED lifecycle map the MCP tools actually consult. Deliberately not the `SEMANTICS_DEFAULTS` literal in the source. The first version of the sibling semantics guard read the dictionary and passed while the derive step was dropping entries on the floor — checking the artifact you designed instead of the one that ships is the failure this file exists to prevent, and it should not be reintroduced inside it. """ out = subprocess.run( ["node", str(MCP_SERVER), "--dump-semantics"], capture_output=True, text=True, check=True, # Pinned to a path that cannot exist so the server falls back to its builtin defaults. The # server reads ~/.nexuscrm-connect/semantics.json when this is unset, so a developer with a # local override file was testing their machine's config instead of the repo's. env={**os.environ, "NEXUS_SEMANTICS_FILE": os.devnull}, ) return json.loads(out.stdout)["object_lifecycle"] def declaration(obj: dict): """The object's lifecycle declaration, or None. Two carriers, deliberately. In the L2 source the declaration is the `lifecycleStatus` / `supersededBy` / `retiredOn` keys. The build strips them — L0's object-spec struct is closed and decodes with a plain json.Unmarshal, so an unrecognised key installs green and evaporates — and the state reaches runtime in the generated description prefix instead, which direct DB reads, SOQL, describe and pack-file reads can all already see. So in the built artifact the prefix IS the declaration. Reading it here is not a fallback for convenience: it is the assertion that stripping the keys did not drop the fact on the floor. """ status = obj.get("lifecycleStatus") match = PREFIX_RE.match(obj.get("description") or "") if status: return { "status": status, "supersededBy": obj.get("supersededBy"), "retiredOn": obj.get("retiredOn"), "from_key": True, } if match: return { "status": match.group("state").lower(), "supersededBy": match.group("superseded_by"), "retiredOn": match.group("retired_on"), "from_key": False, } return None def declared(objects: dict) -> dict: out = {} for name, obj in objects.items(): decl = declaration(obj) if decl: out[name] = obj return out def check_declarations(objects: dict, guarded: set, mcp: dict, derived: bool = True) -> list: """Every inconsistency between the declarations and their derived markers. Returns a list of human-readable problems rather than asserting inline, so the regression tests below can feed it known-bad shapes and assert it notices. A guard that is never shown a failing input is a guard nobody has tested. """ problems = [] declarations = declared(objects) for name, obj in sorted(declarations.items()): decl = declaration(obj) status = decl["status"] if status not in SUFFIX: problems.append(f"{name}: unknown lifecycleStatus {status!r}") continue superseded_by = decl["supersededBy"] retired_on = decl["retiredOn"] if not superseded_by: problems.append(f"{name}: lifecycleStatus declared without supersededBy") if not retired_on: problems.append(f"{name}: lifecycleStatus declared without retiredOn") # The L2 source no longer carries these: the build derives them from the declaration, so # asserting them there would pin the generator's input to its own output. They are checked # on the built pack, which is where they now come from. if not derived: continue suffix = SUFFIX[status] for key in ("label", "pluralLabel"): value = obj.get(key) if value is None: continue if not value.endswith(suffix): problems.append( f"{name}: {key} {value!r} does not end with {suffix!r} for " f"lifecycleStatus {status!r}" ) # A retired object whose label kept only the shorter `(Legacy)` reads # to a user as the state it is no longer in. other = SUFFIX["deprecated" if status == "retired" else "retired"] if suffix != other and value.endswith(other): problems.append(f"{name}: {key} carries the {other!r} suffix but is {status!r}") # The state/successor/date inside the prefix are NOT compared against the declaration here. # In the built pack the prefix is what `declaration()` parsed, so the comparison is a value # against itself; in the source the build has not written a prefix yet. Cross-carrier # agreement is asserted by test_built_markers_follow_the_source_declarations, which reads # the declaration from one artifact and the prefix from the other. # # The instruction sentence IS checked: it is derived from the status but never parsed back # out, so it can disagree with the state it sits next to. match = PREFIX_RE.match(obj.get("description") or "") if match: expected_instruction = INSTRUCTION[status].format(superseded_by=superseded_by) if match.group("instruction") != expected_instruction: problems.append( f"{name}: description instruction {match.group('instruction')!r} " f"is not the {status!r} instruction {expected_instruction!r}" ) has_guard = name in guarded if has_guard != NEEDS_GUARD_VR[status]: if has_guard: # Writing a guard rule for a deprecated object seals a form the # user can still reach — reachable, fillable, unsaveable. That is # the dead end #4791 shipped, and the reason the read path being # deliberately open makes the guard rule wrong rather than missing. problems.append( f"{name}: is {status!r} but carries {GUARD_VR_NAME}; its UI is " "deliberately open, so a write block creates a reachable dead end" ) else: problems.append(f"{name}: is {status!r} but has no {GUARD_VR_NAME}") entry = mcp.get(name.lower()) if entry is None: problems.append(f"{name}: declared {status!r} but absent from the MCP lifecycle map") else: if entry.get("status") != status: problems.append( f"{name}: MCP says {entry.get('status')!r}, pack declares {status!r}" ) if superseded_by and entry.get("supersededBy") != superseded_by: problems.append( f"{name}: MCP names {entry.get('supersededBy')!r} as successor, " f"pack declares {superseded_by!r}" ) # The date is user-visible — create_record quotes it back in its rejection message — # and it was the field that actually drifted: the semantics document asserted a state # from 2026-07-12 for eleven days after the object moved on 2026-07-23. Comparing the # spec key against its own prose, as the other date check does, compares two copies # inside one file; this is the one that spans the layers that diverged. if retired_on and entry.get("retiredOn") != retired_on: problems.append( f"{name}: MCP dates retirement {entry.get('retiredOn')!r}, " f"pack declares {retired_on!r}" ) return problems def check_nothing_marked_without_declaration(objects: dict, guarded: set, mcp: dict) -> list: """The reverse direction: a marker with no declaration behind it. This is the half that catches what actually shipped. `Opportunity_Product__c` and `Quote_Product__c` carried the guard rule with no suffix and no declaration; a forward-only check has nothing to say about an object it was never told to look at. """ problems = [] declarations = declared(objects) for name in sorted(guarded): if name not in declarations: problems.append( f"{name}: carries {GUARD_VR_NAME} but declares no lifecycleStatus" ) elif declaration(declarations[name])["status"] != "retired": problems.append( f"{name}: carries {GUARD_VR_NAME} but is declared " f"{declaration(declarations[name])['status']!r}, not 'retired'" ) for name, obj in sorted(objects.items()): if name in declarations: continue label = obj.get("label") or "" description = obj.get("description") or "" if MARKED_LABEL_RE.search(label): problems.append( f"{name}: label {label!r} is marked as legacy but declares no lifecycleStatus" ) elif MARKED_DESCRIPTION_RE.match(description): problems.append( f"{name}: description opens as retired but declares no lifecycleStatus" ) for lowered in sorted(mcp): if not any(name.lower() == lowered for name in declarations): problems.append( f"{lowered}: present in the MCP lifecycle map but declared by no object spec" ) return problems class ObjectLifecycleDeclarationTests(unittest.TestCase): def assert_consistent(self, root: Path): objects = objects_in(root) problems = check_declarations( objects, guard_rule_objects(root), mcp_object_lifecycle(), derived=(root == FINAL_ROOT) ) self.assertEqual(problems, [], "\n".join(problems)) def assert_nothing_marked_without_declaration(self, root: Path): objects = objects_in(root) problems = check_nothing_marked_without_declaration( objects, guard_rule_objects(root), mcp_object_lifecycle() ) self.assertEqual(problems, [], "\n".join(problems)) def test_l2_declarations_match_their_markers(self): self.assert_consistent(L2_ROOT) def test_l2_has_no_marker_without_a_declaration(self): self.assert_nothing_marked_without_declaration(L2_ROOT) def test_final_pack_declarations_match_their_markers(self): # The build merges L1 and L2 and rewrites ids. A check that only reads L2 # asserts the source, not the artifact that installs. self.assert_consistent(FINAL_ROOT) def test_final_pack_has_no_marker_without_a_declaration(self): self.assert_nothing_marked_without_declaration(FINAL_ROOT) def test_built_markers_follow_the_source_declarations(self): """The generator's output must match its input — across artifacts, not within one. This is the assertion PR 2 turns on. Every other check reads one artifact and compares it with itself; this one reads the declaration from the L2 source and the markers from the built pack, so a generator that silently stopped deriving, or derived from stale values, fails here and nowhere else. """ source = declared(objects_in(L2_ROOT)) built = objects_in(FINAL_ROOT) self.assertTrue(source, "no declarations in the source — the check would be vacuous") for name, src_obj in sorted(source.items()): with self.subTest(object=name): decl = declaration(src_obj) self.assertTrue( decl["from_key"], f"{name} must declare through the spec key; the prose is the build's output", ) obj = built.get(name) self.assertIsNotNone(obj, f"{name} declared in L2 but absent from the built pack") expected_prefix = ( f"{decl['status'].upper()} — superseded by {decl['supersededBy']} " f"({decl['retiredOn']}). " + INSTRUCTION[decl["status"]].format(superseded_by=decl["supersededBy"]) ) self.assertTrue( (obj.get("description") or "").startswith(expected_prefix), f"{name}: built description does not open with the derived prefix\n" f" expected: {expected_prefix}\n" f" actual: {(obj.get('description') or '')[:len(expected_prefix) + 20]}", ) suffix = SUFFIX[decl["status"]] for key in ("label", "pluralLabel"): value = obj.get(key) if value is None: continue self.assertTrue( value.endswith(suffix), f"{name}: built {key} {value!r} lacks the derived suffix {suffix!r}", ) def test_source_carries_no_derived_marker(self): """The source declares; it must not also hand-maintain what the build writes. A hand-written suffix survives (the derivation is idempotent), so this cannot fail closed on its own — but leaving one there recreates the two-sources-of-truth shape #4881 exists to remove, and the next author would not know which one wins. """ for name, obj in sorted(declared(objects_in(L2_ROOT)).items()): with self.subTest(object=name): for key in ("label", "pluralLabel"): value = obj.get(key) or "" for suffix in SUFFIX.values(): self.assertFalse( value.endswith(suffix), f"{name}: source {key} carries {suffix!r}, which the build derives", ) self.assertIsNone( PREFIX_RE.match(obj.get("description") or ""), f"{name}: source description carries a generated prefix, which the build derives", ) def test_at_least_one_object_of_each_state_is_declared(self): # Not a content assertion — a collapse detector. Every check above is # vacuously true over an empty declaration set, so a refactor that stops # finding declarations would turn this whole file green. states = {declaration(obj)["status"] for obj in declared(objects_in(L2_ROOT)).values()} self.assertEqual(states, set(SUFFIX), "both lifecycle states must be exercised") def test_l2_declares_through_the_spec_key_not_only_the_prose(self): # Without this, `lifecycleStatus` carries no weight: deleting all three keys from all four # L2 specs left every other test green, because `declaration()` fell back to the prose and # the prose was still right. That makes the key a redundant copy rather than the single # declaration this file's docstring claims it is — and PR 2 derives the prose FROM the key, # so a spec that only has prose has nothing to derive from. for name, obj in sorted(declared(objects_in(L2_ROOT)).items()): with self.subTest(object=name): self.assertTrue( declaration(obj)["from_key"], f"{name} is marked as retired but carries no lifecycleStatus key; " "the prose alone is not the declaration", ) # Sample objects for the mutations below. Naming them here is deliberate and is not the "second # copy of the list" the docstring warns about: these are inputs to failure-injection, and a # mutation test built from imagined shapes rather than real ones is how the sibling retired-claims # guard shipped a matcher that only its own author's paraphrase could satisfy. RETIRED_SAMPLE = "Order_Product__c" DEPRECATED_SAMPLE = "PoC__c" def _mutations(): """(id, mutate, which_check, exact expected problem, fixture) for every check in this file. `fixture` picks the carrier a mutation acts on, and the split is forced by the design rather than chosen for convenience. The L2 source declares through `lifecycleStatus` / `supersededBy` / `retiredOn`; the build consumes those keys and the built pack carries the state in the generated description prefix instead. So "declared without supersededBy" is reachable only on the source (the built pack has no key left to remove) and "label lost its suffix" only on the built pack (the source has no suffix to lose). Running every row against one fixture quietly passed the half that could not fire. One row per `problems.append` site. Substring assertions were not enough: several checks emit overlapping phrases, so `any("as successor" in p)` was satisfied by the MCP check while the description check it named was deleted, and both reverse checks covered for each other. Each row now pins its own check by exact message — delete that check and exactly this row goes red. """ def with_desc(name, new_desc): def mutate(objects, guarded, mcp): objects[name]["description"] = new_desc return objects, guarded, mcp return mutate retired_desc = lambda objects: objects[RETIRED_SAMPLE]["description"] rows = [ ( "unknown status", lambda o, g, m: (_set(o, RETIRED_SAMPLE, lifecycleStatus="sunset"), g, m), "forward", f"{RETIRED_SAMPLE}: unknown lifecycleStatus 'sunset'", "source", ), ( "declared without supersededBy", lambda o, g, m: (_pop(o, RETIRED_SAMPLE, "supersededBy"), g, m), "forward", f"{RETIRED_SAMPLE}: lifecycleStatus declared without supersededBy", "source", ), ( "declared without retiredOn", lambda o, g, m: (_pop(o, RETIRED_SAMPLE, "retiredOn"), g, m), "forward", f"{RETIRED_SAMPLE}: lifecycleStatus declared without retiredOn", "source", ), ( "label loses its suffix", lambda o, g, m: (_set(o, RETIRED_SAMPLE, label="Order Product"), g, m), "forward", f"{RETIRED_SAMPLE}: label 'Order Product' does not end with " f"'{SUFFIX['retired']}' for lifecycleStatus 'retired'", "built", ), ( "pluralLabel loses its suffix", lambda o, g, m: (_set(o, RETIRED_SAMPLE, pluralLabel="Order Products"), g, m), "forward", f"{RETIRED_SAMPLE}: pluralLabel 'Order Products' does not end with " f"'{SUFFIX['retired']}' for lifecycleStatus 'retired'", "built", ), ( "retired object wearing the deprecated suffix", lambda o, g, m: (_set(o, RETIRED_SAMPLE, label=f"Order Product {SUFFIX['deprecated']}"), g, m), "forward", f"{RETIRED_SAMPLE}: label carries the '{SUFFIX['deprecated']}' suffix but is 'retired'", "built", ), ( "description uses the other state's instruction", lambda o, g, m: (with_desc( RETIRED_SAMPLE, retired_desc(o).replace( INSTRUCTION["retired"].format(superseded_by="OrderItem"), INSTRUCTION["deprecated"], 1, ), )(o, g, m)), "forward", f"{RETIRED_SAMPLE}: description instruction " f"{INSTRUCTION['deprecated']!r} is not the 'retired' instruction " f"{INSTRUCTION['retired'].format(superseded_by='OrderItem')!r}", "built", ), ( "guard rule sealing a deprecated object", lambda o, g, m: (o, g | {DEPRECATED_SAMPLE}, m), "forward", f"{DEPRECATED_SAMPLE}: is 'deprecated' but carries {GUARD_VR_NAME}; its UI is " "deliberately open, so a write block creates a reachable dead end", "built", ), ( "retired object with no guard rule", lambda o, g, m: (o, g - {RETIRED_SAMPLE}, m), "forward", f"{RETIRED_SAMPLE}: is 'retired' but has no {GUARD_VR_NAME}", "built", ), ( "object absent from the MCP map", lambda o, g, m: (o, g, {k: v for k, v in m.items() if k != DEPRECATED_SAMPLE.lower()}), "forward", f"{DEPRECATED_SAMPLE}: declared 'deprecated' but absent from the MCP lifecycle map", "built", ), ( "MCP disagrees about the state", lambda o, g, m: (o, g, _mcp(m, DEPRECATED_SAMPLE, status="retired")), "forward", f"{DEPRECATED_SAMPLE}: MCP says 'retired', pack declares 'deprecated'", "built", ), ( "MCP disagrees about the successor", lambda o, g, m: (o, g, _mcp(m, RETIRED_SAMPLE, supersededBy="Elsewhere__c")), "forward", f"{RETIRED_SAMPLE}: MCP names 'Elsewhere__c' as successor, pack declares 'OrderItem'", "built", ), ( "MCP disagrees about the date", lambda o, g, m: (o, g, _mcp(m, RETIRED_SAMPLE, retiredOn="2026-07-12")), "forward", f"{RETIRED_SAMPLE}: MCP dates retirement '2026-07-12', pack declares '2026-07-23'", "built", ), ( # Opportunity_Product__c exactly as it stood on origin/main: guard rule present, label # reading like a live object, description opening with RETIRED but not in the generated # form. Deleting only the key does NOT model this — the prose is the second carrier, so # the object stays declared and nothing fires. "guard rule on an object with no declaration at all", lambda o, g, m: (_set( _pop(o, "Opportunity_Product__c", "lifecycleStatus", "supersededBy", "retiredOn"), "Opportunity_Product__c", label="Opportunity Product", pluralLabel="Opportunity Products", description=( "RETIRED legacy line table — emptied by the 2026-07-20 product-chain cleanup " "and non-admin creates/updates are blocked by Legacy_Table_Retired_Guard " "(validation rules do not cover deletes; the table is empty). Line items live " "on the standard OpportunityLineItem. Do not add readers or writers." ), ), g, m), "reverse", f"Opportunity_Product__c: carries {GUARD_VR_NAME} but declares no lifecycleStatus", "built", ), ( "guard rule on an object declared deprecated", lambda o, g, m: (o, g | {DEPRECATED_SAMPLE}, m), "reverse", f"{DEPRECATED_SAMPLE}: carries {GUARD_VR_NAME} but is declared 'deprecated', not 'retired'", "built", ), ( "label marked legacy with nothing declaring it", lambda o, g, m: (_set( _pop(o, "Quote_Product__c", "lifecycleStatus", "supersededBy", "retiredOn"), "Quote_Product__c", description="An ordinary object description.", ), g - {"Quote_Product__c"}, _drop(m, "Quote_Product__c")), "reverse", f"Quote_Product__c: label 'Quote Product {SUFFIX['retired']}' is marked as legacy " "but declares no lifecycleStatus", "built", ), ( "description reading as retired with nothing declaring it", lambda o, g, m: (_set( _pop(o, "Quote_Product__c", "lifecycleStatus", "supersededBy", "retiredOn"), "Quote_Product__c", label="Quote Product", pluralLabel="Quote Products", description="RETIRED legacy line table, superseded a while back.", ), g - {"Quote_Product__c"}, _drop(m, "Quote_Product__c")), "reverse", "Quote_Product__c: description opens as retired but declares no lifecycleStatus", "built", ), ( "MCP entry for an object no spec declares", lambda o, g, m: (o, g, {**m, "ghost__c": { "object": "Ghost__c", "status": "retired", "supersededBy": "X", "retiredOn": "2026-01-01", }}), "reverse", "ghost__c: present in the MCP lifecycle map but declared by no object spec", "built", ), ] return rows def _set(objects, name, **kv): objects[name].update(kv) return objects def _pop(objects, name, *keys): for key in keys: objects[name].pop(key, None) return objects def _mcp(mcp, name, **kv): out = deepcopy(mcp) out[name.lower()].update(kv) return out def _drop(mcp, name): return {k: v for k, v in mcp.items() if k != name.lower()} class GuardCatchesEveryDriftItClaimsToTests(unittest.TestCase): """Every check in this file, pinned by a mutation that only it can catch. Ran the systematic version of this while reviewing: neuter one `problems.append` at a time and re-run. Fifteen of twenty checks survived — including "retired but no guard rule", the headline scenario in #4881. Substring assertions were the reason: overlapping phrasings let unrelated checks satisfy each other's tests. """ def setUp(self): # Two fixtures, because there are two carriers. The built pack holds the derived markers # these mutations corrupt; the source holds the declaration keys. self.objects = objects_in(FINAL_ROOT) self.guarded = guard_rule_objects(FINAL_ROOT) self.source_objects = objects_in(L2_ROOT) self.source_guarded = guard_rule_objects(L2_ROOT) self.mcp = mcp_object_lifecycle() self.assertEqual( check_declarations(self.objects, self.guarded, self.mcp), [], "fixture must start clean, or the mutations below prove nothing", ) self.assertEqual( check_nothing_marked_without_declaration(self.objects, self.guarded, self.mcp), [], "fixture must start clean, or the mutations below prove nothing", ) def test_every_check_is_pinned_by_a_mutation(self): checker = { "forward": check_declarations, "reverse": check_nothing_marked_without_declaration, } for name, mutate, which, expected, fixture in _mutations(): with self.subTest(mutation=name): built = fixture == "built" objects, guarded, mcp = mutate( deepcopy(self.objects if built else self.source_objects), set(self.guarded if built else self.source_guarded), deepcopy(self.mcp), ) problems = ( check_declarations(objects, guarded, mcp, derived=built) if which == "forward" else check_nothing_marked_without_declaration(objects, guarded, mcp) ) self.assertIn( expected, problems, f"mutation {name!r} produced {problems} — the check it pins did not fire", ) if __name__ == "__main__": unittest.main()