gabriel / muse public
test_bundle_supercharge.py python
1,043 lines 42.0 KB
Raw
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 148 days ago
1 """Supercharged tests for ``muse bundle`` — three new agent-first features.
2
3 Feature 1 — ``muse bundle inspect <file> [--json]``
4 -----------------------------------------------------
5 Read and display the commit log and branch state from a bundle file without
6 unbundling. No repository required. Agents use this to decide whether to
7 apply a bundle before committing to the operation.
8
9 JSON schema::
10
11 {
12 "total_commits": int,
13 "total_objects": int,
14 "branches": {"<name>": "<commit_id>"},
15 "commits": [
16 {
17 "commit_id": str,
18 "message": str,
19 "committed_at": str, # ISO-8601
20 "agent_id": str, # "" when not an agent commit
21 "branches": [str] # branch names whose head == this commit
22 },
23 ... # newest first (by committed_at)
24 ]
25 }
26
27 Feature 2 — ``--verify`` flag on ``muse bundle unbundle``
28 ----------------------------------------------------------
29 Verify bundle integrity atomically before applying. Exits 1 (with no
30 writes) if the bundle is corrupt. JSON output gains a ``"verified"`` bool.
31
32 Feature 3 — ``muse bundle diff <file> [--json]``
33 -------------------------------------------------
34 Show which commits in the bundle are not already present in the local
35 repository. Agents use this to answer "what would this bundle add?" before
36 deciding to apply.
37
38 JSON schema::
39
40 {
41 "new_commits": int,
42 "known_commits": int,
43 "refs_to_advance": [str], # branch names that would move
44 "commits": [
45 {"commit_id": str, "message": str, "committed_at": str}
46 ]
47 }
48
49 Test categories
50 ---------------
51 - unit : internal helpers and schema shapes
52 - integration : CLI flag parsing and output contracts
53 - e2e : full round-trips via CliRunner
54 - security : ANSI/control injection in bundle content
55 - data_integrity: inspect/diff remain consistent across create-verify-unbundle
56 - performance : inspect and diff on 100-commit bundles under 1 s
57 - stress : inspect and diff on 200-commit bundles
58 """
59
60 from __future__ import annotations
61
62 import datetime
63 import hashlib
64 import json
65 import os
66 import pathlib
67 import time
68 import threading
69
70 import msgpack
71 import pytest
72
73 from tests.cli_test_helper import CliRunner, InvokeResult
74 from muse.core.object_store import write_object
75 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
76 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
77 from muse.core._types import Manifest, long_id
78
79 runner = CliRunner()
80 _REPO_ID = "bundle-supercharged-test"
81
82
83 # ---------------------------------------------------------------------------
84 # Helpers
85 # ---------------------------------------------------------------------------
86
87
88 def _sha(data: bytes) -> str:
89 return hashlib.sha256(data).hexdigest()
90
91
92 def _init_repo(path: pathlib.Path, repo_id: str = _REPO_ID) -> pathlib.Path:
93 muse = path / ".muse"
94 for d in ("commits", "snapshots", "objects", "refs/heads"):
95 (muse / d).mkdir(parents=True, exist_ok=True)
96 (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8")
97 (muse / "repo.json").write_text(
98 json.dumps({"repo_id": repo_id, "domain": "midi"}), encoding="utf-8"
99 )
100 return path
101
102
103 def _env(repo: pathlib.Path) -> Manifest:
104 return {"MUSE_REPO_ROOT": str(repo)}
105
106
107 _counter = 0
108
109
110 def _make_commit(
111 root: pathlib.Path,
112 parent_id: str | None = None,
113 content: bytes = b"data",
114 branch: str = "main",
115 message: str | None = None,
116 agent_id: str = "",
117 ) -> str:
118 global _counter
119 _counter += 1
120 c = content + str(_counter).encode()
121 obj_id = long_id(_sha(c))
122 write_object(root, obj_id, c)
123 manifest = {f"f_{_counter}.txt": obj_id}
124 snap_id = compute_snapshot_id(manifest)
125 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
126 committed_at = datetime.datetime.now(datetime.timezone.utc)
127 parent_ids = [parent_id] if parent_id else []
128 msg = message or f"commit {_counter}"
129 commit_id = compute_commit_id(parent_ids, snap_id, msg, committed_at.isoformat())
130 write_commit(root, CommitRecord(
131 commit_id=commit_id,
132 repo_id=_REPO_ID,
133 branch=branch,
134 snapshot_id=snap_id,
135 message=msg,
136 committed_at=committed_at,
137 parent_commit_id=parent_id,
138 agent_id=agent_id,
139 ))
140 ref_dir = root / ".muse" / "refs" / "heads"
141 if "/" in branch:
142 (ref_dir / branch).parent.mkdir(parents=True, exist_ok=True)
143 (ref_dir / branch).write_text(commit_id, encoding="utf-8")
144 return commit_id
145
146
147 def _invoke(args: list[str], env: Manifest | None = None) -> InvokeResult:
148 return runner.invoke(None, args, env=env)
149
150
151 def _create_bundle(
152 repo: pathlib.Path, out: pathlib.Path, *extra_args: str
153 ) -> InvokeResult:
154 return _invoke(["bundle", "create", str(out), *extra_args], env=_env(repo))
155
156
157 def _parse_inspect(result: InvokeResult) -> dict:
158 return json.loads(result.output)
159
160
161 def _parse_diff(result: InvokeResult) -> dict:
162 return json.loads(result.output)
163
164
165 # ===========================================================================
166 # Feature 1: muse bundle inspect
167 # ===========================================================================
168
169
170 class TestBundleInspectUnit:
171 """Unit-level schema and output contracts for bundle inspect."""
172
173 def test_inspect_help_exits_0(self) -> None:
174 result = _invoke(["bundle", "inspect", "--help"])
175 assert result.exit_code == 0
176
177 def test_inspect_help_mentions_agent(self) -> None:
178 result = _invoke(["bundle", "inspect", "--help"])
179 assert "agent" in result.output.lower() or "Agent" in result.output
180
181 def test_inspect_help_mentions_json_schema(self) -> None:
182 result = _invoke(["bundle", "inspect", "--help"])
183 assert "JSON" in result.output
184
185 def test_inspect_json_schema_keys(self, tmp_path: pathlib.Path) -> None:
186 _init_repo(tmp_path)
187 _make_commit(tmp_path, content=b"inspect-schema")
188 bundle = tmp_path / "schema.bundle"
189 _create_bundle(tmp_path, bundle)
190 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
191 assert result.exit_code == 0
192 data = _parse_inspect(result)
193 for key in ("total_commits", "total_objects", "branches", "commits"):
194 assert key in data, f"missing key: {key}"
195
196 def test_inspect_commit_entry_schema(self, tmp_path: pathlib.Path) -> None:
197 _init_repo(tmp_path)
198 _make_commit(tmp_path, content=b"inspect-entry")
199 bundle = tmp_path / "entry.bundle"
200 _create_bundle(tmp_path, bundle)
201 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
202 assert result.exit_code == 0
203 data = _parse_inspect(result)
204 assert len(data["commits"]) >= 1
205 entry = data["commits"][0]
206 for key in ("commit_id", "message", "committed_at", "agent_id", "branches"):
207 assert key in entry, f"commit entry missing key: {key}"
208
209 def test_inspect_total_commits_count(self, tmp_path: pathlib.Path) -> None:
210 _init_repo(tmp_path)
211 prev = None
212 for i in range(5):
213 prev = _make_commit(tmp_path, parent_id=prev, content=f"cnt-{i}".encode())
214 bundle = tmp_path / "cnt.bundle"
215 _create_bundle(tmp_path, bundle)
216 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
217 data = _parse_inspect(result)
218 assert data["total_commits"] == 5
219
220 def test_inspect_total_objects_positive(self, tmp_path: pathlib.Path) -> None:
221 _init_repo(tmp_path)
222 _make_commit(tmp_path, content=b"obj-count")
223 bundle = tmp_path / "objcnt.bundle"
224 _create_bundle(tmp_path, bundle)
225 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
226 data = _parse_inspect(result)
227 assert data["total_objects"] > 0
228
229 def test_inspect_branches_map(self, tmp_path: pathlib.Path) -> None:
230 _init_repo(tmp_path)
231 cid = _make_commit(tmp_path, content=b"branches-map")
232 bundle = tmp_path / "bmap.bundle"
233 _create_bundle(tmp_path, bundle)
234 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
235 data = _parse_inspect(result)
236 assert "main" in data["branches"]
237
238 def test_inspect_commit_message_present(self, tmp_path: pathlib.Path) -> None:
239 _init_repo(tmp_path)
240 _make_commit(tmp_path, content=b"msg-check", message="feat: add audio engine")
241 bundle = tmp_path / "msg.bundle"
242 _create_bundle(tmp_path, bundle)
243 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
244 data = _parse_inspect(result)
245 messages = [c["message"] for c in data["commits"]]
246 assert any("feat: add audio engine" in m for m in messages)
247
248 def test_inspect_agent_id_from_agent_commit(self, tmp_path: pathlib.Path) -> None:
249 _init_repo(tmp_path)
250 _make_commit(tmp_path, content=b"agent-commit", agent_id="claude-code")
251 bundle = tmp_path / "agent.bundle"
252 _create_bundle(tmp_path, bundle)
253 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
254 data = _parse_inspect(result)
255 agent_ids = [c["agent_id"] for c in data["commits"]]
256 assert "claude-code" in agent_ids
257
258 def test_inspect_agent_id_empty_for_human_commit(self, tmp_path: pathlib.Path) -> None:
259 _init_repo(tmp_path)
260 _make_commit(tmp_path, content=b"human-commit", agent_id="")
261 bundle = tmp_path / "human.bundle"
262 _create_bundle(tmp_path, bundle)
263 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
264 data = _parse_inspect(result)
265 # Human commits have empty or None agent_id
266 assert data["commits"][0]["agent_id"] in ("", None)
267
268 def test_inspect_commits_newest_first(self, tmp_path: pathlib.Path) -> None:
269 """Commits must be ordered newest first (by committed_at)."""
270 _init_repo(tmp_path)
271 prev = None
272 for i in range(3):
273 prev = _make_commit(tmp_path, parent_id=prev, content=f"ord-{i}".encode())
274 bundle = tmp_path / "ord.bundle"
275 _create_bundle(tmp_path, bundle)
276 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
277 data = _parse_inspect(result)
278 dates = [c["committed_at"] for c in data["commits"]]
279 assert dates == sorted(dates, reverse=True)
280
281 def test_inspect_branch_annotated_on_tip_commit(self, tmp_path: pathlib.Path) -> None:
282 """The commit that is a branch head should have that branch in its branches list."""
283 _init_repo(tmp_path)
284 cid = _make_commit(tmp_path, content=b"tip-commit")
285 bundle = tmp_path / "tip.bundle"
286 _create_bundle(tmp_path, bundle)
287 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
288 data = _parse_inspect(result)
289 tip_entry = next(c for c in data["commits"] if c["commit_id"] == cid)
290 assert "main" in tip_entry["branches"]
291
292 def test_inspect_non_tip_commit_has_no_branch(self, tmp_path: pathlib.Path) -> None:
293 """Commits that are not at the tip of any branch have empty branches list."""
294 _init_repo(tmp_path)
295 c1 = _make_commit(tmp_path, content=b"non-tip-parent")
296 _make_commit(tmp_path, parent_id=c1, content=b"non-tip-child")
297 bundle = tmp_path / "nontip.bundle"
298 _create_bundle(tmp_path, bundle)
299 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
300 data = _parse_inspect(result)
301 parent_entry = next(c for c in data["commits"] if c["commit_id"] == c1)
302 assert parent_entry["branches"] == []
303
304 def test_inspect_does_not_require_repo(self, tmp_path: pathlib.Path) -> None:
305 """inspect must work without MUSE_REPO_ROOT (no repo needed)."""
306 src = tmp_path / "src"
307 src.mkdir()
308 _init_repo(src)
309 _make_commit(src, content=b"no-repo-needed")
310 bundle = tmp_path / "norepo.bundle"
311 _create_bundle(src, bundle)
312 # Invoke with no env — no repo context at all
313 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
314 assert result.exit_code == 0
315
316 def test_inspect_file_not_found(self, tmp_path: pathlib.Path) -> None:
317 result = _invoke(["bundle", "inspect", str(tmp_path / "missing.bundle"), "--json"])
318 assert result.exit_code != 0
319
320 def test_inspect_invalid_msgpack(self, tmp_path: pathlib.Path) -> None:
321 bad = tmp_path / "bad.bundle"
322 bad.write_bytes(b"not msgpack")
323 result = _invoke(["bundle", "inspect", str(bad), "--json"])
324 assert result.exit_code != 0
325
326 def test_inspect_empty_bundle(self, tmp_path: pathlib.Path) -> None:
327 empty = tmp_path / "empty.bundle"
328 empty.write_bytes(msgpack.packb({}, use_bin_type=True))
329 result = _invoke(["bundle", "inspect", str(empty), "--json"])
330 assert result.exit_code == 0
331 data = _parse_inspect(result)
332 assert data["total_commits"] == 0
333 assert data["commits"] == []
334 assert data["branches"] == {}
335
336 def test_inspect_j_alias(self, tmp_path: pathlib.Path) -> None:
337 _init_repo(tmp_path)
338 _make_commit(tmp_path, content=b"j-alias")
339 bundle = tmp_path / "jalias.bundle"
340 _create_bundle(tmp_path, bundle)
341 r1 = _invoke(["bundle", "inspect", str(bundle), "--json"])
342 r2 = _invoke(["bundle", "inspect", str(bundle), "-j"])
343 assert r1.exit_code == 0
344 assert r2.exit_code == 0
345 assert json.loads(r1.output)["total_commits"] == json.loads(r2.output)["total_commits"]
346
347
348 class TestBundleInspectText:
349 """Text output (no --json) contracts for bundle inspect."""
350
351 def test_text_output_mentions_commits(self, tmp_path: pathlib.Path) -> None:
352 _init_repo(tmp_path)
353 _make_commit(tmp_path, content=b"txt-commits")
354 bundle = tmp_path / "txt.bundle"
355 _create_bundle(tmp_path, bundle)
356 result = _invoke(["bundle", "inspect", str(bundle)])
357 assert result.exit_code == 0
358 assert "commit" in result.output.lower()
359
360 def test_text_output_mentions_branch(self, tmp_path: pathlib.Path) -> None:
361 _init_repo(tmp_path)
362 _make_commit(tmp_path, content=b"txt-branch")
363 bundle = tmp_path / "txt-br.bundle"
364 _create_bundle(tmp_path, bundle)
365 result = _invoke(["bundle", "inspect", str(bundle)])
366 assert result.exit_code == 0
367 assert "main" in result.output
368
369 def test_text_output_includes_commit_message(self, tmp_path: pathlib.Path) -> None:
370 _init_repo(tmp_path)
371 _make_commit(tmp_path, content=b"txt-msg", message="feat: melody engine")
372 bundle = tmp_path / "txt-msg.bundle"
373 _create_bundle(tmp_path, bundle)
374 result = _invoke(["bundle", "inspect", str(bundle)])
375 assert "feat: melody engine" in result.output
376
377
378 class TestBundleInspectSecurity:
379 """Security: ANSI and control injection from crafted bundle content."""
380
381 def _has_ansi(self, s: str) -> bool:
382 return "\x1b[" in s
383
384 def test_ansi_in_commit_message_stripped(self, tmp_path: pathlib.Path) -> None:
385 _init_repo(tmp_path)
386 _make_commit(tmp_path, content=b"ansi-msg", message="\x1b[31mevil\x1b[0m")
387 bundle = tmp_path / "ansi-msg.bundle"
388 _create_bundle(tmp_path, bundle)
389 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
390 assert result.exit_code == 0
391 data = _parse_inspect(result)
392 for c in data["commits"]:
393 assert not self._has_ansi(c["message"]), "ANSI in message not stripped"
394
395 def test_ansi_in_branch_name_stripped(self, tmp_path: pathlib.Path) -> None:
396 """A crafted bundle with ANSI in a branch_heads key must not reach stdout."""
397 _init_repo(tmp_path)
398 cid = _make_commit(tmp_path, content=b"ansi-branch")
399 bundle = tmp_path / "ansi-br.bundle"
400 _create_bundle(tmp_path, bundle)
401 # Inject ANSI into branch_heads in the msgpack
402 raw = msgpack.unpackb(bundle.read_bytes(), raw=False)
403 raw["branch_heads"] = {"\x1b[31mevil\x1b[0m": cid}
404 bundle.write_bytes(msgpack.packb(raw, use_bin_type=True))
405 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
406 assert result.exit_code == 0
407 assert not self._has_ansi(result.output)
408
409 def test_ansi_in_agent_id_stripped(self, tmp_path: pathlib.Path) -> None:
410 _init_repo(tmp_path)
411 _make_commit(tmp_path, content=b"ansi-agent", agent_id="\x1b[31mhacked\x1b[0m")
412 bundle = tmp_path / "ansi-agent.bundle"
413 _create_bundle(tmp_path, bundle)
414 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
415 assert result.exit_code == 0
416 assert not self._has_ansi(result.output)
417
418 def test_oversized_bundle_rejected(self, tmp_path: pathlib.Path) -> None:
419 """Bundle larger than the safety cap must be rejected."""
420 from muse.core.store import MAX_PACK_MSGPACK_BYTES
421 oversized = tmp_path / "oversized.bundle"
422 oversized.write_bytes(b"\x00" * (MAX_PACK_MSGPACK_BYTES + 1))
423 result = _invoke(["bundle", "inspect", str(oversized), "--json"])
424 assert result.exit_code != 0
425
426
427 class TestBundleInspectDataIntegrity:
428 """Data integrity: inspect output is consistent with create and unbundle."""
429
430 def test_inspect_commit_ids_match_create_json(self, tmp_path: pathlib.Path) -> None:
431 """Commits listed by inspect must equal those packed by create."""
432 _init_repo(tmp_path)
433 prev = None
434 cids = []
435 for i in range(4):
436 prev = _make_commit(tmp_path, parent_id=prev, content=f"di-{i}".encode())
437 cids.append(prev)
438 bundle = tmp_path / "di.bundle"
439 _create_bundle(tmp_path, bundle)
440 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
441 data = _parse_inspect(result)
442 inspect_ids = {c["commit_id"] for c in data["commits"]}
443 for cid in cids:
444 assert cid in inspect_ids
445
446 def test_inspect_consistent_with_verify(self, tmp_path: pathlib.Path) -> None:
447 """A bundle that verify says is clean must also inspect cleanly."""
448 _init_repo(tmp_path)
449 prev = None
450 for i in range(3):
451 prev = _make_commit(tmp_path, parent_id=prev, content=f"vdi-{i}".encode())
452 bundle = tmp_path / "vdi.bundle"
453 _create_bundle(tmp_path, bundle)
454 v = _invoke(["bundle", "verify", str(bundle), "--json"])
455 assert json.loads(v.output)["all_ok"] is True
456 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
457 assert result.exit_code == 0
458 data = _parse_inspect(result)
459 assert data["total_commits"] == 3
460
461 def test_inspect_branch_commit_id_matches_list_heads(self, tmp_path: pathlib.Path) -> None:
462 """branches map in inspect must match list-heads output."""
463 _init_repo(tmp_path)
464 _make_commit(tmp_path, content=b"lh-match")
465 bundle = tmp_path / "lh.bundle"
466 _create_bundle(tmp_path, bundle)
467 lh = json.loads(
468 _invoke(["bundle", "list-heads", str(bundle), "--json"]).output
469 )
470 ins = _parse_inspect(
471 _invoke(["bundle", "inspect", str(bundle), "--json"])
472 )
473 assert ins["branches"] == lh
474
475
476 class TestBundleInspectPerformance:
477 def test_inspect_100_commit_bundle_under_1s(self, tmp_path: pathlib.Path) -> None:
478 _init_repo(tmp_path)
479 prev = None
480 for i in range(100):
481 prev = _make_commit(tmp_path, parent_id=prev, content=f"perf-{i}".encode())
482 bundle = tmp_path / "perf100.bundle"
483 _create_bundle(tmp_path, bundle)
484 start = time.monotonic()
485 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
486 elapsed = time.monotonic() - start
487 assert result.exit_code == 0
488 data = _parse_inspect(result)
489 assert data["total_commits"] == 100
490 assert elapsed < 1.0, f"inspect 100-commit bundle took {elapsed:.2f}s"
491
492
493 class TestBundleInspectStress:
494 def test_inspect_200_commit_bundle(self, tmp_path: pathlib.Path) -> None:
495 _init_repo(tmp_path)
496 prev = None
497 for i in range(200):
498 prev = _make_commit(tmp_path, parent_id=prev, content=f"s200-{i}".encode())
499 bundle = tmp_path / "s200.bundle"
500 _create_bundle(tmp_path, bundle)
501 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
502 assert result.exit_code == 0
503 data = _parse_inspect(result)
504 assert data["total_commits"] == 200
505
506 def test_inspect_multi_branch_bundle(self, tmp_path: pathlib.Path) -> None:
507 _init_repo(tmp_path)
508 base = _make_commit(tmp_path, content=b"multi-base")
509 for i in range(10):
510 br = f"feat/branch-{i}"
511 ref = tmp_path / ".muse" / "refs" / "heads" / br
512 ref.parent.mkdir(parents=True, exist_ok=True)
513 ref.write_text(base, encoding="utf-8")
514 bundle = tmp_path / "multibr.bundle"
515 _create_bundle(tmp_path, bundle)
516 result = _invoke(["bundle", "inspect", str(bundle), "--json"])
517 data = _parse_inspect(result)
518 assert len(data["branches"]) == 11 # main + 10 feature branches
519 tip = next(c for c in data["commits"] if c["commit_id"] == base)
520 assert len(tip["branches"]) == 11
521
522 def test_concurrent_inspect_consistent(self, tmp_path: pathlib.Path) -> None:
523 _init_repo(tmp_path)
524 prev = None
525 for i in range(20):
526 prev = _make_commit(tmp_path, parent_id=prev, content=f"conc-{i}".encode())
527 bundle = tmp_path / "concurrent.bundle"
528 _create_bundle(tmp_path, bundle)
529 errors: list[str] = []
530
531 def _read() -> None:
532 r = _invoke(["bundle", "inspect", str(bundle), "--json"])
533 if r.exit_code != 0:
534 errors.append(f"exit {r.exit_code}")
535 else:
536 try:
537 d = json.loads(r.output)
538 if d["total_commits"] != 20:
539 errors.append(f"count {d['total_commits']}")
540 except Exception as exc:
541 errors.append(str(exc))
542
543 threads = [threading.Thread(target=_read) for _ in range(8)]
544 for t in threads:
545 t.start()
546 for t in threads:
547 t.join()
548 assert not errors, f"Concurrent inspect failures: {errors}"
549
550
551 # ===========================================================================
552 # Feature 2: --verify flag on muse bundle unbundle
553 # ===========================================================================
554
555
556 class TestBundleUnbundleVerifyFlag:
557 """--verify flag: verify integrity before applying."""
558
559 def _src_dst(
560 self, tmp_path: pathlib.Path, dst_id: str = "verify-dst"
561 ) -> tuple[pathlib.Path, pathlib.Path]:
562 src = tmp_path / "src"
563 dst = tmp_path / "dst"
564 src.mkdir()
565 dst.mkdir()
566 _init_repo(src)
567 _init_repo(dst, repo_id=dst_id)
568 return src, dst
569
570 def test_verify_flag_help_mentioned(self) -> None:
571 result = _invoke(["bundle", "unbundle", "--help"])
572 assert result.exit_code == 0
573 assert "--verify" in result.output
574
575 def test_verify_flag_clean_bundle_exits_0(self, tmp_path: pathlib.Path) -> None:
576 src, dst = self._src_dst(tmp_path)
577 _make_commit(src, content=b"vf-clean")
578 bundle = tmp_path / "clean.bundle"
579 _create_bundle(src, bundle)
580 result = _invoke(["bundle", "unbundle", str(bundle), "--verify"], env=_env(dst))
581 assert result.exit_code == 0
582
583 def test_verify_flag_applies_objects(self, tmp_path: pathlib.Path) -> None:
584 src, dst = self._src_dst(tmp_path)
585 _make_commit(src, content=b"vf-apply")
586 bundle = tmp_path / "apply.bundle"
587 _create_bundle(src, bundle)
588 result = _invoke(["bundle", "unbundle", str(bundle), "--verify"], env=_env(dst))
589 assert result.exit_code == 0
590 assert "unpacked" in result.output.lower() or "commit" in result.output.lower()
591
592 def test_verify_flag_corrupt_bundle_exits_1(self, tmp_path: pathlib.Path) -> None:
593 src, dst = self._src_dst(tmp_path)
594 _make_commit(src, content=b"vf-corrupt")
595 bundle = tmp_path / "corrupt.bundle"
596 _create_bundle(src, bundle)
597 # Corrupt an object
598 raw = msgpack.unpackb(bundle.read_bytes(), raw=False)
599 if raw.get("objects"):
600 raw["objects"][0]["content"] = b"TAMPERED"
601 bundle.write_bytes(msgpack.packb(raw, use_bin_type=True))
602 result = _invoke(["bundle", "unbundle", str(bundle), "--verify"], env=_env(dst))
603 assert result.exit_code != 0
604
605 def test_verify_flag_corrupt_does_not_write(self, tmp_path: pathlib.Path) -> None:
606 """When --verify fails, no objects must be written to the destination."""
607 src, dst = self._src_dst(tmp_path)
608 _make_commit(src, content=b"vf-no-write")
609 bundle = tmp_path / "nowrite.bundle"
610 _create_bundle(src, bundle)
611 raw = msgpack.unpackb(bundle.read_bytes(), raw=False)
612 obj_ids_before = set(raw.get("branch_heads", {}).values())
613 if raw.get("objects"):
614 raw["objects"][0]["content"] = b"CORRUPTED"
615 bundle.write_bytes(msgpack.packb(raw, use_bin_type=True))
616 _invoke(["bundle", "unbundle", str(bundle), "--verify"], env=_env(dst))
617 # Destination object store must be empty
618 objects_dir = dst / ".muse" / "objects"
619 written = list(objects_dir.rglob("*")) if objects_dir.exists() else []
620 written_files = [p for p in written if p.is_file()]
621 assert len(written_files) == 0, "Objects were written despite corrupt bundle"
622
623 def test_verify_flag_json_output_has_verified_field(
624 self, tmp_path: pathlib.Path
625 ) -> None:
626 src, dst = self._src_dst(tmp_path)
627 _make_commit(src, content=b"vf-json")
628 bundle = tmp_path / "json.bundle"
629 _create_bundle(src, bundle)
630 result = _invoke(
631 ["bundle", "unbundle", str(bundle), "--verify", "--json"], env=_env(dst)
632 )
633 assert result.exit_code == 0
634 data = json.loads(result.output)
635 assert "verified" in data
636 assert data["verified"] is True
637
638 def test_verify_flag_json_corrupt_verified_false(
639 self, tmp_path: pathlib.Path
640 ) -> None:
641 src, dst = self._src_dst(tmp_path)
642 _make_commit(src, content=b"vf-json-corrupt")
643 bundle = tmp_path / "json-corrupt.bundle"
644 _create_bundle(src, bundle)
645 raw = msgpack.unpackb(bundle.read_bytes(), raw=False)
646 if raw.get("objects"):
647 raw["objects"][0]["content"] = b"CORRUPT"
648 bundle.write_bytes(msgpack.packb(raw, use_bin_type=True))
649 result = _invoke(
650 ["bundle", "unbundle", str(bundle), "--verify", "--json"], env=_env(dst)
651 )
652 assert result.exit_code != 0
653
654 def test_no_verify_flag_still_works(self, tmp_path: pathlib.Path) -> None:
655 """Without --verify the old behavior is unchanged."""
656 src, dst = self._src_dst(tmp_path)
657 _make_commit(src, content=b"vf-no-flag")
658 bundle = tmp_path / "noflag.bundle"
659 _create_bundle(src, bundle)
660 result = _invoke(["bundle", "unbundle", str(bundle)], env=_env(dst))
661 assert result.exit_code == 0
662
663 def test_verify_and_no_update_refs_combined(self, tmp_path: pathlib.Path) -> None:
664 """--verify and --no-update-refs must be combinable."""
665 src, dst = self._src_dst(tmp_path)
666 _make_commit(src, content=b"vf-no-refs")
667 bundle = tmp_path / "norefs.bundle"
668 _create_bundle(src, bundle)
669 result = _invoke(
670 ["bundle", "unbundle", str(bundle), "--verify", "--no-update-refs", "--json"],
671 env=_env(dst),
672 )
673 assert result.exit_code == 0
674 data = json.loads(result.output)
675 assert data["verified"] is True
676 assert data["refs_updated"] == []
677
678 def test_verify_flag_security_corrupt_before_parse(
679 self, tmp_path: pathlib.Path
680 ) -> None:
681 """Bytes-level corruption (not msgpack) is caught before any write."""
682 src, dst = self._src_dst(tmp_path)
683 _make_commit(src, content=b"vf-bytes-corrupt")
684 bundle = tmp_path / "bytes-corrupt.bundle"
685 _create_bundle(src, bundle)
686 raw = bundle.read_bytes()
687 # Flip bytes in the middle to corrupt msgpack framing
688 mid = len(raw) // 2
689 corrupted = raw[:mid] + bytes(b ^ 0xFF for b in raw[mid:mid + 20]) + raw[mid + 20:]
690 bundle.write_bytes(corrupted)
691 result = _invoke(["bundle", "unbundle", str(bundle), "--verify"], env=_env(dst))
692 assert result.exit_code != 0
693
694
695 class TestBundleUnbundleVerifyStress:
696 def test_verify_flag_100_commit_bundle(self, tmp_path: pathlib.Path) -> None:
697 src = tmp_path / "src"
698 dst = tmp_path / "dst"
699 src.mkdir()
700 dst.mkdir()
701 _init_repo(src)
702 _init_repo(dst, repo_id="stress-verify-dst")
703 prev = None
704 for i in range(100):
705 prev = _make_commit(src, parent_id=prev, content=f"sv-{i}".encode())
706 bundle = tmp_path / "sv100.bundle"
707 _create_bundle(src, bundle)
708 start = time.monotonic()
709 result = _invoke(
710 ["bundle", "unbundle", str(bundle), "--verify", "--json"], env=_env(dst)
711 )
712 elapsed = time.monotonic() - start
713 assert result.exit_code == 0
714 data = json.loads(result.output)
715 assert data["verified"] is True
716 assert data["commits_written"] == 100
717 assert elapsed < 5.0, f"verify+unbundle 100 commits took {elapsed:.2f}s"
718
719
720 # ===========================================================================
721 # Feature 3: muse bundle diff
722 # ===========================================================================
723
724
725 class TestBundleDiffUnit:
726 """Unit-level schema and output contracts for bundle diff."""
727
728 def test_diff_help_exits_0(self) -> None:
729 result = _invoke(["bundle", "diff", "--help"])
730 assert result.exit_code == 0
731
732 def test_diff_help_mentions_agent(self) -> None:
733 result = _invoke(["bundle", "diff", "--help"])
734 assert "agent" in result.output.lower() or "Agent" in result.output
735
736 def test_diff_json_schema_keys(self, tmp_path: pathlib.Path) -> None:
737 _init_repo(tmp_path)
738 _make_commit(tmp_path, content=b"diff-schema")
739 bundle = tmp_path / "dschema.bundle"
740 _create_bundle(tmp_path, bundle)
741 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(tmp_path))
742 assert result.exit_code == 0
743 data = _parse_diff(result)
744 for key in ("new_commits", "known_commits", "refs_to_advance", "commits"):
745 assert key in data, f"diff JSON missing key: {key}"
746
747 def test_diff_known_commits_when_already_applied(
748 self, tmp_path: pathlib.Path
749 ) -> None:
750 """If the repo already has all bundle commits, new_commits == 0."""
751 _init_repo(tmp_path)
752 _make_commit(tmp_path, content=b"diff-known")
753 bundle = tmp_path / "known.bundle"
754 _create_bundle(tmp_path, bundle)
755 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(tmp_path))
756 data = _parse_diff(result)
757 assert data["new_commits"] == 0
758 assert data["known_commits"] >= 1
759
760 def test_diff_new_commits_in_fresh_repo(self, tmp_path: pathlib.Path) -> None:
761 """Diff against a fresh repo with no commits: all bundle commits are new."""
762 src = tmp_path / "src"
763 dst = tmp_path / "dst"
764 src.mkdir()
765 dst.mkdir()
766 _init_repo(src)
767 _init_repo(dst, repo_id="diff-fresh-dst")
768 prev = None
769 for i in range(3):
770 prev = _make_commit(src, parent_id=prev, content=f"df-{i}".encode())
771 bundle = tmp_path / "fresh.bundle"
772 _create_bundle(src, bundle)
773 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
774 data = _parse_diff(result)
775 assert data["new_commits"] == 3
776 assert data["known_commits"] == 0
777
778 def test_diff_refs_to_advance_populated(self, tmp_path: pathlib.Path) -> None:
779 """refs_to_advance must contain branch names that would move."""
780 src = tmp_path / "src"
781 dst = tmp_path / "dst"
782 src.mkdir()
783 dst.mkdir()
784 _init_repo(src)
785 _init_repo(dst, repo_id="diff-refs-dst")
786 _make_commit(src, content=b"diff-refs")
787 bundle = tmp_path / "refs.bundle"
788 _create_bundle(src, bundle)
789 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
790 data = _parse_diff(result)
791 assert "main" in data["refs_to_advance"]
792
793 def test_diff_refs_to_advance_empty_when_known(
794 self, tmp_path: pathlib.Path
795 ) -> None:
796 """When the repo is already up-to-date, refs_to_advance is empty."""
797 _init_repo(tmp_path)
798 _make_commit(tmp_path, content=b"diff-upto-date")
799 bundle = tmp_path / "upto.bundle"
800 _create_bundle(tmp_path, bundle)
801 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(tmp_path))
802 data = _parse_diff(result)
803 assert data["refs_to_advance"] == []
804
805 def test_diff_commits_list_contains_new_entries(
806 self, tmp_path: pathlib.Path
807 ) -> None:
808 src = tmp_path / "src"
809 dst = tmp_path / "dst"
810 src.mkdir()
811 dst.mkdir()
812 _init_repo(src)
813 _init_repo(dst, repo_id="diff-commits-dst")
814 prev = None
815 cids = []
816 for i in range(3):
817 prev = _make_commit(src, parent_id=prev, content=f"dc-{i}".encode())
818 cids.append(prev)
819 bundle = tmp_path / "commits.bundle"
820 _create_bundle(src, bundle)
821 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
822 data = _parse_diff(result)
823 listed_ids = {c["commit_id"] for c in data["commits"]}
824 for cid in cids:
825 assert cid in listed_ids
826
827 def test_diff_commit_entry_schema(self, tmp_path: pathlib.Path) -> None:
828 src = tmp_path / "src"
829 dst = tmp_path / "dst"
830 src.mkdir()
831 dst.mkdir()
832 _init_repo(src)
833 _init_repo(dst, repo_id="diff-entry-dst")
834 _make_commit(src, content=b"diff-entry")
835 bundle = tmp_path / "entry.bundle"
836 _create_bundle(src, bundle)
837 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
838 data = _parse_diff(result)
839 if data["commits"]:
840 entry = data["commits"][0]
841 for key in ("commit_id", "message", "committed_at"):
842 assert key in entry
843
844 def test_diff_partial_known_commits(self, tmp_path: pathlib.Path) -> None:
845 """When the dst repo has some but not all commits, count matches."""
846 src = tmp_path / "src"
847 dst = tmp_path / "dst"
848 src.mkdir()
849 dst.mkdir()
850 _init_repo(src)
851 _init_repo(dst, repo_id="diff-partial-dst")
852 # Build 5-commit chain; write first 2 to dst manually
853 prev = None
854 all_ids: list[str] = []
855 for i in range(5):
856 prev = _make_commit(src, parent_id=prev, content=f"partial-{i}".encode())
857 all_ids.append(prev)
858 # Copy first 2 commits into dst so they are "known"
859 from muse.core.store import read_commit
860 for cid in all_ids[:2]:
861 rec = read_commit(src, cid)
862 if rec:
863 write_commit(dst, rec)
864 bundle = tmp_path / "partial.bundle"
865 _create_bundle(src, bundle)
866 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
867 data = _parse_diff(result)
868 assert data["new_commits"] == 3
869 assert data["known_commits"] == 2
870
871 def test_diff_requires_repo(self, tmp_path: pathlib.Path) -> None:
872 """diff requires a repository (unlike inspect/verify/list-heads)."""
873 src = tmp_path / "src"
874 src.mkdir()
875 _init_repo(src)
876 _make_commit(src, content=b"diff-needs-repo")
877 bundle = tmp_path / "needsrepo.bundle"
878 _create_bundle(src, bundle)
879 # Point MUSE_REPO_ROOT at a directory with no .muse → require_repo() fails.
880 no_repo = tmp_path / "no_repo"
881 no_repo.mkdir()
882 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(no_repo))
883 assert result.exit_code != 0
884
885 def test_diff_file_not_found(self, tmp_path: pathlib.Path) -> None:
886 _init_repo(tmp_path)
887 result = _invoke(
888 ["bundle", "diff", str(tmp_path / "missing.bundle"), "--json"],
889 env=_env(tmp_path),
890 )
891 assert result.exit_code != 0
892
893 def test_diff_j_alias(self, tmp_path: pathlib.Path) -> None:
894 src = tmp_path / "src"
895 dst = tmp_path / "dst"
896 src.mkdir()
897 dst.mkdir()
898 _init_repo(src)
899 _init_repo(dst, repo_id="diff-j-alias-dst")
900 _make_commit(src, content=b"diff-jalias")
901 bundle = tmp_path / "jalias.bundle"
902 _create_bundle(src, bundle)
903 r1 = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
904 r2 = _invoke(["bundle", "diff", str(bundle), "-j"], env=_env(dst))
905 assert r1.exit_code == 0
906 assert r2.exit_code == 0
907 d1 = json.loads(r1.output)
908 d2 = json.loads(r2.output)
909 assert d1["new_commits"] == d2["new_commits"]
910
911
912 class TestBundleDiffText:
913 def test_text_output_mentions_new_commits(self, tmp_path: pathlib.Path) -> None:
914 src = tmp_path / "src"
915 dst = tmp_path / "dst"
916 src.mkdir()
917 dst.mkdir()
918 _init_repo(src)
919 _init_repo(dst, repo_id="diff-txt-dst")
920 _make_commit(src, content=b"diff-txt")
921 bundle = tmp_path / "txt.bundle"
922 _create_bundle(src, bundle)
923 result = _invoke(["bundle", "diff", str(bundle)], env=_env(dst))
924 assert result.exit_code == 0
925 assert "new" in result.output.lower() or "commit" in result.output.lower()
926
927 def test_text_output_up_to_date_message(self, tmp_path: pathlib.Path) -> None:
928 """When nothing is new, output should say so."""
929 _init_repo(tmp_path)
930 _make_commit(tmp_path, content=b"diff-uptodate-txt")
931 bundle = tmp_path / "uptodate.bundle"
932 _create_bundle(tmp_path, bundle)
933 result = _invoke(["bundle", "diff", str(bundle)], env=_env(tmp_path))
934 assert result.exit_code == 0
935 # Should mention up-to-date or 0 new commits
936 assert "0" in result.output or "up-to-date" in result.output.lower()
937
938
939 class TestBundleDiffSecurity:
940 def _has_ansi(self, s: str) -> bool:
941 return "\x1b[" in s
942
943 def test_ansi_in_bundle_message_stripped(self, tmp_path: pathlib.Path) -> None:
944 src = tmp_path / "src"
945 dst = tmp_path / "dst"
946 src.mkdir()
947 dst.mkdir()
948 _init_repo(src)
949 _init_repo(dst, repo_id="diff-sec-ansi-dst")
950 _make_commit(src, content=b"diff-sec-ansi", message="\x1b[31mevil\x1b[0m")
951 bundle = tmp_path / "ansi.bundle"
952 _create_bundle(src, bundle)
953 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
954 assert result.exit_code == 0
955 assert not self._has_ansi(result.output)
956
957
958 class TestBundleDiffDataIntegrity:
959 def test_diff_then_unbundle_gives_zero_new(self, tmp_path: pathlib.Path) -> None:
960 """After unbundling, a second diff should show 0 new commits."""
961 src = tmp_path / "src"
962 dst = tmp_path / "dst"
963 src.mkdir()
964 dst.mkdir()
965 _init_repo(src)
966 _init_repo(dst, repo_id="diff-di-dst")
967 prev = None
968 for i in range(3):
969 prev = _make_commit(src, parent_id=prev, content=f"di-dt-{i}".encode())
970 bundle = tmp_path / "di.bundle"
971 _create_bundle(src, bundle)
972 # Before unbundle: 3 new
973 r1 = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
974 assert json.loads(r1.output)["new_commits"] == 3
975 # Unbundle
976 _invoke(["bundle", "unbundle", str(bundle)], env=_env(dst))
977 # After unbundle: 0 new
978 r2 = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
979 assert json.loads(r2.output)["new_commits"] == 0
980
981 def test_diff_new_count_matches_actual_writes(
982 self, tmp_path: pathlib.Path
983 ) -> None:
984 """new_commits from diff must equal commits_written from unbundle --json."""
985 src = tmp_path / "src"
986 dst = tmp_path / "dst"
987 src.mkdir()
988 dst.mkdir()
989 _init_repo(src)
990 _init_repo(dst, repo_id="diff-di2-dst")
991 prev = None
992 for i in range(5):
993 prev = _make_commit(src, parent_id=prev, content=f"match-{i}".encode())
994 bundle = tmp_path / "match.bundle"
995 _create_bundle(src, bundle)
996 diff_data = json.loads(
997 _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst)).output
998 )
999 unbundle_data = json.loads(
1000 _invoke(["bundle", "unbundle", str(bundle), "--json"], env=_env(dst)).output
1001 )
1002 assert diff_data["new_commits"] == unbundle_data["commits_written"]
1003
1004
1005 class TestBundleDiffPerformance:
1006 def test_diff_100_commit_bundle_under_1s(self, tmp_path: pathlib.Path) -> None:
1007 src = tmp_path / "src"
1008 dst = tmp_path / "dst"
1009 src.mkdir()
1010 dst.mkdir()
1011 _init_repo(src)
1012 _init_repo(dst, repo_id="diff-perf-dst")
1013 prev = None
1014 for i in range(100):
1015 prev = _make_commit(src, parent_id=prev, content=f"dp-{i}".encode())
1016 bundle = tmp_path / "dp100.bundle"
1017 _create_bundle(src, bundle)
1018 start = time.monotonic()
1019 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
1020 elapsed = time.monotonic() - start
1021 assert result.exit_code == 0
1022 data = _parse_diff(result)
1023 assert data["new_commits"] == 100
1024 assert elapsed < 1.0, f"diff 100-commit bundle took {elapsed:.2f}s"
1025
1026
1027 class TestBundleDiffStress:
1028 def test_diff_200_commit_bundle(self, tmp_path: pathlib.Path) -> None:
1029 src = tmp_path / "src"
1030 dst = tmp_path / "dst"
1031 src.mkdir()
1032 dst.mkdir()
1033 _init_repo(src)
1034 _init_repo(dst, repo_id="diff-stress-dst")
1035 prev = None
1036 for i in range(200):
1037 prev = _make_commit(src, parent_id=prev, content=f"ds-{i}".encode())
1038 bundle = tmp_path / "ds200.bundle"
1039 _create_bundle(src, bundle)
1040 result = _invoke(["bundle", "diff", str(bundle), "--json"], env=_env(dst))
1041 assert result.exit_code == 0
1042 data = _parse_diff(result)
1043 assert data["new_commits"] == 200
File History 1 commit
sha256:88ac91129873e6a496e9189515aa690eb893ae25d69c8f72af141a2be5068eb3 docs: docstring sprint contract→find-symbol — idiomatic run… Sonnet 4.6 patch 148 days ago