diff --git a/tests/test_test_risk_map.py b/tests/test_test_risk_map.py index e508246..34f8d7b 100644 --- a/tests/test_test_risk_map.py +++ b/tests/test_test_risk_map.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import sys from copy import deepcopy from pathlib import Path @@ -28,7 +29,9 @@ def test_committed_test_risk_map_is_complete(): def test_risk_map_rejects_duplicate_missing_and_unowned_modules(tmp_path): risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) risk["owner"] = "" - duplicate = deepcopy(risk["modules"][0]) + duplicate = deepcopy( + next(module for module in risk["modules"] if module["path"] == "obliteratus/cli.py") + ) duplicate["required_tests"] = ["tests/missing.py"] duplicate["risk_class"] = "unknown" risk["modules"].append(duplicate) @@ -94,3 +97,210 @@ def test_risk_map_cannot_drop_critical_cpu_path(tmp_path): ) assert "critical CPU path is missing from test risk map: obliteratus/cli.py" in errors + + +def test_contract_inventory_rejects_unmapped_and_multiply_owned_source(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + risk["source_inventory"]["roots"].append("scripts/check_test_risk_map.py") + second_surface = risk["contract_surfaces"][1] + second_surface["paths"].append("app.py") + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "unmapped production source path: scripts/check_test_risk_map.py" in errors + assert ( + "production source path has multiple contract owners: " + "app.py (package-entrypoints, core-pipeline)" + ) in errors + + +def test_contract_surface_requires_valid_identity_contracts_and_tests(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + surface = risk["contract_surfaces"][0] + surface["id"] = "Invalid surface" + surface["owner"] = "" + surface["description"] = "" + surface["contract_types"] = ["package-entrypoint", "invented-contract", {"bad": "type"}] + surface["required_tests"] = ["tests/missing.py"] + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "contract surface 0 has invalid id: 'Invalid surface'" in errors + assert "contract surface index-0 requires a non-empty owner" in errors + assert "contract surface index-0 requires a non-empty description" in errors + assert "contract surface index-0 has invalid contract type: 'invented-contract'" in errors + assert "contract surface index-0 has invalid contract type: {'bad': 'type'}" in errors + assert "contract surface index-0 maps missing test path: tests/missing.py" in errors + + +def test_risk_module_must_resolve_to_contract_owner(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + surface = next( + item for item in risk["contract_surfaces"] if item["id"] == "public-interface-and-services" + ) + surface["paths"].remove("obliteratus/cli.py") + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "unmapped production source path: obliteratus/cli.py" in errors + assert "risk module is missing a contract owner: obliteratus/cli.py" in errors + + +def test_risk_map_rejects_legacy_schema_and_invalid_inventory_roots(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + risk["schema_version"] = 1 + risk["source_inventory"]["roots"] = ["missing-source-root", "/absolute/source.py"] + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "test risk map schema_version must be 2" in errors + assert "source_inventory root does not exist: missing-source-root" in errors + assert "source_inventory has invalid root: '/absolute/source.py'" in errors + + +def test_loader_rejects_unreadable_and_nonobject_documents(tmp_path): + errors = [] + assert check_test_risk_map._load_object(tmp_path / "missing.json", "fixture", errors) == {} + assert errors and errors[0].startswith("cannot read fixture:") + + errors = [] + path = tmp_path / "list.json" + path.write_text("[]", encoding="utf-8") + assert check_test_risk_map._load_object(path, "fixture", errors) == {} + assert errors == ["fixture root must be an object"] + + +def test_source_inventory_rejects_invalid_shape_duplicates_and_non_python_file(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + risk["source_inventory"] = {"roots": ["app.py", "app.py", "README.md"]} + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "test risk map source_inventory has duplicate roots" in errors + assert "source_inventory root is not Python source: README.md" in errors + assert any(error.startswith("contract surface ") and "maps source outside inventory" in error for error in errors) + + risk["source_inventory"] = [] + errors = check_test_risk_map.validate( + _write(tmp_path / "risk-invalid.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + assert "test risk map source_inventory must be an object" in errors + + +def test_contract_surface_rejects_duplicate_and_malformed_fields(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + first = risk["contract_surfaces"][0] + second = risk["contract_surfaces"][1] + second["id"] = first["id"] + first["contract_types"] = ["public-interface", "public-interface"] + first["required_tests"] = ["tests/test_cli.py", "tests/test_cli.py", 42] + first["paths"] = ["app.py", "app.py", 42, "missing.py"] + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "duplicate contract surface id: package-entrypoints" in errors + assert "contract surface package-entrypoints has duplicate contract types" in errors + assert "contract surface package-entrypoints has duplicate test paths" in errors + assert "contract surface package-entrypoints has invalid test path: 42" in errors + assert "contract surface package-entrypoints has duplicate source paths" in errors + assert "contract surface package-entrypoints has invalid source path: 42" in errors + assert "contract surface package-entrypoints maps source outside inventory: missing.py" in errors + + +def test_contract_surface_and_module_collections_must_be_nonempty(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + risk["contract_surfaces"] = [] + risk["modules"] = [] + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "test risk map contract_surfaces must be a non-empty list" in errors + assert "test risk map modules must be a non-empty list" in errors + + +def test_risk_modules_reject_malformed_shape_risk_tests_and_gates(tmp_path): + risk = json.loads((ROOT / "ci" / "test-risk-map.json").read_text()) + original_count = len(risk["modules"]) + malformed = deepcopy(risk["modules"][0]) + malformed["path"] = "obliteratus/cli.py" + malformed["risk"] = "" + malformed["required_tests"] = [] + malformed["conditional_gates"] = ["unknown-gate", "unknown-gate", {"bad": "gate"}] + risk["modules"].extend([None, {}, malformed]) + + errors = check_test_risk_map.validate( + _write(tmp_path / "risk.json", risk), + ROOT / "ci" / "test-quality-policy.json", + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert f"risk module {original_count} must be an object" in errors + assert f"risk module {original_count + 1} requires a non-empty path" in errors + assert "duplicate risk module path: obliteratus/cli.py" in errors + assert "risk module obliteratus/cli.py requires a non-empty risk" in errors + assert "risk module obliteratus/cli.py requires at least one test" in errors + assert "risk module obliteratus/cli.py has duplicate conditional gates" in errors + assert "risk module obliteratus/cli.py references unknown gate unknown-gate" in errors + assert "risk module obliteratus/cli.py has invalid conditional gate: {'bad': 'gate'}" in errors + + +def test_quality_policy_collection_shapes_are_validated(tmp_path): + risk = ROOT / "ci" / "test-risk-map.json" + quality = json.loads((ROOT / "ci" / "test-quality-policy.json").read_text()) + quality["mature_cpu_scope"]["exclusions"] = [None] + quality["critical_cpu_paths"] = "not-a-list" + + errors = check_test_risk_map.validate( + risk, + _write(tmp_path / "quality.json", quality), + ROOT / "ci" / "conditional-test-policy.json", + ) + + assert "quality policy exclusion must be an object" in errors + assert "quality policy critical_cpu_paths must be a list" in errors + + +def test_main_reports_success_and_failure(tmp_path, monkeypatch, capsys): + monkeypatch.setattr(sys, "argv", ["check_test_risk_map.py"]) + assert check_test_risk_map.main() == 0 + assert capsys.readouterr().out == "test risk map: valid\n" + + bad_risk = tmp_path / "bad-risk.json" + bad_risk.write_text("{}", encoding="utf-8") + monkeypatch.setattr( + sys, + "argv", + ["check_test_risk_map.py", "--risk-map", str(bad_risk)], + ) + assert check_test_risk_map.main() == 1 + assert "ERROR: test risk map schema_version must be 2" in capsys.readouterr().out