gabriel / muse public

test_cmd_integration.py file-level

at sha256:c · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:b adding issues docs to bust staging mpack prebuild cache. · gabriel · Jun 20, 2026
1 """Cross-command integration tests for Muse commands.
2
3 These tests chain multiple commands together the way real agent
4 pipelines and scripts would, verifying that the output of one command is
5 correctly consumed by the next and that the whole chain is self-consistent.
6
7 Pipelines tested:
8 - hash-object β†’ cat-object β†’ verify-object (object write/read/integrity)
9 - commit-tree β†’ update-ref β†’ rev-parse (commit creation end-to-end)
10 - pack-objects β†’ unpack-objects round-trip (transport)
11 - snapshot-diff β†’ ls-files cross-check (diff vs. manifest consistency)
12 - show-ref β†’ for-each-ref consistency (ref listing cross-check)
13 - symbolic-ref β†’ rev-parse β†’ read-commit (HEAD dereference chain)
14 - merge-base β†’ snapshot-diff (divergence analysis)
15 - commit-graph β†’ name-rev (graph walk + naming)
16 """
17
18 from __future__ import annotations
19
20 import datetime
21 import json
22 import pathlib
23
24 from tests.cli_test_helper import CliRunner
25 from muse.core.types import Manifest, MsgpackDict, blob_id, fake_id
26
27 cli = None # argparse migration β€” CliRunner ignores this arg
28 from muse.core.object_store import write_object
29 from muse.core.ids import hash_commit, hash_snapshot
30 from muse.core.commits import (
31 CommitRecord,
32 write_commit,
33 )
34 from muse.core.snapshots import (
35 SnapshotRecord,
36 write_snapshot,
37 )
38 from muse.core.paths import muse_dir, ref_path
39
40 runner = CliRunner()
41
42
43 # ---------------------------------------------------------------------------
44 # Shared helpers
45 # ---------------------------------------------------------------------------
46
47
48 def _sha_bytes(data: bytes) -> str:
49 return blob_id(data)
50
51
52 def _init_repo(path: pathlib.Path) -> pathlib.Path:
53 dot_muse = muse_dir(path)
54 (dot_muse / "commits").mkdir(parents=True)
55 (dot_muse / "snapshots").mkdir(parents=True)
56 (dot_muse / "objects").mkdir(parents=True)
57 (dot_muse / "refs" / "heads").mkdir(parents=True)
58 (dot_muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
59 (dot_muse / "repo.json").write_text(
60 json.dumps({"repo_id": "test-repo", "domain": "midi"}), encoding="utf-8"
61 )
62 return path
63
64
65 def _env(repo: pathlib.Path) -> Manifest:
66 return {"MUSE_REPO_ROOT": str(repo)}
67
68
69 def _snap(repo: pathlib.Path, manifest: Manifest | None = None, tag: str = "s") -> str:
70 m = manifest or {}
71 sid = hash_snapshot(m)
72 write_snapshot(
73 repo,
74 SnapshotRecord(
75 snapshot_id=sid,
76 manifest=m,
77 created_at=datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc),
78 ),
79 )
80 return sid
81
82
83 def _commit(
84 repo: pathlib.Path, tag: str, sid: str, branch: str = "main", parent: str | None = None
85 ) -> str:
86 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
87 parent_ids: list[str] = [parent] if parent else []
88 cid = hash_commit( parent_ids=parent_ids,
89 snapshot_id=sid,
90 message=tag,
91 committed_at_iso=committed_at.isoformat(),
92 author="tester",
93 )
94 write_commit(
95 repo,
96 CommitRecord(
97 commit_id=cid,
98 branch=branch,
99 snapshot_id=sid,
100 message=tag,
101 committed_at=committed_at,
102 author="tester",
103 parent_commit_id=parent,
104 ),
105 )
106 ref = ref_path(repo, branch)
107 ref.parent.mkdir(parents=True, exist_ok=True)
108 ref.write_text(cid, encoding="utf-8")
109 return cid
110
111
112 def _obj(repo: pathlib.Path, content: bytes) -> str:
113 oid = _sha_bytes(content)
114 write_object(repo, oid, content)
115 return oid
116
117
118 def _invoke(args: list[str], repo: pathlib.Path, stdin: str | None = None) -> MsgpackDict:
119 if "--json" not in args and "-j" not in args:
120 args = [args[0], "--json"] + args[1:]
121 result = runner.invoke(cli, args, env=_env(repo), input=stdin)
122 assert result.exit_code == 0, f"Command {args!r} failed: {result.output}"
123 parsed = json.loads(result.stdout)
124 assert isinstance(parsed, dict)
125 return parsed
126
127
128 def _invoke_text(args: list[str], repo: pathlib.Path) -> str:
129 result = runner.invoke(cli, args, env=_env(repo))
130 assert result.exit_code == 0, f"Command {args!r} failed: {result.output}"
131 return result.stdout.strip()
132
133
134 # ---------------------------------------------------------------------------
135 # Pipeline 1: hash-object β†’ cat-object β†’ verify-object
136 # ---------------------------------------------------------------------------
137
138
139 class TestHashCatVerifyPipeline:
140 def test_write_then_cat_returns_same_bytes(self, tmp_path: pathlib.Path) -> None:
141 content = b"pipeline test content"
142 f = tmp_path / "src.mid"
143 f.write_bytes(content)
144 repo = _init_repo(tmp_path / "repo")
145
146 # Step 1: hash-object --write
147 ho = _invoke(["hash-object", "--write", str(f)], repo)
148 oid = ho["object_id"]
149 assert ho["stored"] is True
150
151 # Step 2: cat-object --format info β†’ size matches
152 info = _invoke(["cat-object", "--json", oid], repo)
153 assert info["size_bytes"] == len(content)
154 assert info["present"] is True
155
156 # Step 3: verify-object β†’ all_ok
157 vfy = _invoke(["verify-object", oid], repo)
158 assert vfy["all_ok"] is True
159 assert vfy["failed"] == 0
160
161 def test_hash_without_write_not_in_store(self, tmp_path: pathlib.Path) -> None:
162 content = b"no-write"
163 f = tmp_path / "nw.mid"
164 f.write_bytes(content)
165 repo = _init_repo(tmp_path / "repo")
166
167 ho = _invoke(["hash-object", str(f)], repo)
168 oid = ho["object_id"]
169
170 # cat-object with --format info should report present=False
171 result = runner.invoke(
172 cli,
173 ["cat-object", "--json", oid],
174 env=_env(repo),
175 )
176 assert result.exit_code != 0
177 assert json.loads(result.stdout)["present"] is False
178
179
180 # ---------------------------------------------------------------------------
181 # Pipeline 2: commit-tree β†’ update-ref β†’ rev-parse
182 # ---------------------------------------------------------------------------
183
184
185 class TestCommitTreeUpdateRefRevParse:
186 def test_full_commit_creation_pipeline(self, tmp_path: pathlib.Path) -> None:
187 repo = _init_repo(tmp_path)
188 sid = _snap(repo)
189
190 # Step 1: commit-tree
191 ct = _invoke(
192 ["commit-tree", "--snapshot", sid, "--message", "pipeline"],
193 repo,
194 )
195 cid = ct["commit_id"]
196
197 # Step 2: update-ref
198 ur = _invoke(["update-ref", "main", cid], repo)
199 assert ur["commit_id"] == cid
200
201 # Step 3: rev-parse HEAD β†’ should resolve to the same commit
202 rp = _invoke(["rev-parse", "HEAD"], repo)
203 assert rp["commit_id"] == cid
204
205 def test_two_commit_chain_rev_parse_follows_ref(self, tmp_path: pathlib.Path) -> None:
206 repo = _init_repo(tmp_path)
207 sid1 = _snap(repo, tag="s1")
208 sid2 = _snap(repo, tag="s2")
209
210 ct1 = _invoke(["commit-tree", "--snapshot", sid1, "--message", "c1"], repo)
211 cid1 = ct1["commit_id"]
212 _invoke(["update-ref", "main", cid1], repo)
213
214 ct2 = _invoke(
215 ["commit-tree", "--snapshot", sid2, "--message", "c2", "--parent", cid1],
216 repo,
217 )
218 cid2 = ct2["commit_id"]
219 _invoke(["update-ref", "main", cid2], repo)
220
221 rp = _invoke(["rev-parse", "main"], repo)
222 assert rp["commit_id"] == cid2
223
224
225 # ---------------------------------------------------------------------------
226 # Pipeline 3: pack-objects β†’ unpack-objects round-trip
227 # ---------------------------------------------------------------------------
228
229
230 class TestPackUnpackPipeline:
231 def test_all_objects_survive_transport(self, tmp_path: pathlib.Path) -> None:
232 from muse.core.object_store import has_object
233 from muse.core.commits import read_commit
234 from muse.core.snapshots import read_snapshot
235
236 src = _init_repo(tmp_path / "src")
237 dst = _init_repo(tmp_path / "dst")
238
239 content = b"MIDI blob for transport"
240 oid = _obj(src, content)
241 sid = _snap(src, {"track.mid": oid})
242 cid = _commit(src, "transport-test", sid)
243
244 pack_result = runner.invoke(cli, ["pack-objects", cid], env=_env(src))
245 assert pack_result.exit_code == 0
246 bundle_bytes = pack_result.stdout_bytes
247
248 unpack_result = runner.invoke(
249 cli, ["unpack-objects"], input=bundle_bytes, env=_env(dst)
250 )
251 assert unpack_result.exit_code == 0
252
253 assert read_commit(dst, cid) is not None
254 assert read_snapshot(dst, sid) is not None
255 assert has_object(dst, oid)
256
257 def test_pack_then_verify_object_in_dst(self, tmp_path: pathlib.Path) -> None:
258 src = _init_repo(tmp_path / "src")
259 dst = _init_repo(tmp_path / "dst")
260 oid = _obj(src, b"verify after unpack")
261 sid = _snap(src, {"v.mid": oid})
262 cid = _commit(src, "verify-after", sid)
263
264 bundle_bytes = runner.invoke(
265 cli, ["pack-objects", cid], env=_env(src)
266 ).stdout_bytes
267 runner.invoke(cli, ["unpack-objects"], input=bundle_bytes, env=_env(dst))
268
269 vfy = _invoke(["verify-object", oid], dst)
270 assert vfy["all_ok"] is True
271
272
273 # ---------------------------------------------------------------------------
274 # Pipeline 4: snapshot-diff vs. ls-files cross-check
275 # ---------------------------------------------------------------------------
276
277
278 class TestSnapshotDiffLsFilesCrossCheck:
279 def test_added_files_in_diff_appear_in_new_ls_files(self, tmp_path: pathlib.Path) -> None:
280 repo = _init_repo(tmp_path)
281 oid_a = fake_id("obj-a")
282 oid_b = fake_id("obj-b")
283
284 sid1 = _snap(repo, {"a.mid": oid_a}, "s1")
285 sid2 = _snap(repo, {"a.mid": oid_a, "b.mid": oid_b}, "s2")
286 cid1 = _commit(repo, "c1", sid1)
287 cid2 = _commit(repo, "c2", sid2, parent=cid1)
288
289 diff = _invoke(["snapshot-diff", sid1, sid2], repo)
290 added_paths = {e["path"] for e in diff["added"]}
291
292 ls = _invoke(["ls-files", "--commit", cid2], repo)
293 ls_paths = {f["path"] for f in ls["files"]}
294
295 assert added_paths.issubset(ls_paths)
296
297 def test_deleted_files_absent_from_new_ls_files(self, tmp_path: pathlib.Path) -> None:
298 repo = _init_repo(tmp_path)
299 oid = fake_id("obj")
300 sid1 = _snap(repo, {"gone.mid": oid}, "s1")
301 sid2 = _snap(repo, {}, "s2")
302 cid1 = _commit(repo, "d1", sid1)
303 cid2 = _commit(repo, "d2", sid2, parent=cid1)
304
305 diff = _invoke(["snapshot-diff", sid1, sid2], repo)
306 deleted_paths = {e["path"] for e in diff["deleted"]}
307
308 ls = _invoke(["ls-files", "--commit", cid2], repo)
309 ls_paths = {f["path"] for f in ls["files"]}
310
311 assert deleted_paths.isdisjoint(ls_paths)
312
313
314 # ---------------------------------------------------------------------------
315 # Pipeline 5: show-ref ↔ for-each-ref consistency
316 # ---------------------------------------------------------------------------
317
318
319 class TestShowRefForEachRefConsistency:
320 def test_both_commands_report_same_commit_ids(self, tmp_path: pathlib.Path) -> None:
321 repo = _init_repo(tmp_path)
322 sid = _snap(repo)
323 cid_main = _commit(repo, "main-tip", sid, branch="main")
324 cid_dev = _commit(repo, "dev-tip", sid, branch="dev")
325
326 show = _invoke(["show-ref"], repo)
327 show_ids = {r["commit_id"] for r in show["refs"]}
328
329 each = _invoke(["for-each-ref"], repo)
330 each_ids = {r["commit_id"] for r in each["refs"]}
331
332 assert show_ids == each_ids
333
334 def test_both_commands_report_same_branch_count(self, tmp_path: pathlib.Path) -> None:
335 repo = _init_repo(tmp_path)
336 sid = _snap(repo)
337 for branch in ("main", "dev", "feat"):
338 _commit(repo, f"{branch}-tip", sid, branch=branch)
339
340 show = _invoke(["show-ref"], repo)
341 each = _invoke(["for-each-ref"], repo)
342 assert show["count"] == len(each["refs"])
343
344
345 # ---------------------------------------------------------------------------
346 # Pipeline 6: symbolic-ref β†’ rev-parse β†’ read-commit
347 # ---------------------------------------------------------------------------
348
349
350 class TestSymbolicRefRevParseReadCommit:
351 def test_symbolic_ref_branch_matches_rev_parse_commit(self, tmp_path: pathlib.Path) -> None:
352 repo = _init_repo(tmp_path)
353 sid = _snap(repo)
354 cid = _commit(repo, "head-chain", sid)
355
356 sym = _invoke(["symbolic-ref"], repo)
357 branch = sym["branch"]
358
359 rp = _invoke(["rev-parse", branch], repo)
360 assert rp["commit_id"] == cid
361
362 rc = _invoke(["read-commit", cid], repo)
363 assert rc["branch"] == branch
364
365 def test_set_and_read_symbolic_ref_consistent(self, tmp_path: pathlib.Path) -> None:
366 repo = _init_repo(tmp_path)
367 sid = _snap(repo)
368 _commit(repo, "main-c", sid, branch="main")
369 dev_cid = _commit(repo, "dev-c", sid, branch="dev")
370
371 # Switch HEAD to dev
372 result = runner.invoke(
373 cli, ["symbolic-ref", "--set", "dev"], env=_env(repo)
374 )
375 assert result.exit_code == 0
376
377 sym = _invoke(["symbolic-ref"], repo)
378 assert sym["branch"] == "dev"
379
380 rp = _invoke(["rev-parse", "HEAD"], repo)
381 assert rp["commit_id"] == dev_cid
382
383
384 # ---------------------------------------------------------------------------
385 # Pipeline 7: merge-base β†’ snapshot-diff (divergence analysis)
386 # ---------------------------------------------------------------------------
387
388
389 class TestMergeBaseSnapshotDiff:
390 def test_diff_between_branches_using_merge_base(self, tmp_path: pathlib.Path) -> None:
391 repo = _init_repo(tmp_path)
392 oid_common = fake_id("common")
393 oid_main = fake_id("main-only")
394 oid_feat = fake_id("feat-only")
395
396 sid_base = _snap(repo, {"common.mid": oid_common}, "base")
397 sid_main = _snap(repo, {"common.mid": oid_common, "main.mid": oid_main}, "main")
398 sid_feat = _snap(repo, {"common.mid": oid_common, "feat.mid": oid_feat}, "feat")
399
400 c_base = _commit(repo, "base-commit", sid_base)
401 c_main = _commit(repo, "main-commit", sid_main, branch="main", parent=c_base)
402 c_feat = _commit(repo, "feat-commit", sid_feat, branch="feat", parent=c_base)
403
404 mb = _invoke(["merge-base", "main", "feat"], repo)
405 base_cid = mb["merge_base"]
406 assert base_cid == c_base
407
408 # Snapshot of the merge base
409 rc_base = _invoke(["read-commit", base_cid], repo)
410 sid_at_base = rc_base["snapshot_id"]
411
412 # Diff main's snapshot vs. base β€” should show main.mid as added
413 diff_main = _invoke(["snapshot-diff", str(sid_at_base), str(sid_main)], repo)
414 added = {e["path"] for e in diff_main["added"]}
415 assert "main.mid" in added
416
417
418 # ---------------------------------------------------------------------------
419 # Pipeline 8: commit-graph β†’ name-rev
420 # ---------------------------------------------------------------------------
421
422
423 class TestCommitGraphNameRev:
424 def test_graph_tip_named_branch_tilde_zero(self, tmp_path: pathlib.Path) -> None:
425 repo = _init_repo(tmp_path)
426 sid = _snap(repo)
427 c0 = _commit(repo, "c0", sid)
428 c1 = _commit(repo, "c1", sid, parent=c0)
429 c2 = _commit(repo, "c2", sid, parent=c1)
430
431 graph = _invoke(["commit-graph"], repo)
432 tip = graph["tip"]
433
434 nr = _invoke(["name-rev", tip], repo)
435 named = nr["results"][0]
436 assert named["commit_id"] == tip
437 # Tip commit: distance=0, name is just the branch name (no ~0 suffix).
438 assert named["name"] == "main"
439
440 def test_all_graph_commits_nameable(self, tmp_path: pathlib.Path) -> None:
441 repo = _init_repo(tmp_path)
442 sid = _snap(repo)
443 parent: str | None = None
444 cids: list[str] = []
445 for i in range(5):
446 cid = _commit(repo, f"chain-{i}", sid, parent=parent)
447 cids.append(cid)
448 parent = cid
449
450 graph = _invoke(["commit-graph"], repo)
451 graph_ids = [c["commit_id"] for c in graph["commits"]]
452
453 nr = _invoke(["name-rev", *graph_ids], repo)
454 for entry in nr["results"]:
455 assert not entry["undefined"], f"Commit {entry['commit_id']} is undefined"