"""The RC ledger gate must fail closed on ambiguous plans or incomplete evidence.""" import importlib.util import json from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] SPEC = importlib.util.spec_from_file_location("rc_readiness", ROOT / "scripts/rc_readiness.py") assert SPEC is not None and SPEC.loader is not None readiness = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(readiness) @pytest.fixture def repository(tmp_path: Path) -> Path: for relative in (readiness.CONFIG, readiness.LEDGER, readiness.ROADMAP): target = tmp_path / relative target.parent.mkdir(parents=True, exist_ok=True) target.write_text((ROOT / relative).read_text(encoding="utf-8"), encoding="utf-8") return tmp_path def mutate_plan(repository: Path, change) -> None: path = repository / readiness.CONFIG plan = json.loads(path.read_text(encoding="utf-8")) change(plan) path.write_text(json.dumps(plan), encoding="utf-8") def test_actual_repository_is_valid_but_incomplete() -> None: result = readiness.validate(ROOT) plan = json.loads((ROOT / readiness.CONFIG).read_text(encoding="utf-8")) assert result["packet_count"] == len(plan["packets"]) assert result["required_gap_count"] > result["verified_count"] > 0 assert result["outstanding_ids"] assert result["all_required_rows_verified"] is False assert result["topological_order"].index("P06") < result["topological_order"].index("P03") assert "release_ready" not in result def test_repository_path_guard_rejects_escape_before_read(repository: Path) -> None: with pytest.raises(readiness.InvalidPlan, match="escapes root"): readiness._read(repository, "../outside.md") def test_status_documentation_is_not_parsed_as_issue_rows(repository: Path) -> None: before = readiness.validate(repository) path = repository / readiness.LEDGER with path.open("a", encoding="utf-8") as output: output.write("\n| Classification | UNKNOWN |\nProse RC-FAKE-01 is not an issue row.\n") assert readiness.validate(repository) == before @pytest.mark.parametrize("change", [ lambda p: p.update(schema_version=True), lambda p: p.update(schema_version=2), lambda p: p.update(extra=True), lambda p: p.pop("ledger"), lambda p: p.update(target_version="stable"), lambda p: p.update(baseline_commit="main"), lambda p: p.update(ledger="../../outside.md"), lambda p: p.update(roadmap="C:/outside.md"), lambda p: p.update(packets=[]), lambda p: p["packets"].append(p["packets"][0]), lambda p: p["packets"][0].update(id="bad"), lambda p: p["packets"][0]["depends_on"].append("P99"), lambda p: p["packets"][0]["depends_on"].append("P15"), lambda p: p["packets"][0]["depends_on"].extend(["P01", "P01"]), lambda p: p["packets"][0]["gaps"].pop(), lambda p: p["packets"][0]["gaps"].append("RC-UNKNOWN-01"), lambda p: p["packets"][0]["gaps"].append(p["packets"][1]["gaps"][0]), lambda p: p["deferred_gaps"].append(p["deferred_gaps"][0]), lambda p: p["deferred_gaps"].append(p["packets"][0]["gaps"].pop()), lambda p: p["packets"][0]["gaps"].append(p["deferred_gaps"].pop()), ]) def test_invalid_plans_are_rejected(repository: Path, change) -> None: mutate_plan(repository, change) with pytest.raises(readiness.InvalidPlan): readiness.validate(repository) @pytest.mark.parametrize("text", ["{", '{"schema_version":1,"schema_version":1}', '{"packets":[{"id":"P00","id":"P01"}]}']) def test_malformed_and_duplicate_json_fields(repository: Path, text: str) -> None: (repository / readiness.CONFIG).write_text(text, encoding="utf-8") with pytest.raises(ValueError): readiness.validate(repository) @pytest.mark.parametrize("relative", [readiness.CONFIG, readiness.LEDGER, readiness.ROADMAP]) def test_missing_inputs_fail(repository: Path, relative: str) -> None: (repository / relative).unlink() with pytest.raises(OSError): readiness.validate(repository) @pytest.mark.parametrize("mode", ["duplicate", "empty", "unknown_status", "false_verified", "deferral", "classification", "malformed"]) def test_invalid_ledger_rows_fail(repository: Path, mode: str) -> None: path = repository / readiness.LEDGER text = path.read_text(encoding="utf-8") row = next(line for line in text.splitlines() if line.startswith("| RC-")) changed = { "duplicate": text + "\n" + row, "empty": "# No rows", "unknown_status": text.replace("| VERIFIED |", "| APPROVED |", 1), "false_verified": text.replace("| VERIFIED |", "| VERIFIED — pending |", 1), "deferral": text.replace("| VERIFIED |", "| DEFERRED |", 1), "classification": text.replace(row, row.replace(row.split("|")[4], " UNKNOWN ", 1)), "malformed": text.replace(row, row.rstrip("|"), 1), }[mode] path.write_text(changed, encoding="utf-8") with pytest.raises(readiness.InvalidPlan): readiness.validate(repository) def test_cli_exit_codes_and_full_verification(repository: Path, monkeypatch, capsys) -> None: original = readiness.validate monkeypatch.setattr(readiness, "validate", lambda: original(repository)) assert readiness.main([]) == 0 assert readiness.main(["--require-verified"]) == 1 path = repository / readiness.LEDGER lines = path.read_text(encoding="utf-8").splitlines() path.write_text("\n".join( line.rsplit("|", 2)[0] + "| VERIFIED |" if line.startswith("| RC-") and "| POST-RC |" not in line else line for line in lines ), encoding="utf-8") assert readiness.main(["--require-verified"]) == 0 result = json.loads(capsys.readouterr().out.splitlines()[-1]) assert result["all_required_rows_verified"] is True assert result["outstanding_ids"] == [] assert result["verified_count"] == result["required_gap_count"] path.unlink() assert readiness.main([]) == 2 assert json.loads(capsys.readouterr().out)["all_required_rows_verified"] is False