from __future__ import annotations import importlib.util from pathlib import Path from types import SimpleNamespace def _load_module(): script_path = Path(__file__).resolve().parents[1] / "scripts" / "validate_governance_assets.py" spec = importlib.util.spec_from_file_location("validate_governance_assets", script_path) assert spec is not None assert spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def test_is_policy_file_detects_ouroboros_policy_docs() -> None: module = _load_module() assert module.is_policy_file("docs/ouroboros/85_loss_recovery_action_plan.md") assert not module.is_policy_file("docs/ouroboros/01_requirements_registry.md") assert not module.is_policy_file("docs/workflow.md") assert not module.is_policy_file("docs/ouroboros/notes.txt") def test_validate_registry_sync_requires_registry_update_when_policy_changes() -> None: module = _load_module() errors: list[str] = [] module.validate_registry_sync( ["docs/ouroboros/85_loss_recovery_action_plan.md"], errors, ) assert errors assert "policy file changed without updating" in errors[0] def test_validate_registry_sync_passes_when_registry_included() -> None: module = _load_module() errors: list[str] = [] module.validate_registry_sync( [ "docs/ouroboros/85_loss_recovery_action_plan.md", "docs/ouroboros/01_requirements_registry.md", ], errors, ) assert errors == [] def test_load_changed_files_supports_explicit_paths() -> None: module = _load_module() errors: list[str] = [] changed = module.load_changed_files( ["./docs/ouroboros/85_loss_recovery_action_plan.md", " src/main.py "], errors, ) assert errors == [] assert changed == [ "docs/ouroboros/85_loss_recovery_action_plan.md", "src/main.py", ] def test_load_changed_files_with_range_uses_git_diff(monkeypatch) -> None: module = _load_module() errors: list[str] = [] def fake_run(cmd, check, capture_output, text): # noqa: ANN001 assert cmd[:3] == ["git", "diff", "--name-only"] assert check is True assert capture_output is True assert text is True return SimpleNamespace( stdout="docs/ouroboros/85_loss_recovery_action_plan.md\nsrc/main.py\n" ) monkeypatch.setattr(module.subprocess, "run", fake_run) changed = module.load_changed_files(["abc...def"], errors) assert errors == [] assert changed == [ "docs/ouroboros/85_loss_recovery_action_plan.md", "src/main.py", ] def test_validate_task_req_mapping_reports_missing_req_reference(tmp_path) -> None: module = _load_module() doc = tmp_path / "work_orders.md" doc.write_text( "- `TASK-OPS-999` no req mapping line\n", encoding="utf-8", ) errors: list[str] = [] module.validate_task_req_mapping(errors, task_doc=doc) assert errors assert "TASK without REQ mapping" in errors[0] def test_validate_task_req_mapping_passes_when_req_present(tmp_path) -> None: module = _load_module() doc = tmp_path / "work_orders.md" doc.write_text( "- `TASK-OPS-999` (`REQ-OPS-001`): enforce timezone labels\n", encoding="utf-8", ) errors: list[str] = [] module.validate_task_req_mapping(errors, task_doc=doc) assert errors == [] def test_validate_pr_traceability_fails_when_req_missing(monkeypatch) -> None: module = _load_module() monkeypatch.setenv("GOVERNANCE_PR_TITLE", "feat: update policy checker") monkeypatch.setenv("GOVERNANCE_PR_BODY", "Refs: TASK-OPS-001 TEST-ACC-007") errors: list[str] = [] module.validate_pr_traceability(errors) assert errors assert "PR text missing REQ-ID reference" in errors def test_validate_read_only_approval_requires_evidence(monkeypatch) -> None: module = _load_module() changed_files = ["src/core/risk_manager.py"] errors: list[str] = [] warnings: list[str] = [] monkeypatch.setenv( "GOVERNANCE_PR_BODY", "\n".join( [ "## READ-ONLY Approval (Required when touching READ-ONLY files)", "- Touched READ-ONLY files: src/core/risk_manager.py", "- Human approval: TBD", "- Test suite 1: pytest -q", "- Test suite 2: TBD", ] ), ) module.validate_read_only_approval(changed_files, errors, warnings) assert warnings == [] assert any("Human approval" in err for err in errors) assert any("Test suite 2" in err for err in errors) def test_validate_read_only_approval_passes_with_complete_evidence(monkeypatch) -> None: module = _load_module() changed_files = ["src/core/risk_manager.py"] errors: list[str] = [] warnings: list[str] = [] monkeypatch.setenv( "GOVERNANCE_PR_BODY", "\n".join( [ "## READ-ONLY Approval (Required when touching READ-ONLY files)", "- Touched READ-ONLY files: src/core/risk_manager.py", "- Human approval: https://example.com/review/123", "- Test suite 1: pytest -q tests/test_risk.py", "- Test suite 2: pytest -q tests/test_main.py -k risk", ] ), ) module.validate_read_only_approval(changed_files, errors, warnings) assert errors == [] assert warnings == [] def test_validate_read_only_approval_fails_without_pr_body(monkeypatch) -> None: module = _load_module() changed_files = ["src/core/risk_manager.py"] errors: list[str] = [] warnings: list[str] = [] monkeypatch.delenv("GOVERNANCE_PR_BODY", raising=False) module.validate_read_only_approval(changed_files, errors, warnings) assert warnings == [] assert errors assert "approval evidence is required" in errors[0] def test_validate_read_only_approval_skips_when_no_readonly_file_changed() -> None: module = _load_module() changed_files = ["src/main.py"] errors: list[str] = [] warnings: list[str] = [] module.validate_read_only_approval(changed_files, errors, warnings) assert errors == [] assert warnings == [] def test_must_contain_enforces_workflow_newline_helper_tokens(tmp_path) -> None: module = _load_module() workflow_doc = tmp_path / "workflow.md" workflow_doc.write_text( "\n".join( [ "Session Handover Gate (Mandatory)", "python3 scripts/session_handover_check.py --strict", "scripts/tea_comment.sh", ] ), encoding="utf-8", ) errors: list[str] = [] module.must_contain( workflow_doc, [ "Session Handover Gate (Mandatory)", "session_handover_check.py --strict", "scripts/tea_comment.sh", ], errors, ) assert errors == [] def test_must_contain_fails_when_workflow_missing_newline_helper_token(tmp_path) -> None: module = _load_module() workflow_doc = tmp_path / "workflow.md" workflow_doc.write_text( "\n".join( [ "Session Handover Gate (Mandatory)", "python3 scripts/session_handover_check.py --strict", ] ), encoding="utf-8", ) errors: list[str] = [] module.must_contain( workflow_doc, ["scripts/tea_comment.sh"], errors, ) assert any("scripts/tea_comment.sh" in err for err in errors) def test_must_contain_enforces_commands_newline_section_tokens(tmp_path) -> None: module = _load_module() commands_doc = tmp_path / "commands.md" commands_doc.write_text( "\n".join( [ "Session Handover Preflight (Mandatory)", "python3 scripts/session_handover_check.py --strict", "Comment Newline Escaping", "scripts/tea_comment.sh", ] ), encoding="utf-8", ) errors: list[str] = [] module.must_contain( commands_doc, [ "Session Handover Preflight (Mandatory)", "session_handover_check.py --strict", "Comment Newline Escaping", "scripts/tea_comment.sh", ], errors, ) assert errors == [] def test_must_contain_fails_when_commands_missing_newline_section_token(tmp_path) -> None: module = _load_module() commands_doc = tmp_path / "commands.md" commands_doc.write_text( "\n".join( [ "Session Handover Preflight (Mandatory)", "python3 scripts/session_handover_check.py --strict", "scripts/tea_comment.sh", ] ), encoding="utf-8", ) errors: list[str] = [] module.must_contain( commands_doc, ["Comment Newline Escaping"], errors, ) assert any("Comment Newline Escaping" in err for err in errors) def test_validate_task_test_pairing_reports_missing_test_reference(tmp_path) -> None: module = _load_module() doc = tmp_path / "work_orders.md" doc.write_text( "- `TASK-OPS-999` (`REQ-OPS-001`): enforce timezone labels only\n", encoding="utf-8", ) errors: list[str] = [] module.validate_task_test_pairing(errors, task_doc=doc) assert errors assert "TASK without TEST mapping" in errors[0] def test_validate_task_test_pairing_passes_when_test_present(tmp_path) -> None: module = _load_module() doc = tmp_path / "work_orders.md" doc.write_text( "- `TASK-OPS-999` (`REQ-OPS-001`,`TEST-ACC-007`): enforce timezone labels\n", encoding="utf-8", ) errors: list[str] = [] module.validate_task_test_pairing(errors, task_doc=doc) assert errors == [] def test_validate_timezone_policy_tokens_requires_kst_or_utc(tmp_path, monkeypatch) -> None: module = _load_module() docs = tmp_path / "docs" ouroboros = docs / "ouroboros" docs.mkdir(parents=True) ouroboros.mkdir(parents=True) monkeypatch.chdir(tmp_path) (ouroboros / "01_requirements_registry.md").write_text("REQ-OPS-001\nUTC\n", encoding="utf-8") (ouroboros / "30_code_level_work_orders.md").write_text( "TASK-OPS-001 (`REQ-OPS-001`,`TEST-ACC-007`)\nKST\n", encoding="utf-8", ) (docs / "workflow.md").write_text("timezone policy: KST and UTC\n", encoding="utf-8") errors: list[str] = [] module.validate_timezone_policy_tokens(errors) assert errors == [] (docs / "workflow.md").write_text("timezone policy missing labels\n", encoding="utf-8") errors = [] module.validate_timezone_policy_tokens(errors) assert errors assert any("missing timezone policy token" in err for err in errors)