test_gsw_write_path.py python
257 lines 10.0 KB
Raw
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 3 days ago
1 """Unit tests for the §GSW governance-sync write-path order (§GSW.10 unit tier).
2
3 Covers: apply-plan ordering (realign → branch ensure → doc writes),
4 ``commit_feature`` already-on-branch short-circuit for all three adapters,
5 Muse dirty-carry checkout, branch-state capture/restore, and
6 marker-not-written-on-commit-failure.
7 """
8
9 from __future__ import annotations
10
11 from pathlib import Path
12
13 from cli.kit_root import kit_root
14 from tests.support import (
15 adapter_for,
16 fail,
17 gsw_runner,
18 make_runner,
19 ok,
20 run_cli,
21 seed_gsw_repo,
22 )
23 from tools.governance_hygiene.engine import (
24 BranchState,
25 _capture_branch_state,
26 _restore_branch_state,
27 )
28
29
30 class _EventRunner:
31 """Wrap a runner and interleave its commands into a shared event list."""
32
33 def __init__(self, inner, events: list) -> None:
34 self.inner = inner
35 self.events = events
36
37 def run(self, command: str, *, cwd: str | None = None):
38 self.events.append(("cmd", command))
39 return self.inner.run(command, cwd=cwd)
40
41 @property
42 def calls(self):
43 return self.inner.calls
44
45
46 def test_apply_plan_realign_then_ensure_then_writes(tmp_path: Path, monkeypatch) -> None:
47 """§GSW.3.1: realign runs on the original branch before branch ensure,
48 and no doc write precedes successful branch ensure."""
49 seed_gsw_repo(tmp_path, "git-only")
50 events: list = []
51
52 from tools.governance_hygiene import engine as engine_mod
53
54 real_guard = engine_mod.execute_realign_guard
55
56 def recording_guard(config, adapter, reads, drift, *, dry_run):
57 events.append(("realign", dry_run))
58 return real_guard(config, adapter, reads, drift, dry_run=dry_run)
59
60 real_write = engine_mod.atomic_write_text
61
62 def recording_write(path: Path, text: str) -> None:
63 events.append(("write", path.name))
64 real_write(path, text)
65
66 monkeypatch.setattr(engine_mod, "execute_realign_guard", recording_guard)
67 monkeypatch.setattr(engine_mod, "atomic_write_text", recording_write)
68
69 runner = _EventRunner(gsw_runner(tmp_path, "git-only"), events)
70 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
71 assert code == 0
72
73 realign_apply = events.index(("realign", False))
74 first_checkout = next(
75 index for index, event in enumerate(events)
76 if event[0] == "cmd" and "git checkout -b" in event[1]
77 )
78 doc_writes = [
79 index for index, event in enumerate(events)
80 if event[0] == "write" and event[1] in {"OVERSEER-HANDOVER.md", "ROADMAP.md"}
81 ]
82 assert doc_writes, "expected handover/roadmap writes"
83 assert realign_apply < first_checkout, "realign must run before branch ensure"
84 assert first_checkout < min(doc_writes), "no doc write may precede branch ensure"
85
86
87 def test_commit_feature_short_circuit_git_only(git_only_config, repo_root) -> None:
88 """§GSW.6.1: already on branch → skip checkout, commit dirty docs."""
89 runner = make_runner(
90 {
91 "git rev-parse --abbrev-ref HEAD": ok("feat/gsw"),
92 "git add": ok(""),
93 "git commit": ok(""),
94 "git rev-parse HEAD": ok("feedface"),
95 }
96 )
97 adapter = adapter_for(git_only_config, repo_root, runner)
98 result = adapter.commit_feature(branch="feat/gsw", message="m", paths=["docs/ROADMAP.md"])
99 assert result.committed is True
100 assert not any("checkout" in call[0] for call in runner.calls)
101
102
103 def test_commit_feature_short_circuit_muse_only(muse_only_config, repo_root) -> None:
104 root = str(repo_root)
105 runner = make_runner(
106 {
107 f"muse -C {root} rev-parse --abbrev-ref HEAD": ok("feat/gsw"),
108 f"muse -C {root} code add": ok(""),
109 f"muse -C {root} commit": ok(""),
110 f"muse -C {root} rev-parse HEAD": ok("sha256:abc"),
111 }
112 )
113 adapter = adapter_for(muse_only_config, repo_root, runner)
114 result = adapter.commit_feature(branch="feat/gsw", message="m", paths=["docs/R.md"])
115 assert result.committed is True
116 assert not any("checkout" in call[0] for call in runner.calls)
117 _assert_muse_stage_uses_code_add(runner.calls)
118
119
120 def test_commit_feature_short_circuit_muse_git_mirror(muse_git_mirror_config, repo_root) -> None:
121 root = str(repo_root)
122 runner = make_runner(
123 {
124 f"muse -C {root} rev-parse --abbrev-ref HEAD": ok("feat/gsw"),
125 f"muse -C {root} code add": ok(""),
126 f"muse -C {root} commit": ok(""),
127 f"muse -C {root} rev-parse HEAD": ok("sha256:abc"),
128 }
129 )
130 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
131 result = adapter.commit_feature(branch="feat/gsw", message="m", paths=["docs/R.md"])
132 assert result.committed is True
133 assert not any("checkout" in call[0] for call in runner.calls)
134 _assert_muse_stage_uses_code_add(runner.calls)
135
136
137 def _assert_muse_stage_uses_code_add(calls) -> None:
138 """Live Muse 0.2.x has no top-level `add` — staging must be `muse code add`
139 (GSW land-b live regression)."""
140 stage_calls = [call[0] for call in calls if " add " in call[0] or call[0].endswith(" add")]
141 assert stage_calls, "expected a staging command"
142 for command in stage_calls:
143 assert " code add" in command, f"bare `muse add` is not a live subcommand: {command}"
144
145
146 def test_muse_only_dirty_off_branch_uses_autoshelf(muse_only_config, repo_root) -> None:
147 """§GSW.6.2: off-branch + dirty tree → dirty-carry flag, never bare-checkout-only."""
148 runner = gsw_runner(
149 repo_root,
150 "muse-only",
151 muse_branch="main",
152 muse_dirty=True,
153 existing_muse_branches={"feat/gsw"},
154 )
155 adapter = adapter_for(muse_only_config, repo_root, runner)
156 result = adapter.commit_feature(branch="feat/gsw", message="m", paths=["docs/R.md"])
157 assert result.committed is True
158 assert runner.muse_branch == "feat/gsw"
159 assert any("--autoshelf" in call[0] for call in runner.calls)
160 assert not any("--force" in call[0] for call in runner.calls)
161
162
163 def test_muse_git_mirror_dirty_off_branch_uses_autoshelf(muse_git_mirror_config, repo_root) -> None:
164 runner = gsw_runner(
165 repo_root,
166 "muse+git-mirror",
167 muse_branch="main",
168 muse_dirty=True,
169 existing_muse_branches={"feat/gsw"},
170 )
171 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
172 result = adapter.commit_feature(branch="feat/gsw", message="m", paths=["docs/R.md"])
173 assert result.committed is True
174 assert runner.muse_branch == "feat/gsw"
175 assert any("--autoshelf" in call[0] for call in runner.calls)
176 assert not any("--force" in call[0] for call in runner.calls)
177
178
179 def test_capture_branch_state_fails_closed_on_unreadable_head(
180 git_only_config, repo_root
181 ) -> None:
182 """§GSW.4.1: unreadable HEAD → (None, failing command); no fabrication."""
183 runner = make_runner({"git rev-parse --abbrev-ref HEAD": fail("bad head")})
184 adapter = adapter_for(git_only_config, repo_root, runner)
185 state, command = _capture_branch_state(git_only_config, adapter, runner, repo_root)
186 assert state is None
187 assert command == "git rev-parse --abbrev-ref HEAD"
188
189
190 def test_capture_branch_state_dual_fields_for_mirror(muse_git_mirror_config, repo_root) -> None:
191 runner = gsw_runner(repo_root, "muse+git-mirror", git_branch="work", muse_branch="work")
192 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
193 state, command = _capture_branch_state(muse_git_mirror_config, adapter, runner, repo_root)
194 assert command is None
195 assert state == BranchState(git_branch="work", muse_branch="work")
196
197
198 def test_restore_branch_state_restores_each_regime(
199 git_only_config, muse_only_config, muse_git_mirror_config, repo_root
200 ) -> None:
201 """§GSW.4.2: rollback returns HEAD(s) to the captured original branch."""
202 cases = [
203 (git_only_config, "git-only"),
204 (muse_only_config, "muse-only"),
205 (muse_git_mirror_config, "muse+git-mirror"),
206 ]
207 for config, regime in cases:
208 runner = gsw_runner(
209 repo_root,
210 regime,
211 git_branch="feat/gsw",
212 muse_branch="feat/gsw",
213 existing_git_branches={"main"},
214 existing_muse_branches={"main"},
215 )
216 adapter = adapter_for(config, repo_root, runner)
217 state = BranchState(git_branch="main", muse_branch="main")
218 errors = _restore_branch_state(config, adapter, runner, repo_root, state)
219 assert errors == ()
220 if regime in {"git-only", "muse+git-mirror"}:
221 assert runner.git_branch == "main"
222 if regime in {"muse-only", "muse+git-mirror"}:
223 assert runner.muse_branch == "main"
224 assert not any("--force" in call[0] for call in runner.calls)
225
226
227 def test_restore_branch_state_dirty_muse_uses_autoshelf(
228 muse_git_mirror_config, repo_root
229 ) -> None:
230 """§GSW.4.3: dirty Muse restore falls back to --autoshelf, never --force."""
231 runner = gsw_runner(
232 repo_root,
233 "muse+git-mirror",
234 git_branch="feat/gsw",
235 muse_branch="feat/gsw",
236 muse_dirty=True,
237 existing_git_branches={"main"},
238 existing_muse_branches={"main"},
239 )
240 adapter = adapter_for(muse_git_mirror_config, repo_root, runner)
241 state = BranchState(git_branch="main", muse_branch="main")
242 errors = _restore_branch_state(muse_git_mirror_config, adapter, runner, repo_root, state)
243 assert errors == ()
244 assert runner.muse_branch == "main"
245 assert runner.git_branch == "main"
246 assert any("--autoshelf" in call[0] for call in runner.calls)
247 assert not any("--force" in call[0] for call in runner.calls)
248
249
250 def test_marker_not_written_when_commit_fails(tmp_path: Path) -> None:
251 """§GSW.3.4: sync marker is written only after successful commit_feature."""
252 seed_gsw_repo(tmp_path, "git-only")
253 runner = gsw_runner(tmp_path, "git-only", git_commit_fails=True)
254 code = run_cli(["governance-sync", "--write"], cwd=tmp_path, runner=runner, kit=kit_root())
255 assert code == 2
256 assert not (tmp_path / ".overseer" / "last_governance_sync").exists()
257 assert runner.git_branch == "main"
File History 1 commit
sha256:a78e7e5a8740e03315f325d19edeb3aa1b306b3337d04abbaa9a9e0f3bbeb7a1 docs: MuseHub-first before ISR #74 — staging solidify NEXT Human 3 days ago