test_governance_freshness.py python
200 lines 8.2 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago
1 """Unit tests for governance freshness resolution (§GFG.9 unit)."""
2
3 from __future__ import annotations
4
5 from pathlib import Path
6 from unittest.mock import MagicMock
7
8 from adapters.errors import ReadError
9 from adapters.types import AnchorResult, HeadResult, StatusResult
10 from tests.support import FIXTURES, load_fixture_config, write_config
11 from tools.governance_freshness import check_governance_freshness, parse_sync_marker
12 from tools.governance_freshness.check import SyncMarker
13
14
15 def _write_lock(repo: Path) -> None:
16 (repo / ".overseer").mkdir(parents=True, exist_ok=True)
17 (repo / ".overseer" / "version.lock").write_text(
18 "lock_version: 1\nkit_version: 0.1.0\nconfig_version: 1\n"
19 "footprint_digest: sha256:" + ("0" * 64) + "\n"
20 "installed_at: \"2026-01-01T00:00:00Z\"\nsynced_at: \"2026-01-01T00:00:00Z\"\n"
21 "footprint: []\n",
22 encoding="utf-8",
23 )
24
25
26 def _seed_docs(repo: Path, *, claim: str = "cafebabe") -> None:
27 docs = repo / "docs"
28 docs.mkdir(parents=True, exist_ok=True)
29 (docs / "OVERSEER-HANDOVER.md").write_text(
30 f"| GitHub `main` | `{claim}` |\n",
31 encoding="utf-8",
32 )
33 (docs / "ROADMAP.md").write_text("## Build queue\n\n", encoding="utf-8")
34
35
36 def _adapter(*, tip: str = "cafebabe", fail: str | None = None) -> MagicMock:
37 adapter = MagicMock()
38 if fail == "status":
39 adapter.status.return_value = ReadError("git status", "boom")
40 return adapter
41 adapter.status.return_value = StatusResult(
42 regime="git-only", dirty=False, branch="main", muse_dirty=None, git_dirty=False
43 )
44 if fail == "r1":
45 adapter.read_head.return_value = ReadError("git rev-parse origin/main", "missing")
46 return adapter
47 adapter.read_head.return_value = HeadResult(sha=tip, kind="git")
48 adapter.read_canonical_anchor.return_value = AnchorResult(
49 anchor_sha=tip, source="origin/main"
50 )
51 return adapter
52
53
54 def test_parse_enriched_and_legacy_marker() -> None:
55 enriched = parse_sync_marker("2026-07-28T00:00:00Z\nr1=abc\nr3=def\n")
56 assert isinstance(enriched, SyncMarker)
57 assert enriched.timestamp == "2026-07-28T00:00:00Z"
58 assert enriched.r1 == "abc"
59 assert enriched.r3 == "def"
60 assert enriched.legacy is False
61
62 legacy = parse_sync_marker("2026-07-28T00:00:00Z\n")
63 assert legacy is not None
64 assert legacy.legacy is True
65 assert legacy.r1 is None
66
67
68 def test_not_applicable_without_lock(tmp_path: Path) -> None:
69 write_config(tmp_path, "config-git-only.yaml")
70 config = load_fixture_config(tmp_path, "config-git-only.yaml")
71 report = check_governance_freshness(config, tmp_path, adapter=_adapter())
72 assert report.state == "not_applicable"
73 assert report.ok
74
75
76 def test_ok_when_aligned_and_marker_matches(tmp_path: Path) -> None:
77 write_config(tmp_path, "config-git-only.yaml")
78 _write_lock(tmp_path)
79 _seed_docs(tmp_path, claim="cafebabe")
80 stamp = "2026-07-28T00:00:00Z"
81 (tmp_path / ".overseer" / "last_governance_sync").write_text(
82 f"{stamp}\nr1=cafebabe\nr3=cafebabe\n", encoding="utf-8"
83 )
84 config = load_fixture_config(tmp_path, "config-git-only.yaml")
85 report = check_governance_freshness(config, tmp_path, adapter=_adapter())
86 assert report.state == "ok"
87 assert report.ok
88 assert report.d1 == "aligned"
89 assert report.d2 == "aligned"
90
91
92 def test_drifted_d1(tmp_path: Path) -> None:
93 write_config(tmp_path, "config-git-only.yaml")
94 _write_lock(tmp_path)
95 _seed_docs(tmp_path, claim="deadbeef")
96 config = load_fixture_config(tmp_path, "config-git-only.yaml")
97 report = check_governance_freshness(config, tmp_path, adapter=_adapter(tip="cafebabe"))
98 assert report.state == "drifted"
99 assert not report.ok
100 assert report.d1 == "drifted"
101 assert "governance-sync" in (report.remediation or "")
102
103
104 def test_drifted_d2(tmp_path: Path) -> None:
105 write_config(tmp_path, "config-git-only.yaml")
106 _write_lock(tmp_path)
107 _seed_docs(tmp_path, claim="cafebabe")
108 adapter = _adapter(tip="cafebabe")
109 adapter.read_canonical_anchor.return_value = AnchorResult(
110 anchor_sha="aaaaaaaa", source="origin/main"
111 )
112 # R1 still cafebabe via read_head; D2 compares r2 anchor vs r3 (=r1 for git-only)
113 # Force r3 path: for git-only r3 = r1, so change read_head for origin/main only once.
114 # Instead make anchor differ from r1 by returning different tip from read_head vs anchor.
115 adapter.read_head.return_value = HeadResult(sha="cafebabe", kind="git")
116 adapter.read_canonical_anchor.return_value = AnchorResult(
117 anchor_sha="deadbeef00", source="weird"
118 )
119 # For git-only D2: if r3 is None use r1; else compare r2 to r3.
120 # Actually git-only sets r3 = r1 in freshness reads, so D2 compares anchor to r1.
121 # So anchor deadbeef00 vs r1 cafebabe → drifted.
122 config = load_fixture_config(tmp_path, "config-git-only.yaml")
123 report = check_governance_freshness(config, tmp_path, adapter=adapter)
124 assert report.state == "drifted"
125 assert report.d2 == "drifted"
126
127
128 def test_stale_marker_missing(tmp_path: Path) -> None:
129 write_config(tmp_path, "config-git-only.yaml")
130 _write_lock(tmp_path)
131 _seed_docs(tmp_path, claim="cafebabe")
132 config = load_fixture_config(tmp_path, "config-git-only.yaml")
133 report = check_governance_freshness(config, tmp_path, adapter=_adapter())
134 assert report.state == "stale_marker"
135 assert report.marker_present is False
136
137
138 def test_stale_marker_r1_mismatch(tmp_path: Path) -> None:
139 write_config(tmp_path, "config-git-only.yaml")
140 _write_lock(tmp_path)
141 _seed_docs(tmp_path, claim="cafebabe")
142 (tmp_path / ".overseer" / "last_governance_sync").write_text(
143 "2026-07-28T00:00:00Z\nr1=oldsha00\nr3=oldsha00\n", encoding="utf-8"
144 )
145 config = load_fixture_config(tmp_path, "config-git-only.yaml")
146 report = check_governance_freshness(config, tmp_path, adapter=_adapter(tip="cafebabe"))
147 assert report.state == "stale_marker"
148 assert report.marker_present is True
149
150
151 def test_stale_marker_legacy_timestamp_only(tmp_path: Path) -> None:
152 write_config(tmp_path, "config-git-only.yaml")
153 _write_lock(tmp_path)
154 _seed_docs(tmp_path, claim="cafebabe")
155 (tmp_path / ".overseer" / "last_governance_sync").write_text(
156 "2026-07-28T00:00:00Z\n", encoding="utf-8"
157 )
158 config = load_fixture_config(tmp_path, "config-git-only.yaml")
159 report = check_governance_freshness(config, tmp_path, adapter=_adapter())
160 assert report.state == "stale_marker"
161 assert "legacy" in report.message.lower() or "timestamp" in report.message.lower()
162
163
164 def test_unreadable_on_r1_failure(tmp_path: Path) -> None:
165 write_config(tmp_path, "config-git-only.yaml")
166 _write_lock(tmp_path)
167 _seed_docs(tmp_path)
168 config = load_fixture_config(tmp_path, "config-git-only.yaml")
169 report = check_governance_freshness(config, tmp_path, adapter=_adapter(fail="r1"))
170 assert report.state == "unreadable"
171 assert not report.ok
172
173
174 def test_d3_only_drift_does_not_force_not_ok(tmp_path: Path) -> None:
175 """D3 drifted alone must not fail GFG when D1/D2 aligned and marker current."""
176 write_config(tmp_path, "config-git-only.yaml")
177 _write_lock(tmp_path)
178 docs = tmp_path / "docs"
179 docs.mkdir(parents=True, exist_ok=True)
180 (docs / "OVERSEER-HANDOVER.md").write_text(
181 "| GitHub `main` | `cafebabe` |\n", encoding="utf-8"
182 )
183 # Roadmap claims MERGED for a phase with empty R4 — D3 may be aligned by vacuity.
184 # Explicitly: GFG ignores D3; even if detect_drift would mark D3 drifted with PRs,
185 # freshness with empty R4 stays ok when D1/D2 + marker good.
186 (docs / "ROADMAP.md").write_text(
187 "## Build queue\n\n"
188 "| Phase | Model | Status | Deliverable |\n"
189 "| --- | --- | --- | --- |\n"
190 "| **Z** | Auto | MERGED | something |\n",
191 encoding="utf-8",
192 )
193 (tmp_path / ".overseer" / "last_governance_sync").write_text(
194 "2026-07-28T00:00:00Z\nr1=cafebabe\nr3=cafebabe\n", encoding="utf-8"
195 )
196 config = load_fixture_config(tmp_path, "config-git-only.yaml")
197 report = check_governance_freshness(config, tmp_path, adapter=_adapter())
198 # With empty R4, D3 is drifted (MERGED row with no matching PR) — GFG still ok
199 assert report.ok
200 assert report.state == "ok"
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 1 day ago