mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-08 04:58:12 +00:00
test(scanner): structure heal release evidence lanes (#7442)
Co-authored-by: zhi22915 <qiuzgang@gmail.com>
This commit is contained in:
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+20
-12
@@ -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
|
||||
|
||||
+144
-12
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user