gabriel / muse public
test_plumbing_commit_graph.py python
302 lines 10.5 KB
99746394 feat(tests+docs): supercharge plumbing test suite and update reference doc Gabriel Cardona <gabriel@tellurstori.com> 2d 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 typer.testing import CliRunner
19
20 from muse.cli.app import cli
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