test_land_closeout_status.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """Integration: land-closeout wiring across status / land-closeout / land-check / |
| 2 | governance-sync (§PMHF.10 integration).""" |
| 3 | |
| 4 | from __future__ import annotations |
| 5 | |
| 6 | import io |
| 7 | import json |
| 8 | import subprocess |
| 9 | from contextlib import redirect_stdout |
| 10 | from pathlib import Path |
| 11 | |
| 12 | from adapters.config import load_config |
| 13 | from cli.kit_root import kit_root |
| 14 | from tests.support import ( |
| 15 | FIXTURES, |
| 16 | git_status_runner, |
| 17 | land_a_fence_body, |
| 18 | land_handover_text, |
| 19 | land_roadmap_text, |
| 20 | make_runner, |
| 21 | ok, |
| 22 | run_cli, |
| 23 | seed_land_repo, |
| 24 | ) |
| 25 | from tools.close_ritual.land_check import run_land_check |
| 26 | |
| 27 | |
| 28 | def _seed_via_init(tmp_path: Path, *, claim: str, marker_tip: str | None) -> None: |
| 29 | assert ( |
| 30 | run_cli( |
| 31 | ["init", "--from-config", str(FIXTURES / "config-git-only.yaml"), "--non-interactive"], |
| 32 | cwd=tmp_path, |
| 33 | runner=git_status_runner(tip="cafebabe"), |
| 34 | ) |
| 35 | == 0 |
| 36 | ) |
| 37 | (tmp_path / "docs" / "OVERSEER-HANDOVER.md").write_text( |
| 38 | land_handover_text(claim), encoding="utf-8" |
| 39 | ) |
| 40 | (tmp_path / "docs" / "ROADMAP.md").write_text(land_roadmap_text(), encoding="utf-8") |
| 41 | marker = tmp_path / ".overseer" / "last_governance_sync" |
| 42 | if marker_tip is None: |
| 43 | if marker.exists(): |
| 44 | marker.unlink() |
| 45 | else: |
| 46 | marker.write_text( |
| 47 | f"2026-07-30T00:00:00Z\nr1={marker_tip}\nr3={marker_tip}\n", encoding="utf-8" |
| 48 | ) |
| 49 | |
| 50 | |
| 51 | def test_status_exit_2_post_merge_incomplete_when_land_a_and_d1_drifted( |
| 52 | tmp_path: Path, capsys |
| 53 | ) -> None: |
| 54 | _seed_via_init(tmp_path, claim="deadbeef", marker_tip="cafebabe") |
| 55 | capsys.readouterr() |
| 56 | code = run_cli( |
| 57 | ["status", "--json", "--exit-code"], |
| 58 | cwd=tmp_path, |
| 59 | runner=git_status_runner(tip="cafebabe"), |
| 60 | json_mode=True, |
| 61 | ) |
| 62 | assert code == 2 |
| 63 | payload = json.loads(capsys.readouterr().out) |
| 64 | assert payload["land_closeout"]["state"] == "post_merge_incomplete" |
| 65 | assert payload["land_closeout"]["ok"] is False |
| 66 | assert payload["land_closeout"]["land_phase"] == "land-a" |
| 67 | assert payload["land_closeout"]["remediation"].startswith("land-b required:") |
| 68 | |
| 69 | |
| 70 | def test_status_exit_0_when_land_a_and_aligned(tmp_path: Path, capsys) -> None: |
| 71 | _seed_via_init(tmp_path, claim="cafebabe", marker_tip="cafebabe") |
| 72 | capsys.readouterr() |
| 73 | code = run_cli( |
| 74 | ["status", "--json", "--exit-code"], |
| 75 | cwd=tmp_path, |
| 76 | runner=git_status_runner(tip="cafebabe"), |
| 77 | json_mode=True, |
| 78 | ) |
| 79 | assert code == 0 |
| 80 | payload = json.loads(capsys.readouterr().out) |
| 81 | assert payload["land_closeout"]["state"] == "land_a_in_progress" |
| 82 | assert payload["land_closeout"]["ok"] is True |
| 83 | |
| 84 | |
| 85 | def test_land_closeout_command_exit_codes(tmp_path: Path, capsys) -> None: |
| 86 | _seed_via_init(tmp_path, claim="cafebabe", marker_tip="cafebabe") |
| 87 | capsys.readouterr() |
| 88 | code = run_cli( |
| 89 | ["land-closeout", "--json"], |
| 90 | cwd=tmp_path, |
| 91 | runner=git_status_runner(tip="cafebabe"), |
| 92 | json_mode=True, |
| 93 | ) |
| 94 | assert code == 0 |
| 95 | payload = json.loads(capsys.readouterr().out) |
| 96 | assert payload["state"] == "land_a_in_progress" |
| 97 | assert payload["exit_code"] == 0 |
| 98 | |
| 99 | (tmp_path / "docs" / "OVERSEER-HANDOVER.md").write_text( |
| 100 | land_handover_text("deadbeef"), encoding="utf-8" |
| 101 | ) |
| 102 | capsys.readouterr() |
| 103 | code = run_cli( |
| 104 | ["land-closeout", "--json"], |
| 105 | cwd=tmp_path, |
| 106 | runner=git_status_runner(tip="cafebabe"), |
| 107 | json_mode=True, |
| 108 | ) |
| 109 | assert code == 2 |
| 110 | payload = json.loads(capsys.readouterr().out) |
| 111 | assert payload["state"] == "post_merge_incomplete" |
| 112 | assert payload["exit_code"] == 2 |
| 113 | |
| 114 | |
| 115 | def test_land_closeout_human_output_frozen_tokens(tmp_path: Path, capsys) -> None: |
| 116 | _seed_via_init(tmp_path, claim="deadbeef", marker_tip="cafebabe") |
| 117 | capsys.readouterr() |
| 118 | code = run_cli( |
| 119 | ["land-closeout"], |
| 120 | cwd=tmp_path, |
| 121 | runner=git_status_runner(tip="cafebabe"), |
| 122 | ) |
| 123 | out = capsys.readouterr().out |
| 124 | assert code == 2 |
| 125 | assert "land_closeout: post_merge_incomplete" in out |
| 126 | assert ( |
| 127 | "land_closeout-remediation: land-b required: ok governance-sync --dry-run " |
| 128 | "then apply; paste land-b; do not re-paste land-a" |
| 129 | ) in out |
| 130 | |
| 131 | |
| 132 | # --- ok land-check refusal (§PMHF.6.2) --- |
| 133 | |
| 134 | |
| 135 | def _ritual_config_yaml(tmp_path: Path) -> Path: |
| 136 | cfg = tmp_path / ".overseer" / "config.yaml" |
| 137 | cfg.parent.mkdir(parents=True, exist_ok=True) |
| 138 | cfg.write_text( |
| 139 | """ |
| 140 | overseer_config_version: 1 |
| 141 | repo: |
| 142 | name: fixture |
| 143 | root_relative_docs: "docs" |
| 144 | vcs: |
| 145 | regime: git-only |
| 146 | canonical: git |
| 147 | git: |
| 148 | remote: origin |
| 149 | main_branch: main |
| 150 | mirror_branch: null |
| 151 | feature_branch_pattern: "feat/{slug}" |
| 152 | muse: |
| 153 | staging_remote: null |
| 154 | main_branch: null |
| 155 | working_dir: null |
| 156 | docs: |
| 157 | handover: OVERSEER-HANDOVER.md |
| 158 | roadmap: ROADMAP.md |
| 159 | standing_decisions: ROADMAP.md |
| 160 | thresholds: |
| 161 | realign_max_commits: 50 |
| 162 | drift_warn_only: true |
| 163 | freeze_contract: |
| 164 | enabled: true |
| 165 | reviewer: |
| 166 | mode: agent |
| 167 | model: thinking-high |
| 168 | provider: local |
| 169 | fallback: human |
| 170 | human_escalation: [security] |
| 171 | close_ritual: |
| 172 | enabled: true |
| 173 | mode: verify_landed |
| 174 | require_paths: ["BOARD.json"] |
| 175 | """, |
| 176 | encoding="utf-8", |
| 177 | ) |
| 178 | return cfg |
| 179 | |
| 180 | |
| 181 | def _seed_land_check_repo(tmp_path: Path, *, handover_text: str) -> object: |
| 182 | init = subprocess.run( |
| 183 | ["git", "init", "-b", "main"], cwd=tmp_path, capture_output=True, text=True |
| 184 | ) |
| 185 | assert init.returncode == 0, init.stderr |
| 186 | subprocess.run( |
| 187 | ["git", "config", "user.email", "[email protected]"], |
| 188 | cwd=tmp_path, |
| 189 | check=True, |
| 190 | capture_output=True, |
| 191 | ) |
| 192 | subprocess.run( |
| 193 | ["git", "config", "user.name", "t"], cwd=tmp_path, check=True, capture_output=True |
| 194 | ) |
| 195 | (tmp_path / "BOARD.json").write_text('{"ok": true}\n', encoding="utf-8") |
| 196 | subprocess.run(["git", "add", "-A"], cwd=tmp_path, check=True, capture_output=True) |
| 197 | subprocess.run( |
| 198 | ["git", "commit", "-m", "init"], cwd=tmp_path, check=True, capture_output=True |
| 199 | ) |
| 200 | cfg_path = _ritual_config_yaml(tmp_path) |
| 201 | (tmp_path / ".overseer" / "version.lock").write_text( |
| 202 | "lock_version: 1\nkit_version: 0.1.0\nconfig_version: 1\n" |
| 203 | "footprint_digest: sha256:" + ("0" * 64) + "\n" |
| 204 | 'installed_at: "2026-01-01T00:00:00Z"\nsynced_at: "2026-01-01T00:00:00Z"\n' |
| 205 | "footprint: []\n", |
| 206 | encoding="utf-8", |
| 207 | ) |
| 208 | docs = tmp_path / "docs" |
| 209 | docs.mkdir(parents=True, exist_ok=True) |
| 210 | (docs / "OVERSEER-HANDOVER.md").write_text(handover_text, encoding="utf-8") |
| 211 | (docs / "ROADMAP.md").write_text(land_roadmap_text(), encoding="utf-8") |
| 212 | (tmp_path / ".overseer" / "last_governance_sync").write_text( |
| 213 | "2026-07-30T00:00:00Z\nr1=cafebabe\nr3=cafebabe\n", encoding="utf-8" |
| 214 | ) |
| 215 | return load_config(cfg_path) |
| 216 | |
| 217 | |
| 218 | def test_land_check_refuses_landed_while_land_a(tmp_path: Path) -> None: |
| 219 | config = _seed_land_check_repo(tmp_path, handover_text=land_handover_text("cafebabe")) |
| 220 | runner = make_runner( |
| 221 | { |
| 222 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 223 | "git status --porcelain": ok(""), |
| 224 | "git rev-parse origin/main": ok("cafebabe"), |
| 225 | } |
| 226 | ) |
| 227 | result = run_land_check(config, tmp_path, runner=runner) |
| 228 | assert result.exit_code == 2 |
| 229 | assert result.landed is False |
| 230 | assert any("land_closeout: land_a_in_progress" in m for m in result.messages) |
| 231 | |
| 232 | |
| 233 | def test_land_check_refuses_landed_on_post_merge_incomplete(tmp_path: Path) -> None: |
| 234 | handover = land_handover_text( |
| 235 | "cafebabe", |
| 236 | fence_body=land_a_fence_body(paste_extra="PR #206 open — waiting for merge.\n"), |
| 237 | ) |
| 238 | config = _seed_land_check_repo(tmp_path, handover_text=handover) |
| 239 | runner = make_runner( |
| 240 | { |
| 241 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 242 | "git status --porcelain": ok(""), |
| 243 | "git rev-parse origin/main": ok("cafebabe"), |
| 244 | "gh pr view 206 --json state,mergedAt": ok( |
| 245 | '{"state": "MERGED", "mergedAt": "2026-07-30T12:00:00Z"}' |
| 246 | ), |
| 247 | } |
| 248 | ) |
| 249 | result = run_land_check(config, tmp_path, runner=runner) |
| 250 | assert result.exit_code == 2 |
| 251 | assert result.landed is False |
| 252 | assert any("land_closeout: post_merge_incomplete" in m for m in result.messages) |
| 253 | assert any("land-b required:" in m for m in result.messages) |
| 254 | assert not any("landed: true" in m.lower() for m in result.messages) |
| 255 | |
| 256 | |
| 257 | # --- governance-sync dry-run plans land-b (§PMHF.3.4) --- |
| 258 | |
| 259 | |
| 260 | def _sync_runner(): |
| 261 | return make_runner( |
| 262 | { |
| 263 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 264 | "git status --porcelain": ok(""), |
| 265 | "git rev-parse origin/main": ok("cafebabe"), |
| 266 | "gh pr list --state merged --limit 5 --json number,title,mergeCommit,mergedAt": ok( |
| 267 | "[]" |
| 268 | ), |
| 269 | "git remote get-url origin": ok("[email protected]:owner/repo.git"), |
| 270 | } |
| 271 | ) |
| 272 | |
| 273 | |
| 274 | def test_governance_sync_dry_run_plans_land_b_when_land_a_and_drifted( |
| 275 | tmp_path: Path, |
| 276 | ) -> None: |
| 277 | seed_land_repo(tmp_path, claim="deadbeef") |
| 278 | handover = tmp_path / "docs" / "OVERSEER-HANDOVER.md" |
| 279 | before_h = handover.read_text(encoding="utf-8") |
| 280 | |
| 281 | buf = io.StringIO() |
| 282 | with redirect_stdout(buf): |
| 283 | code = run_cli( |
| 284 | ["governance-sync"], |
| 285 | cwd=tmp_path, |
| 286 | runner=_sync_runner(), |
| 287 | kit=kit_root(), |
| 288 | ) |
| 289 | out = buf.getvalue() |
| 290 | assert code == 0 |
| 291 | assert "next_regen: regenerated (land-b)" in out |
| 292 | # §PMHF.3.4 rule 4: dry-run shows the planned land-b body; no doc writes. |
| 293 | assert "land-phase: land-b" in out |
| 294 | assert "ID: PMHF land-b (post-merge sync)" in out |
| 295 | assert handover.read_text(encoding="utf-8") == before_h |
| 296 | |
| 297 | |
| 298 | def test_governance_sync_preserves_land_a_paste_mid_wait(tmp_path: Path) -> None: |
| 299 | # Land-a posture + D3-only drift must not clobber the land-a paste (fail closed). |
| 300 | seed_land_repo( |
| 301 | tmp_path, |
| 302 | claim="cafebabe", |
| 303 | roadmap_text=land_roadmap_text( |
| 304 | "| **PMHF → main** | Operator + Auto | **TODO** | Land PMHF |", |
| 305 | ), |
| 306 | ) |
| 307 | runner = make_runner( |
| 308 | { |
| 309 | "git rev-parse --abbrev-ref HEAD": ok("main"), |
| 310 | "git status --porcelain": ok(""), |
| 311 | "git rev-parse origin/main": ok("cafebabe"), |
| 312 | "gh pr list --state merged --limit 5 --json number,title,mergeCommit,mergedAt": ok( |
| 313 | '[{"number": 999, "title": "PMHF land hygiene", ' |
| 314 | '"mergeCommit": {"oid": "abcabcabc"}, "mergedAt": "2026-07-30T00:00:00Z"}]' |
| 315 | ), |
| 316 | "git remote get-url origin": ok("[email protected]:owner/repo.git"), |
| 317 | } |
| 318 | ) |
| 319 | buf = io.StringIO() |
| 320 | with redirect_stdout(buf): |
| 321 | code = run_cli(["governance-sync"], cwd=tmp_path, runner=runner, kit=kit_root()) |
| 322 | out = buf.getvalue() |
| 323 | assert code == 0 |
| 324 | assert "next_regen: human_authorship_required (land_a_in_progress)" in out |