gabriel / muse public
test_bridge_git_status.py python
355 lines 13.2 KB
Raw
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf chore: bump version to 0.2.0rc14 Sonnet 4.6 patch 16 hours ago
1 """Phase 4 TDD tests for ``muse bridge git-status`` — drift counts.
2
3 NOTE: git subprocess calls in this file are INTENTIONAL — they create real
4 git repositories used as bridge sources. The muse codebase otherwise never
5 uses git.
6 """
7
8 from __future__ import annotations
9
10 import json
11 import os
12 import pathlib
13 import subprocess
14
15 import pytest
16
17 from muse.core.paths import init_repo_dirs
18 from muse.core.types import fake_id
19 from muse.core.bridge.state import BridgeState
20 from tests.cli_test_helper import CliRunner
21
22 runner = CliRunner()
23
24
25 # ---------------------------------------------------------------------------
26 # Helpers
27 # ---------------------------------------------------------------------------
28
29 def _invoke(*args: str, cwd: pathlib.Path | None = None) -> "CliRunner":
30 return runner.invoke(None, list(args), cwd=cwd)
31
32
33 def _make_muse_repo(path: pathlib.Path) -> pathlib.Path:
34 path.mkdir(parents=True, exist_ok=True)
35 result = _invoke("init", cwd=path)
36 assert result.exit_code == 0, f"muse init failed: {result.stderr}"
37 return path
38
39
40 def _make_git_repo(path: pathlib.Path) -> pathlib.Path:
41 """Create a minimal git repo with one initial commit."""
42 path.mkdir(parents=True, exist_ok=True)
43 subprocess.run(["git", "init", str(path)], check=True, capture_output=True)
44 subprocess.run(
45 ["git", "-C", str(path), "config", "user.email", "[email protected]"],
46 check=True, capture_output=True,
47 )
48 subprocess.run(
49 ["git", "-C", str(path), "config", "user.name", "Test"],
50 check=True, capture_output=True,
51 )
52 (path / "README.md").write_text("init")
53 subprocess.run(["git", "-C", str(path), "add", "."], check=True, capture_output=True)
54 subprocess.run(
55 ["git", "-C", str(path), "commit", "-m", "init"],
56 check=True, capture_output=True,
57 )
58 return path
59
60
61 def _git_head_sha(git_dir: pathlib.Path) -> str:
62 return subprocess.check_output(
63 ["git", "-C", str(git_dir), "rev-parse", "HEAD"],
64 text=True,
65 ).strip()
66
67
68 def _git_add_commit(git_dir: pathlib.Path, filename: str, content: str, msg: str) -> str:
69 (git_dir / filename).write_text(content)
70 subprocess.run(["git", "-C", str(git_dir), "add", "."], check=True, capture_output=True)
71 subprocess.run(
72 ["git", "-C", str(git_dir), "commit", "-m", msg],
73 check=True, capture_output=True,
74 )
75 return _git_head_sha(git_dir)
76
77
78 def _write_bridge_state(muse_root: pathlib.Path, state: BridgeState) -> None:
79 from muse.core.bridge.state import write_bridge_state
80 write_bridge_state(muse_root, state)
81
82
83 # ===========================================================================
84 # Tests
85 # ===========================================================================
86
87 class TestGitStatusNoBridgeState:
88 """git-status with no bridge state at all."""
89
90 def test_git_status_no_bridge_state_shows_none(self, tmp_path: pathlib.Path) -> None:
91 """Fresh muse repo: git-status prints '(none)' for both last-import and last-export."""
92 muse_root = _make_muse_repo(tmp_path / "muse")
93 result = _invoke("bridge", "git-status", cwd=muse_root)
94 assert result.exit_code == 0, result.stderr
95 out = result.output
96 assert "(none)" in out
97
98 def test_git_status_no_bridge_state_json_has_keys(self, tmp_path: pathlib.Path) -> None:
99 """JSON output always has last_import and last_export keys."""
100 muse_root = _make_muse_repo(tmp_path / "muse")
101 result = _invoke("bridge", "git-status", "--json", cwd=muse_root)
102 assert result.exit_code == 0, result.stderr
103 data = json.loads(result.output.strip())
104 assert "last_import" in data
105 assert "last_export" in data
106
107
108 class TestGitStatusJsonOutput:
109 """JSON output shape for git-status."""
110
111 def test_git_status_json_has_last_import_last_export(self, tmp_path: pathlib.Path) -> None:
112 """JSON output has both last_import and last_export keys even when empty."""
113 muse_root = _make_muse_repo(tmp_path / "muse")
114 result = _invoke("bridge", "git-status", "--json", cwd=muse_root)
115 assert result.exit_code == 0, result.stderr
116 data = json.loads(result.output.strip())
117 assert isinstance(data["last_import"], dict)
118 assert isinstance(data["last_export"], dict)
119
120 def test_git_status_json_drift_absent_without_git_dir(self, tmp_path: pathlib.Path) -> None:
121 """Without --git-dir, JSON output has no 'drift' key."""
122 muse_root = _make_muse_repo(tmp_path / "muse")
123 result = _invoke("bridge", "git-status", "--json", cwd=muse_root)
124 assert result.exit_code == 0, result.stderr
125 data = json.loads(result.output.strip())
126 assert "drift" not in data
127
128 def test_git_status_json_drift_present_with_git_dir(self, tmp_path: pathlib.Path) -> None:
129 """With --git-dir pointing at a real git repo, 'drift' key is present in JSON."""
130 muse_root = _make_muse_repo(tmp_path / "muse")
131 git_dir = _make_git_repo(tmp_path / "git")
132 result = _invoke(
133 "bridge", "git-status",
134 "--git-dir", str(git_dir),
135 "--json",
136 cwd=muse_root,
137 )
138 assert result.exit_code == 0, result.stderr
139 data = json.loads(result.output.strip())
140 assert "drift" in data
141 assert "git_commits_since_import" in data["drift"]
142 assert "muse_commits_since_export" in data["drift"]
143
144
145 class TestGitStatusAfterImport:
146 """git-status reflects state written by a successful import."""
147
148 def test_git_status_after_import_shows_git_sha(self, tmp_path: pathlib.Path) -> None:
149 """After writing bridge state with last_import, git-status shows the git SHA."""
150 muse_root = _make_muse_repo(tmp_path / "muse")
151 fake_sha = "a" * 40
152 fake_cid = fake_id("commit-1")
153 _write_bridge_state(muse_root, {
154 "last_import": {
155 "git_sha": fake_sha,
156 "git_ref": "main",
157 "git_remote": "origin",
158 "muse_branch": "main",
159 "muse_commit_id": fake_cid,
160 "imported_at": "2026-04-14T10:00:00Z",
161 "commits_written": 5,
162 },
163 "last_export": {},
164 })
165
166 result = _invoke("bridge", "git-status", cwd=muse_root)
167 assert result.exit_code == 0, result.stderr
168 out = result.output
169 assert "aaaaaaaaaaaaaaaa" in out # first 16 chars of SHA
170
171 def test_git_status_after_import_json_shows_git_sha(self, tmp_path: pathlib.Path) -> None:
172 """After writing import state, JSON last_import has git_sha field."""
173 muse_root = _make_muse_repo(tmp_path / "muse")
174 fake_sha = "b" * 40
175 fake_cid = fake_id("commit-2")
176 _write_bridge_state(muse_root, {
177 "last_import": {
178 "git_sha": fake_sha,
179 "git_ref": "main",
180 "git_remote": "origin",
181 "muse_branch": "main",
182 "muse_commit_id": fake_cid,
183 "imported_at": "2026-04-14T10:00:00Z",
184 "commits_written": 2,
185 },
186 "last_export": {},
187 })
188
189 result = _invoke("bridge", "git-status", "--json", cwd=muse_root)
190 assert result.exit_code == 0, result.stderr
191 data = json.loads(result.output.strip())
192 assert data["last_import"]["git_sha"] == fake_sha
193
194
195 class TestGitStatusDrift:
196 """Drift count computation in git-status."""
197
198 def test_git_status_with_git_dir_shows_drift(self, tmp_path: pathlib.Path) -> None:
199 """git repo with 2 commits since last-import git_sha → drift=2."""
200 muse_root = _make_muse_repo(tmp_path / "muse")
201 git_dir = _make_git_repo(tmp_path / "git")
202
203 # The initial commit is the "sync point"
204 base_sha = _git_head_sha(git_dir)
205
206 # Add 2 more commits
207 _git_add_commit(git_dir, "a.txt", "a", "commit A")
208 _git_add_commit(git_dir, "b.txt", "b", "commit B")
209
210 fake_cid = fake_id("commit-muse")
211 _write_bridge_state(muse_root, {
212 "last_import": {
213 "git_sha": base_sha,
214 "git_ref": "main",
215 "git_remote": "origin",
216 "muse_branch": "main",
217 "muse_commit_id": fake_cid,
218 "imported_at": "2026-04-14T10:00:00Z",
219 "commits_written": 1,
220 },
221 "last_export": {},
222 })
223
224 result = _invoke(
225 "bridge", "git-status",
226 "--git-dir", str(git_dir),
227 "--json",
228 cwd=muse_root,
229 )
230 assert result.exit_code == 0, result.stderr
231 data = json.loads(result.output.strip())
232 assert data["drift"]["git_commits_since_import"] == 2
233
234 def test_git_status_zero_drift_when_up_to_date(self, tmp_path: pathlib.Path) -> None:
235 """git repo at same commit as last-import → drift=0."""
236 muse_root = _make_muse_repo(tmp_path / "muse")
237 git_dir = _make_git_repo(tmp_path / "git")
238 current_sha = _git_head_sha(git_dir)
239
240 fake_cid = fake_id("commit-muse")
241 _write_bridge_state(muse_root, {
242 "last_import": {
243 "git_sha": current_sha,
244 "git_ref": "main",
245 "git_remote": "origin",
246 "muse_branch": "main",
247 "muse_commit_id": fake_cid,
248 "imported_at": "2026-04-14T10:00:00Z",
249 "commits_written": 1,
250 },
251 "last_export": {},
252 })
253
254 result = _invoke(
255 "bridge", "git-status",
256 "--git-dir", str(git_dir),
257 "--json",
258 cwd=muse_root,
259 )
260 assert result.exit_code == 0, result.stderr
261 data = json.loads(result.output.strip())
262 assert data["drift"]["git_commits_since_import"] == 0
263
264 def test_git_status_drift_none_without_import_state(self, tmp_path: pathlib.Path) -> None:
265 """No import state → git_commits_since_import is None."""
266 muse_root = _make_muse_repo(tmp_path / "muse")
267 git_dir = _make_git_repo(tmp_path / "git")
268
269 result = _invoke(
270 "bridge", "git-status",
271 "--git-dir", str(git_dir),
272 "--json",
273 cwd=muse_root,
274 )
275 assert result.exit_code == 0, result.stderr
276 data = json.loads(result.output.strip())
277 assert data["drift"]["git_commits_since_import"] is None
278
279
280 class TestGitStatusTextOutput:
281 """Text output format for git-status."""
282
283 def test_git_status_text_output_has_drift_section(self, tmp_path: pathlib.Path) -> None:
284 """Text output with --git-dir contains 'Drift:' section."""
285 muse_root = _make_muse_repo(tmp_path / "muse")
286 git_dir = _make_git_repo(tmp_path / "git")
287
288 base_sha = _git_head_sha(git_dir)
289 _git_add_commit(git_dir, "x.txt", "x", "commit X")
290
291 fake_cid = fake_id("commit-muse-export")
292 _write_bridge_state(muse_root, {
293 "last_import": {
294 "git_sha": base_sha,
295 "git_ref": "main",
296 "git_remote": "origin",
297 "muse_branch": "main",
298 "muse_commit_id": fake_cid,
299 "imported_at": "2026-04-14T10:00:00Z",
300 "commits_written": 1,
301 },
302 "last_export": {},
303 })
304
305 result = _invoke(
306 "bridge", "git-status",
307 "--git-dir", str(git_dir),
308 cwd=muse_root,
309 )
310 assert result.exit_code == 0, result.stderr
311 assert "Drift:" in result.output
312
313 def test_git_status_text_shows_commits_to_import(self, tmp_path: pathlib.Path) -> None:
314 """Text output shows 'commits to import' in Drift section."""
315 muse_root = _make_muse_repo(tmp_path / "muse")
316 git_dir = _make_git_repo(tmp_path / "git")
317
318 base_sha = _git_head_sha(git_dir)
319 _git_add_commit(git_dir, "c.txt", "c", "commit C")
320
321 fake_cid = fake_id("commit-muse-x")
322 _write_bridge_state(muse_root, {
323 "last_import": {
324 "git_sha": base_sha,
325 "git_ref": "main",
326 "git_remote": "origin",
327 "muse_branch": "main",
328 "muse_commit_id": fake_cid,
329 "imported_at": "2026-04-14T10:00:00Z",
330 "commits_written": 1,
331 },
332 "last_export": {},
333 })
334
335 result = _invoke(
336 "bridge", "git-status",
337 "--git-dir", str(git_dir),
338 cwd=muse_root,
339 )
340 assert result.exit_code == 0, result.stderr
341 assert "to import" in result.output
342
343 def test_git_status_text_no_drift_without_git_dir(self, tmp_path: pathlib.Path) -> None:
344 """Without --git-dir, text output does NOT show 'Drift:' section."""
345 muse_root = _make_muse_repo(tmp_path / "muse")
346 result = _invoke("bridge", "git-status", cwd=muse_root)
347 assert result.exit_code == 0, result.stderr
348 assert "Drift:" not in result.output
349
350 def test_git_status_text_shows_header(self, tmp_path: pathlib.Path) -> None:
351 """Text output always starts with 'Muse Bridge Status'."""
352 muse_root = _make_muse_repo(tmp_path / "muse")
353 result = _invoke("bridge", "git-status", cwd=muse_root)
354 assert result.exit_code == 0, result.stderr
355 assert "Muse Bridge Status" in result.output
File History 1 commit
sha256:06dba78c2a78e251b580422dd1fd547f3c8357ff18f7709a860873b2d24dbbbf chore: bump version to 0.2.0rc14 Sonnet 4.6 patch 16 hours ago