139 lines
6.0 KiB
Python
139 lines
6.0 KiB
Python
"""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
|