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