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