gabriel / muse public
test_plumbing_commit_graph.py python
302 lines 10.6 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d ago
1 """Tests for ``muse plumbing commit-graph`` (base functionality).
2
3 Covers: BFS traversal from HEAD, explicit ``--tip``, ``--stop-at`` pruning,
4 ``--max`` truncation, text format (one ID per line), empty-branch error,
5 unknown tip error, truncated flag, and a stress case with 200 commits.
6
7 Enhancement flags (--count, --first-parent, --ancestry-path) are tested in
8 ``tests/test_plumbing_commit_graph_enhancements.py``.
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import hashlib
15 import json
16 import pathlib
17
18 from tests.cli_test_helper import CliRunner
19
20 cli = None # argparse migration — CliRunner ignores this arg
21 from muse.core.errors import ExitCode
22 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
23
24 runner = CliRunner()
25
26
27 # ---------------------------------------------------------------------------
28 # Helpers
29 # ---------------------------------------------------------------------------
30
31
32 def _sha(tag: str) -> str:
33 return hashlib.sha256(tag.encode()).hexdigest()
34
35
36 def _init_repo(path: pathlib.Path) -> pathlib.Path:
37 muse = path / ".muse"
38 (muse / "commits").mkdir(parents=True)
39 (muse / "snapshots").mkdir(parents=True)
40 (muse / "objects").mkdir(parents=True)
41 (muse / "refs" / "heads").mkdir(parents=True)
42 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
43 (muse / "repo.json").write_text(
44 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
45 )
46 return path
47
48
49 def _env(repo: pathlib.Path) -> dict[str, str]:
50 return {"MUSE_REPO_ROOT": str(repo)}
51
52
53 def _snap(repo: pathlib.Path) -> str:
54 sid = _sha("snap")
55 write_snapshot(
56 repo,
57 SnapshotRecord(
58 snapshot_id=sid,
59 manifest={},
60 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
61 ),
62 )
63 return sid
64
65
66 def _commit(
67 repo: pathlib.Path, tag: str, sid: str, branch: str = "main", parent: str | None = None
68 ) -> str:
69 cid = _sha(tag)
70 write_commit(
71 repo,
72 CommitRecord(
73 commit_id=cid,
74 repo_id="test-repo",
75 branch=branch,
76 snapshot_id=sid,
77 message=tag,
78 committed_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
79 author="tester",
80 parent_commit_id=parent,
81 ),
82 )
83 ref = repo / ".muse" / "refs" / "heads" / branch
84 ref.parent.mkdir(parents=True, exist_ok=True)
85 ref.write_text(cid, encoding="utf-8")
86 return cid
87
88
89 def _linear_chain(repo: pathlib.Path, length: int) -> list[str]:
90 """Return list of commit IDs from root (index 0) to tip (index length-1)."""
91 sid = _snap(repo)
92 cids: list[str] = []
93 parent: str | None = None
94 for i in range(length):
95 cid = _commit(repo, f"c{i}", sid, parent=parent)
96 cids.append(cid)
97 parent = cid
98 return cids
99
100
101 # ---------------------------------------------------------------------------
102 # Unit: empty branch and bad tip
103 # ---------------------------------------------------------------------------
104
105
106 class TestCommitGraphUnit:
107 def test_empty_branch_exits_user_error(self, tmp_path: pathlib.Path) -> None:
108 repo = _init_repo(tmp_path)
109 result = runner.invoke(cli, ["plumbing", "commit-graph"], env=_env(repo))
110 assert result.exit_code == ExitCode.USER_ERROR
111 assert "error" in json.loads(result.stdout)
112
113 def test_unknown_tip_exits_user_error(self, tmp_path: pathlib.Path) -> None:
114 repo = _init_repo(tmp_path)
115 ghost = _sha("ghost")
116 result = runner.invoke(
117 cli, ["plumbing", "commit-graph", "--tip", ghost], env=_env(repo)
118 )
119 assert result.exit_code == ExitCode.USER_ERROR
120
121 def test_bad_format_exits_user_error(self, tmp_path: pathlib.Path) -> None:
122 repo = _init_repo(tmp_path)
123 sid = _snap(repo)
124 cid = _commit(repo, "c", sid)
125 result = runner.invoke(
126 cli, ["plumbing", "commit-graph", "--tip", cid, "--format", "toml"], env=_env(repo)
127 )
128 assert result.exit_code == ExitCode.USER_ERROR
129
130
131 # ---------------------------------------------------------------------------
132 # Integration: BFS traversal
133 # ---------------------------------------------------------------------------
134
135
136 class TestCommitGraphBFS:
137 def test_single_commit_graph(self, tmp_path: pathlib.Path) -> None:
138 repo = _init_repo(tmp_path)
139 sid = _snap(repo)
140 cid = _commit(repo, "solo", sid)
141 result = runner.invoke(
142 cli, ["plumbing", "commit-graph", "--tip", cid], env=_env(repo)
143 )
144 assert result.exit_code == 0, result.output
145 data = json.loads(result.stdout)
146 assert data["count"] == 1
147 assert data["commits"][0]["commit_id"] == cid
148 assert data["tip"] == cid
149
150 def test_linear_chain_all_commits_traversed(self, tmp_path: pathlib.Path) -> None:
151 repo = _init_repo(tmp_path)
152 cids = _linear_chain(repo, 5)
153 tip = cids[-1]
154 result = runner.invoke(
155 cli, ["plumbing", "commit-graph", "--tip", tip], env=_env(repo)
156 )
157 assert result.exit_code == 0
158 data = json.loads(result.stdout)
159 assert data["count"] == 5
160 found = {c["commit_id"] for c in data["commits"]}
161 assert found == set(cids)
162
163 def test_head_default_traverses_from_current_branch(self, tmp_path: pathlib.Path) -> None:
164 repo = _init_repo(tmp_path)
165 cids = _linear_chain(repo, 3)
166 result = runner.invoke(cli, ["plumbing", "commit-graph"], env=_env(repo))
167 assert result.exit_code == 0
168 data = json.loads(result.stdout)
169 assert data["count"] == 3
170
171 def test_commit_node_has_required_fields(self, tmp_path: pathlib.Path) -> None:
172 repo = _init_repo(tmp_path)
173 sid = _snap(repo)
174 cid = _commit(repo, "fields-test", sid)
175 result = runner.invoke(
176 cli, ["plumbing", "commit-graph", "--tip", cid], env=_env(repo)
177 )
178 assert result.exit_code == 0
179 node = json.loads(result.stdout)["commits"][0]
180 for field in (
181 "commit_id", "parent_commit_id", "parent2_commit_id",
182 "message", "branch", "committed_at", "snapshot_id", "author",
183 ):
184 assert field in node, f"Missing field: {field}"
185
186
187 # ---------------------------------------------------------------------------
188 # Integration: --stop-at pruning
189 # ---------------------------------------------------------------------------
190
191
192 class TestCommitGraphStopAt:
193 def test_stop_at_excludes_ancestor_commits(self, tmp_path: pathlib.Path) -> None:
194 repo = _init_repo(tmp_path)
195 cids = _linear_chain(repo, 5)
196 # cids = [c0, c1, c2, c3, c4]; stop at c2 — should see c4, c3 only.
197 result = runner.invoke(
198 cli,
199 ["plumbing", "commit-graph", "--tip", cids[4], "--stop-at", cids[2]],
200 env=_env(repo),
201 )
202 assert result.exit_code == 0
203 data = json.loads(result.stdout)
204 found = {c["commit_id"] for c in data["commits"]}
205 assert cids[4] in found
206 assert cids[3] in found
207 assert cids[2] not in found
208 assert cids[1] not in found
209
210 def test_stop_at_tip_yields_no_commits(self, tmp_path: pathlib.Path) -> None:
211 repo = _init_repo(tmp_path)
212 cids = _linear_chain(repo, 3)
213 result = runner.invoke(
214 cli,
215 ["plumbing", "commit-graph", "--tip", cids[2], "--stop-at", cids[2]],
216 env=_env(repo),
217 )
218 assert result.exit_code == 0
219 data = json.loads(result.stdout)
220 assert data["count"] == 0
221
222
223 # ---------------------------------------------------------------------------
224 # Integration: --max truncation
225 # ---------------------------------------------------------------------------
226
227
228 class TestCommitGraphMax:
229 def test_max_limits_traversal(self, tmp_path: pathlib.Path) -> None:
230 repo = _init_repo(tmp_path)
231 _linear_chain(repo, 10)
232 result = runner.invoke(
233 cli, ["plumbing", "commit-graph", "--max", "3"], env=_env(repo)
234 )
235 assert result.exit_code == 0
236 data = json.loads(result.stdout)
237 assert data["count"] == 3
238 assert data["truncated"] is True
239
240 def test_truncated_false_when_all_fit(self, tmp_path: pathlib.Path) -> None:
241 repo = _init_repo(tmp_path)
242 _linear_chain(repo, 5)
243 result = runner.invoke(
244 cli, ["plumbing", "commit-graph", "--max", "100"], env=_env(repo)
245 )
246 assert result.exit_code == 0
247 assert json.loads(result.stdout)["truncated"] is False
248
249
250 # ---------------------------------------------------------------------------
251 # Integration: text format
252 # ---------------------------------------------------------------------------
253
254
255 class TestCommitGraphTextFormat:
256 def test_text_format_emits_one_id_per_line(self, tmp_path: pathlib.Path) -> None:
257 repo = _init_repo(tmp_path)
258 cids = _linear_chain(repo, 4)
259 result = runner.invoke(
260 cli, ["plumbing", "commit-graph", "--format", "text"], env=_env(repo)
261 )
262 assert result.exit_code == 0
263 lines = [ln for ln in result.stdout.splitlines() if ln.strip()]
264 assert len(lines) == 4
265 assert all(len(ln) == 64 for ln in lines)
266
267 def test_short_format_flag(self, tmp_path: pathlib.Path) -> None:
268 repo = _init_repo(tmp_path)
269 _linear_chain(repo, 2)
270 result = runner.invoke(
271 cli, ["plumbing", "commit-graph", "-f", "text"], env=_env(repo)
272 )
273 assert result.exit_code == 0
274
275
276 # ---------------------------------------------------------------------------
277 # Stress: 200-commit linear history
278 # ---------------------------------------------------------------------------
279
280
281 class TestCommitGraphStress:
282 def test_200_commit_chain_fully_traversed(self, tmp_path: pathlib.Path) -> None:
283 repo = _init_repo(tmp_path)
284 cids = _linear_chain(repo, 200)
285 result = runner.invoke(cli, ["plumbing", "commit-graph"], env=_env(repo))
286 assert result.exit_code == 0
287 data = json.loads(result.stdout)
288 assert data["count"] == 200
289 assert data["truncated"] is False
290
291 def test_200_commit_chain_stop_at_midpoint(self, tmp_path: pathlib.Path) -> None:
292 repo = _init_repo(tmp_path)
293 cids = _linear_chain(repo, 200)
294 result = runner.invoke(
295 cli,
296 ["plumbing", "commit-graph", "--tip", cids[199], "--stop-at", cids[99]],
297 env=_env(repo),
298 )
299 assert result.exit_code == 0
300 data = json.loads(result.stdout)
301 # commits 100..199 (100 commits total)
302 assert data["count"] == 100