From 944e26d432c372f8baa865fb2fda468725583ea0 Mon Sep 17 00:00:00 2001 From: houseme Date: Tue, 8 Sep 2026 08:43:43 +0800 Subject: [PATCH] test(scanner): structure heal release evidence lanes (#7442) Co-authored-by: zhi22915 --- .config/scanner-heal-required-tests.json | 242 +++++++++++++++++++--- docs/testing/ci-gates.md | 32 +-- scripts/check_test_wiring.py | 156 ++++++++++++-- scripts/run_scanner_heal_evidence_case.sh | 4 +- 4 files changed, 384 insertions(+), 50 deletions(-) diff --git a/.config/scanner-heal-required-tests.json b/.config/scanner-heal-required-tests.json index b7fb4b268..8f0eecd25 100644 --- a/.config/scanner-heal-required-tests.json +++ b/.config/scanner-heal-required-tests.json @@ -1,5 +1,5 @@ { - "schema": 1, + "schema": 2, "cases": { "background-target-restart": { "gate": "G14", @@ -30,27 +30,221 @@ "scope": "Target process killed during partial background rebuild, real unclean-shutdown marker, exact unversioned S3 bodies and replacement-disk shards; not power loss or EC8+4." } }, - "release_pending": { - "G01": "W02/W04 complete root and quota authority coverage", - "G02": "W03 bounded checkpoint progress and independent version inventory", - "G03": "W17/W18 exact scoped ACK with durable publication and mixed peers", - "G04": "W03/W15/W16 crash at every cache/root/floor/intent boundary", - "G05": "W06/W07 per-object outcomes and bounded terminal retention", - "G06": "W06/W08/W23 concurrent status, legacy clients and truncation", - "G07": "W12/W13/W14 durable MRF responsibility at every commit boundary", - "G08": "W12/W13/W14 MRF capacity, disk-full and replica-loss matrix", - "G09": "W13/W18/W23 actual mixed-version reader/writer and rollback payloads", - "G10": "W05/W09/W10/W11 bounded scheduling and pressure recovery", - "G11": "W04/W19/W24 maintenance and complete producer coverage", - "G12": "W02/W15/W16 both quota paths during reset and settlement", - "G13": "W07/W14 quorum-minus-one, unknown disks, remount, Object Lock, dry-run, grace and commit tail", - "G14": "W20/W21 same-window field evidence; 3x4 EC8+4 and multi-set/pool coverage", - "P1": "W20 measured cold-walk share and foreground latency/throughput", - "P2": "W20/W24 measured post-stop convergence and cold segment reuse", - "P3": "W20 measured two-hour pressure/heal capacity and recovery window", - "P4": "W20 measured MRF scale and replay cost with retained responsibility", - "R-E": "W03/W05 fixed-budget real process restart through enumeration and classification", - "R-D": "W07/W14 manager-to-event-to-ledger exact disposition, including grace", - "R-L": "W13/W14 legacy source conflicts, migration gaps and crash-safe source retirement" - } + "release_lanes": { + "single-set-restart": { + "status": "implemented", + "cases": ["background-target-restart", "background-target-crash"], + "covers": ["four-node one-drive topology", "unversioned objects", "target restart/crash"] + }, + "authority-coverage": { + "status": "pending", + "gates": ["G01", "G12"], + "requires": ["root authority coverage", "quota authority coverage"] + }, + "checkpoint-and-crash": { + "status": "pending", + "gates": ["G02", "G04", "R-E"], + "requires": ["bounded checkpoint progress", "boundary crash matrix", "fixed-budget restart evidence"] + }, + "status-and-outcome": { + "status": "pending", + "gates": ["G05", "G06", "R-D"], + "requires": ["per-object outcomes", "legacy status clients", "manager/event/ledger disposition"] + }, + "mrf-responsibility": { + "status": "pending", + "gates": ["G07", "G08", "P4"], + "requires": ["durable MRF responsibility", "disk-full and replica-loss matrix", "MRF replay cost"] + }, + "mixed-version-rollback": { + "status": "pending", + "gates": ["G03", "G09", "R-L"], + "requires": ["mixed-version peers", "rollback payloads", "crash-safe source retirement"] + }, + "scheduler-pressure": { + "status": "pending", + "gates": ["G10", "P1", "P2", "P3"], + "requires": ["bounded scheduling", "foreground latency and throughput", "two-hour pressure evidence"] + }, + "maintenance-producers": { + "status": "pending", + "gates": ["G11", "G13"], + "requires": ["complete producer coverage", "quorum-minus-one and remount matrix"] + }, + "ec8-4-multiset": { + "status": "pending", + "gates": ["G14"], + "requires": ["3x4 EC8+4 topology", "multi-set coverage", "multi-pool coverage"] + } + }, + "release_requirements": [ + { + "gate": "G01", + "task": "W02/W04", + "lane": "authority-coverage", + "status": "pending", + "description": "Complete root and quota authority coverage", + "requires": ["root authority evidence", "quota authority evidence"] + }, + { + "gate": "G02", + "task": "W03", + "lane": "checkpoint-and-crash", + "status": "pending", + "description": "Bounded checkpoint progress and independent version inventory", + "requires": ["bounded checkpoint oracle", "independent version inventory"] + }, + { + "gate": "G03", + "task": "W17/W18", + "lane": "mixed-version-rollback", + "status": "pending", + "description": "Exact scoped ACK with durable publication and mixed peers", + "requires": ["durable scoped ACK publication", "mixed-peer evidence"] + }, + { + "gate": "G04", + "task": "W03/W15/W16", + "lane": "checkpoint-and-crash", + "status": "pending", + "description": "Crash at every cache, root, floor and intent boundary", + "requires": ["cache boundary crash evidence", "root/floor/intent crash evidence"] + }, + { + "gate": "G05", + "task": "W06/W07", + "lane": "status-and-outcome", + "status": "pending", + "description": "Per-object outcomes and bounded terminal retention", + "requires": ["per-object outcome oracle", "terminal retention bounds"] + }, + { + "gate": "G06", + "task": "W06/W08/W23", + "lane": "status-and-outcome", + "status": "pending", + "description": "Concurrent status, legacy clients and truncation", + "requires": ["concurrent status evidence", "legacy client compatibility", "truncation behavior"] + }, + { + "gate": "G07", + "task": "W12/W13/W14", + "lane": "mrf-responsibility", + "status": "pending", + "description": "Durable MRF responsibility at every commit boundary", + "requires": ["MRF responsibility oracle", "commit-boundary crash matrix"] + }, + { + "gate": "G08", + "task": "W12/W13/W14", + "lane": "mrf-responsibility", + "status": "pending", + "description": "MRF capacity, disk-full and replica-loss matrix", + "requires": ["MRF capacity evidence", "disk-full matrix", "replica-loss matrix"] + }, + { + "gate": "G09", + "task": "W13/W18/W23", + "lane": "mixed-version-rollback", + "status": "pending", + "description": "Actual mixed-version reader/writer and rollback payloads", + "requires": ["mixed-version reader evidence", "mixed-version writer evidence", "rollback payload evidence"] + }, + { + "gate": "G10", + "task": "W05/W09/W10/W11", + "lane": "scheduler-pressure", + "status": "pending", + "description": "Bounded scheduling and pressure recovery", + "requires": ["scheduler bound evidence", "pressure recovery evidence"] + }, + { + "gate": "G11", + "task": "W04/W19/W24", + "lane": "maintenance-producers", + "status": "pending", + "description": "Maintenance and complete producer coverage", + "requires": ["maintenance producer matrix", "complete producer inventory"] + }, + { + "gate": "G12", + "task": "W02/W15/W16", + "lane": "authority-coverage", + "status": "pending", + "description": "Both quota paths during reset and settlement", + "requires": ["reset quota-path evidence", "settlement quota-path evidence"] + }, + { + "gate": "G13", + "task": "W07/W14", + "lane": "maintenance-producers", + "status": "pending", + "description": "Quorum-minus-one, unknown disks, remount, Object Lock, dry-run, grace and commit tail", + "requires": ["quorum-minus-one matrix", "unknown-disk/remount matrix", "Object Lock dry-run grace evidence"] + }, + { + "gate": "G14", + "task": "W20/W21", + "lane": "ec8-4-multiset", + "status": "pending", + "description": "Same-window field evidence with 3x4 EC8+4 and multi-set/pool coverage", + "requires": ["same-window field evidence", "3x4 EC8+4 evidence", "multi-set evidence", "multi-pool evidence"] + }, + { + "gate": "P1", + "task": "W20", + "lane": "scheduler-pressure", + "status": "pending", + "description": "Measured cold-walk share and foreground latency/throughput", + "requires": ["cold-walk share measurement", "foreground latency/throughput measurement"] + }, + { + "gate": "P2", + "task": "W20/W24", + "lane": "scheduler-pressure", + "status": "pending", + "description": "Measured post-stop convergence and cold segment reuse", + "requires": ["post-stop convergence measurement", "cold segment reuse measurement"] + }, + { + "gate": "P3", + "task": "W20", + "lane": "scheduler-pressure", + "status": "pending", + "description": "Measured two-hour pressure/heal capacity and recovery window", + "requires": ["two-hour pressure measurement", "heal capacity measurement", "recovery-window measurement"] + }, + { + "gate": "P4", + "task": "W20", + "lane": "mrf-responsibility", + "status": "pending", + "description": "Measured MRF scale and replay cost with retained responsibility", + "requires": ["MRF scale measurement", "MRF replay-cost measurement", "retained responsibility evidence"] + }, + { + "gate": "R-E", + "task": "W03/W05", + "lane": "checkpoint-and-crash", + "status": "pending", + "description": "Fixed-budget real process restart through enumeration and classification", + "requires": ["fixed-budget restart evidence", "enumeration evidence", "classification evidence"] + }, + { + "gate": "R-D", + "task": "W07/W14", + "lane": "status-and-outcome", + "status": "pending", + "description": "Manager-to-event-to-ledger exact disposition, including grace", + "requires": ["manager disposition evidence", "event disposition evidence", "ledger disposition evidence", "grace handling"] + }, + { + "gate": "R-L", + "task": "W13/W14", + "lane": "mixed-version-rollback", + "status": "pending", + "description": "Legacy source conflicts, migration gaps and crash-safe source retirement", + "requires": ["legacy source-conflict evidence", "migration-gap evidence", "crash-safe source retirement evidence"] + } + ] } diff --git a/docs/testing/ci-gates.md b/docs/testing/ci-gates.md index cff8dc072..060b6a007 100644 --- a/docs/testing/ci-gates.md +++ b/docs/testing/ci-gates.md @@ -148,10 +148,14 @@ and exact S3 content. This case is a **four-node, one-drive-per-node process-restart test**. It is not power-loss validation, a 3x4 EC8+4 experiment, an all-version inventory, or proof of scanner enumeration, exact MRF disposition, legacy migration, or rollback. -The registry keeps all G01-G14/P1-P4 and R-E/R-D/R-L release requirements pending -until their actual feature-specific oracles and required topologies exist. -Missing cases cannot be supplied by synthetic W20 results. W20's bounded JSON -and file-hash helpers are reused; its ABBA performance contracts remain in +The schema 2 registry separates the implemented single-set restart lane from +structured release lanes for authority coverage, checkpoint/crash, status and +outcome, MRF responsibility, mixed-version rollback, scheduler pressure, +maintenance producers, and EC8+4 multi-set coverage. All G01-G14/P1-P4 and +R-E/R-D/R-L release requirements stay `pending` until their actual +feature-specific oracles, measurements and required topologies exist. Missing +cases cannot be supplied by synthetic W20 results. W20's bounded JSON and +file-hash helpers are reused; its ABBA performance contracts remain in `docs/operations/scanner-benchmark-runbook.md`. ### Recording One Case @@ -232,14 +236,18 @@ For automation, `--check-scanner-heal-release "$RUN_DIR"` emits one compact JSON decision and exits nonzero while blocked. `verified_cases` contains only cases that pass the complete receipt, build provenance, nextest/JUnit and real oracle checks; `rejected_cases` names registered cases that do not, and -`pending_gates` names the unimplemented release requirements. Approval requires -every registered case to verify, `pending_gates` to be empty, and a future -registry schema capable of representing the complete release matrix. Schema 1 -is deliberately marked `release_schema_capable: false`: it models only the -single-version, unversioned-object restart/crash cases and cannot represent -mixed-version, rollback, EC8+4 or performance evidence. A focused run, -synthetic harness, compile-only result, skipped/retried test, ordinary CI -success, or removal of pending text therefore cannot become a release approval. +`pending_gates` names the unimplemented release requirements and +`pending_lanes` names the structured release lanes that still need real +evidence. Schema 1 is deliberately marked `release_schema_capable: false` +because it models only the single-version, unversioned-object restart/crash +cases. Schema 2 can describe the wider release matrix, but approval still +requires every registered case to verify and every required gate to leave +`pending` only after a future checker can bind it to real feature-specific +evidence. The current checker hard-rejects missing structured requirements and +pending gates mapped to an implemented lane, so clearing pending text cannot +become approval. A focused run, synthetic harness, compile-only result, +skipped/retried test, ordinary CI success, or unregistered mixed-version, +rollback, EC8+4 or performance claim therefore cannot become a release approval. Run parser/receipt regressions with `scripts/python_bin.sh scripts/check_test_wiring.py --self-test`. Those fixtures diff --git a/scripts/check_test_wiring.py b/scripts/check_test_wiring.py index 8fa09c6fa..905931298 100755 --- a/scripts/check_test_wiring.py +++ b/scripts/check_test_wiring.py @@ -27,6 +27,11 @@ from scanner_abba import MAX_JSON_BYTES, digest, number, read_json, require, sha ROOT = Path(__file__).resolve().parents[1] +SCANNER_HEAL_REGISTRY_SCHEMA_MAX = 2 +SCANNER_HEAL_RELEASE_REQUIRED_GATES = ( + "G01", "G02", "G03", "G04", "G05", "G06", "G07", "G08", "G09", "G10", "G11", "G12", "G13", "G14", + "P1", "P2", "P3", "P4", "R-E", "R-D", "R-L", +) SCHEDULED_ALERT_WORKFLOWS = tuple( item["workflow"] for item in json.loads((ROOT / ".github/scheduled-validations.json").read_text()) @@ -886,9 +891,13 @@ def evidence_integer(value: object, name: str, minimum: int, maximum: int) -> in return value +def scanner_heal_registry_schema(registry: dict[str, object]) -> int: + return evidence_integer(registry.get("schema"), "registry schema", 1, SCANNER_HEAL_REGISTRY_SCHEMA_MAX) + + def scanner_heal_oracle_names(root: Path) -> tuple[str, ...]: registry = read_json(root / ".config/scanner-heal-required-tests.json") - evidence_integer(registry.get("schema"), "registry schema", 1, 1) + scanner_heal_registry_schema(registry) cases = registry.get("cases") require(isinstance(cases, dict) and cases, "invalid scanner/heal registry") names = set() @@ -906,6 +915,77 @@ def scanner_heal_oracle_names(root: Path) -> tuple[str, ...]: return tuple(sorted(names)) +def scanner_heal_release_requirements(registry: dict[str, object]) -> tuple[dict[str, dict[str, object]], bool, list[str]]: + schema = scanner_heal_registry_schema(registry) + if schema == 1: + pending = registry.get("release_pending") + require(isinstance(pending, dict), "invalid scanner/heal release requirements") + requirements = {} + for gate, reason in pending.items(): + require(isinstance(gate, str) and re.fullmatch(r"[A-Z][A-Z0-9-]*", gate) is not None, + "invalid scanner/heal release gate") + require(isinstance(reason, str) and reason.strip(), f"missing release requirement for {gate}") + requirements[gate] = {"gate": gate, "status": "pending", "lane": "schema-1-pending", + "description": reason, "requires": [reason]} + return requirements, False, ["schema-1-pending"] if requirements else [] + + lanes = registry.get("release_lanes") + cases = registry.get("cases") + require(isinstance(cases, dict) and cases, "invalid scanner/heal registry") + require(isinstance(lanes, dict) and lanes, "invalid scanner/heal release lanes") + lane_statuses = {} + lane_gates = {} + for lane_id, lane in lanes.items(): + require(isinstance(lane_id, str) and re.fullmatch(r"[a-z0-9-]+", lane_id) is not None, + "invalid scanner/heal release lane") + require(isinstance(lane, dict), f"invalid release lane {lane_id}") + status = lane.get("status") + require(status in ("implemented", "pending"), f"invalid release lane status for {lane_id}") + lane_statuses[lane_id] = status + if status == "implemented": + lane_cases = lane.get("cases") + require(isinstance(lane_cases, list) and lane_cases and all(isinstance(case, str) and case for case in lane_cases), + f"implemented release lane {lane_id} has no cases") + require(all(case in cases for case in lane_cases), f"implemented release lane {lane_id} has unknown cases") + else: + gates = lane.get("gates") + require(isinstance(gates, list) and gates and all(isinstance(gate, str) and gate for gate in gates), + f"pending release lane {lane_id} has no gates") + lane_gates[lane_id] = set(gates) + + raw_requirements = registry.get("release_requirements") + require(isinstance(raw_requirements, list) and raw_requirements, "invalid scanner/heal release requirements") + requirements: dict[str, dict[str, object]] = {} + for item in raw_requirements: + require(isinstance(item, dict), "invalid scanner/heal release requirement") + gate = item.get("gate") + require(isinstance(gate, str) and re.fullmatch(r"[A-Z][A-Z0-9-]*", gate) is not None, + "invalid scanner/heal release gate") + require(gate not in requirements, f"duplicate scanner/heal release gate {gate}") + status = item.get("status") + require(status == "pending", f"release gate {gate} must stay pending until real evidence is registered") + lane = item.get("lane") + require(isinstance(lane, str) and lane in lane_statuses, f"unknown release lane for {gate}") + require(lane_statuses[lane] == "pending", f"pending release gate {gate} mapped to non-pending lane {lane}") + description = item.get("description") + require(isinstance(description, str) and description.strip(), f"missing release requirement for {gate}") + requires = item.get("requires") + require(isinstance(requires, list) and requires and + all(isinstance(requirement, str) and requirement.strip() for requirement in requires), + f"missing concrete evidence requirements for {gate}") + requirements[gate] = item + + missing = sorted(set(SCANNER_HEAL_RELEASE_REQUIRED_GATES) - set(requirements)) + require(not missing, f"missing scanner/heal release requirements: {', '.join(missing)}") + for lane, gates in lane_gates.items(): + unknown = sorted(gates - set(requirements)) + require(not unknown, f"pending release lane {lane} has unknown gates: {', '.join(unknown)}") + mapped = {gate for gate, requirement in requirements.items() if requirement["lane"] == lane} + require(gates == mapped, f"pending release lane {lane} gates do not match release requirements") + pending_lanes = sorted(lane for lane, status in lane_statuses.items() if status == "pending") + return requirements, True, pending_lanes + + def begin_scanner_heal_receipt(root: Path, directory: Path, binary: Path, test_binary: Path) -> None: """Record an existing build; this command never builds or runs a test.""" require(not directory.exists(), "scanner/heal run directory must be new") @@ -969,7 +1049,7 @@ def check_scanner_heal_evidence(root: Path, directory: Path, case_id: str) -> li """Validate one actual case, or fail the release while required lanes are pending.""" try: registry = read_json(root / ".config/scanner-heal-required-tests.json") - evidence_integer(registry.get("schema"), "registry schema", 1, 1) + scanner_heal_registry_schema(registry) require(registry.get("cases"), "invalid scanner/heal registry") selected = registry["cases"] if case_id == "release" else {case_id: registry["cases"][case_id]} run = read_json(directory / "run.json") @@ -1088,7 +1168,10 @@ def check_scanner_heal_evidence(root: Path, directory: Path, case_id: str) -> li require(all(keys == sorted(obj["key"] for obj in objects) for keys in node_listings), "S3 listing differs from object oracle") if case_id == "release": - errors.extend(f"pending {gate}: {reason}" for gate, reason in registry["release_pending"].items()) + release_requirements, _, _ = scanner_heal_release_requirements(registry) + errors.extend( + f"pending {gate}: {requirement['description']}" for gate, requirement in release_requirements.items() + ) return errors except (OSError, KeyError, TypeError, ValueError, ET.ParseError) as error: return [f"scanner/heal evidence rejected: {error}"] @@ -1097,15 +1180,10 @@ def check_scanner_heal_evidence(root: Path, directory: Path, case_id: str) -> li def scanner_heal_release_status(root: Path, directory: Path) -> dict[str, object]: """Return a compact release decision without weakening case validation.""" registry = read_json(root / ".config/scanner-heal-required-tests.json") - evidence_integer(registry.get("schema"), "registry schema", 1, 1) + scanner_heal_registry_schema(registry) cases = registry.get("cases") require(isinstance(cases, dict) and cases, "invalid scanner/heal registry") - pending = registry.get("release_pending") - require(isinstance(pending, dict), "invalid scanner/heal release requirements") - for gate, reason in pending.items(): - require(isinstance(gate, str) and re.fullmatch(r"[A-Z][A-Z0-9-]*", gate) is not None, - "invalid scanner/heal release gate") - require(isinstance(reason, str) and reason.strip(), f"missing release requirement for {gate}") + release_requirements, release_schema_capable, pending_lanes = scanner_heal_release_requirements(registry) verified_cases = [] rejected_cases = [] @@ -1119,10 +1197,11 @@ def scanner_heal_release_status(root: Path, directory: Path) -> dict[str, object "schema": 1, "decision": "blocked", "release_approved": False, - "release_schema_capable": False, + "release_schema_capable": release_schema_capable, "verified_cases": verified_cases, "rejected_cases": rejected_cases, - "pending_gates": sorted(pending), + "pending_gates": sorted(release_requirements), + "pending_lanes": pending_lanes, } @@ -1346,14 +1425,21 @@ class SelfTests(unittest.TestCase): status = scanner_heal_release_status(root, run_dir) self.assertEqual(status["decision"], "blocked") self.assertFalse(status["release_approved"]) + self.assertTrue(status["release_schema_capable"]) self.assertEqual(status["rejected_cases"], []) self.assertEqual(len(status["pending_gates"]), 21) + self.assertIn("mixed-version-rollback", status["pending_lanes"]) + self.assertIn("ec8-4-multiset", status["pending_lanes"]) + self.assertIn("scheduler-pressure", status["pending_lanes"]) def test_scanner_heal_case_only_schema_cannot_approve_release(self) -> None: with tempfile.TemporaryDirectory() as tmp: root, run_dir = self.scanner_heal_fixture(Path(tmp)) registry = read_json(root / ".config/scanner-heal-required-tests.json") + registry["schema"] = 1 registry["release_pending"] = {} + registry.pop("release_lanes") + registry.pop("release_requirements") write_json(root / ".config/scanner-heal-required-tests.json", registry) status = scanner_heal_release_status(root, run_dir) @@ -1363,11 +1449,53 @@ class SelfTests(unittest.TestCase): self.assertEqual(status["rejected_cases"], []) self.assertEqual(status["pending_gates"], []) + def test_scanner_heal_release_requirements_cannot_be_cleared(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root, run_dir = self.scanner_heal_fixture(Path(tmp)) + registry = read_json(root / ".config/scanner-heal-required-tests.json") + registry["release_requirements"] = [] + write_json(root / ".config/scanner-heal-required-tests.json", registry) + + with self.assertRaisesRegex(ValueError, "invalid scanner/heal release requirements"): + scanner_heal_release_status(root, run_dir) + + def test_scanner_heal_release_matrix_lanes_remain_pending_without_real_evidence(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root, run_dir = self.scanner_heal_fixture(Path(tmp)) + + errors = check_scanner_heal_evidence(root, run_dir, "release") + for gate, text in ( + ("G09", "mixed-version reader/writer"), + ("G14", "3x4 EC8+4"), + ("P3", "two-hour pressure/heal capacity"), + ("R-L", "Legacy source conflicts"), + ): + self.assertTrue(any(error.startswith(f"pending {gate}:") and text in error for error in errors), gate) + + status = scanner_heal_release_status(root, run_dir) + self.assertFalse(status["release_approved"]) + self.assertIn("mixed-version-rollback", status["pending_lanes"]) + self.assertIn("ec8-4-multiset", status["pending_lanes"]) + self.assertIn("scheduler-pressure", status["pending_lanes"]) + + def test_scanner_heal_pending_gate_cannot_map_to_implemented_lane(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + root, run_dir = self.scanner_heal_fixture(Path(tmp)) + registry = read_json(root / ".config/scanner-heal-required-tests.json") + registry["release_requirements"][0]["lane"] = "single-set-restart" + write_json(root / ".config/scanner-heal-required-tests.json", registry) + + with self.assertRaisesRegex(ValueError, "mapped to non-pending lane"): + scanner_heal_release_status(root, run_dir) + def test_scanner_heal_release_status_rejects_synthetic_case(self) -> None: with tempfile.TemporaryDirectory() as tmp: root, run_dir = self.scanner_heal_fixture(Path(tmp)) registry = read_json(root / ".config/scanner-heal-required-tests.json") + registry["schema"] = 1 registry["release_pending"] = {} + registry.pop("release_lanes") + registry.pop("release_requirements") write_json(root / ".config/scanner-heal-required-tests.json", registry) path = run_dir / "background-target-crash.json" oracle = read_json(path) @@ -1379,6 +1507,7 @@ class SelfTests(unittest.TestCase): status = scanner_heal_release_status(root, run_dir) self.assertEqual(status["decision"], "blocked") self.assertFalse(status["release_approved"]) + self.assertFalse(status["release_schema_capable"]) self.assertEqual(status["rejected_cases"], ["background-target-crash"]) self.assertEqual(status["pending_gates"], []) @@ -1386,7 +1515,10 @@ class SelfTests(unittest.TestCase): with tempfile.TemporaryDirectory() as tmp: root, run_dir = self.scanner_heal_fixture(Path(tmp)) registry = read_json(root / ".config/scanner-heal-required-tests.json") + registry["schema"] = 1 registry["release_pending"] = {} + registry.pop("release_lanes") + registry.pop("release_requirements") write_json(root / ".config/scanner-heal-required-tests.json", registry) (run_dir / "background-target-crash.json").unlink() (run_dir / "execution.json").unlink() diff --git a/scripts/run_scanner_heal_evidence_case.sh b/scripts/run_scanner_heal_evidence_case.sh index ff7c30086..8cf918c77 100755 --- a/scripts/run_scanner_heal_evidence_case.sh +++ b/scripts/run_scanner_heal_evidence_case.sh @@ -103,8 +103,8 @@ import sys status = json.loads(pathlib.Path(sys.argv[1]).read_text()) if status.get("decision") != "blocked" or status.get("release_approved") is not False: raise SystemExit("release status did not record a blocked decision") -if status.get("release_schema_capable") is not False: - raise SystemExit("case-only evidence schema unexpectedly became release-capable") +if not status.get("pending_gates"): + raise SystemExit("release status did not retain pending gates") PY }