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