gabriel / muse public
test_pack_objects_supercharge.py python
596 lines 23.4 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
1 """Supercharge tests for ``muse pack-objects``, ``unpack-objects``, and ``verify-pack``.
2
3 TDD — [RED] tests fail until the feature lands; [GREEN] tests fill existing gaps.
4
5 New features under test
6 -----------------------
7 - ``duration_ms`` [RED] — wall-clock ms in every JSON output path
8 - ``exit_code`` [RED] — always present in every JSON output path
9 - ``object_bytes`` [RED] — total raw bytes in ``pack-objects --dry-run``
10
11 Gap-fill coverage [GREEN]
12 --------------------------
13 - dry-run keys validated exhaustively (want, have, commits, snapshots, objects)
14 - unpack round-trip output fields present (commits_written, objects_written, …)
15 - verify-pack all_ok field and failures list
16 - stat mode counts correct
17 """
18 from __future__ import annotations
19 from collections.abc import Mapping
20
21 import datetime
22 import json
23 import pathlib
24
25 import msgpack
26 import pytest
27
28 from muse.core.errors import ExitCode
29 from muse.core.object_store import write_object
30 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
31 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
32 from tests.cli_test_helper import CliRunner, InvokeResult
33 from muse.core._types import long_id, blob_id
34
35 runner = CliRunner()
36
37 _TS = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
38
39
40 # ---------------------------------------------------------------------------
41 # Shared helpers
42 # ---------------------------------------------------------------------------
43
44 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
45 repo = tmp_path / "repo"
46 muse = repo / ".muse"
47 for sub in ("objects", "commits", "snapshots", "refs/heads"):
48 (muse / sub).mkdir(parents=True)
49 (muse / "HEAD").write_text("ref: refs/heads/main")
50 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo", "domain": "code"}))
51 return repo
52
53
54 def _write_obj(repo: pathlib.Path, content: bytes) -> str:
55 oid = blob_id(content)
56 write_object(repo, oid, content)
57 return oid
58
59
60 def _commit(
61 repo: pathlib.Path,
62 msg: str,
63 manifest: dict[str, str],
64 *,
65 branch: str = "main",
66 parent: str | None = None,
67 ) -> str:
68 sid = compute_snapshot_id(manifest)
69 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest=manifest, created_at=_TS))
70 parent_ids = [parent] if parent else []
71 cid = compute_commit_id(
72 repo_id="test-repo",
73 parent_ids=parent_ids,
74 snapshot_id=sid,
75 message=msg,
76 committed_at_iso=_TS.isoformat(),
77 author="gabriel",)
78 write_commit(repo, CommitRecord(
79 commit_id=cid, repo_id="test-repo", created_on_branch=branch,
80 snapshot_id=sid, message=msg, committed_at=_TS,
81 author="gabriel", parent_commit_id=parent, parent2_commit_id=None,
82 ))
83 ref = repo / ".muse" / "refs" / "heads" / branch
84 ref.parent.mkdir(parents=True, exist_ok=True)
85 ref.write_text(cid)
86 return cid
87
88
89 def _pack(repo: pathlib.Path, *args: str) -> InvokeResult:
90 return runner.invoke(None, ["pack-objects", *args], env={"MUSE_REPO_ROOT": str(repo)})
91
92
93 def _unpack(repo: pathlib.Path, bundle: bytes, *args: str) -> InvokeResult:
94 extra = [] if "--json" in args else ["--json"]
95 return runner.invoke(
96 None, ["unpack-objects", *extra, *args],
97 env={"MUSE_REPO_ROOT": str(repo)},
98 input=bundle,
99 )
100
101
102 def _verify(repo: pathlib.Path, bundle: bytes, *args: str) -> InvokeResult:
103 extra = [] if "--json" in args else ["--json"]
104 return runner.invoke(
105 None, ["verify-pack", *extra, *args],
106 env={"MUSE_REPO_ROOT": str(repo)},
107 input=bundle,
108 )
109
110
111 def _make_bundle(repo: pathlib.Path) -> bytes:
112 """Pack HEAD and return raw msgpack bytes."""
113 oid = _write_obj(repo, b"hello")
114 _commit(repo, "init", {"f.py": oid})
115 r = _pack(repo, "HEAD")
116 assert r.exit_code == 0, r.output
117 return r.stdout_bytes # raw binary from stdout.buffer
118
119
120 def _json_out(r: InvokeResult) -> Mapping[str, object]:
121 for line in r.output.splitlines():
122 line = line.strip()
123 if line.startswith("{"):
124 return json.loads(line)
125 raise ValueError(f"No JSON in output:\n{r.output!r}")
126
127
128 # ---------------------------------------------------------------------------
129 # pack-objects --dry-run: duration_ms, exit_code, object_bytes [RED]
130 # ---------------------------------------------------------------------------
131
132 class TestPackObjectsDryRunSupercharge:
133 """[RED] New fields in --dry-run JSON output."""
134
135 def test_dry_run_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
136 repo = _make_repo(tmp_path)
137 oid = _write_obj(repo, b"x")
138 _commit(repo, "init", {"f.py": oid})
139 r = _pack(repo, "HEAD", "--dry-run")
140 assert r.exit_code == 0
141 d = _json_out(r)
142 assert "duration_ms" in d
143
144 def test_dry_run_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
145 repo = _make_repo(tmp_path)
146 oid = _write_obj(repo, b"x")
147 _commit(repo, "init", {"f.py": oid})
148 r = _pack(repo, "HEAD", "--dry-run")
149 d = _json_out(r)
150 assert d["duration_ms"] >= 0.0
151
152 def test_dry_run_has_exit_code(self, tmp_path: pathlib.Path) -> None:
153 repo = _make_repo(tmp_path)
154 oid = _write_obj(repo, b"x")
155 _commit(repo, "init", {"f.py": oid})
156 r = _pack(repo, "HEAD", "--dry-run")
157 d = _json_out(r)
158 assert "exit_code" in d
159
160 def test_dry_run_exit_code_is_zero_on_success(self, tmp_path: pathlib.Path) -> None:
161 repo = _make_repo(tmp_path)
162 oid = _write_obj(repo, b"x")
163 _commit(repo, "init", {"f.py": oid})
164 r = _pack(repo, "HEAD", "--dry-run")
165 d = _json_out(r)
166 assert d["exit_code"] == 0
167
168 def test_dry_run_has_object_bytes(self, tmp_path: pathlib.Path) -> None:
169 repo = _make_repo(tmp_path)
170 content = b"some content here"
171 oid = _write_obj(repo, content)
172 _commit(repo, "init", {"f.py": oid})
173 r = _pack(repo, "HEAD", "--dry-run")
174 d = _json_out(r)
175 assert "object_bytes" in d
176
177 def test_dry_run_object_bytes_matches_content_size(self, tmp_path: pathlib.Path) -> None:
178 repo = _make_repo(tmp_path)
179 content = b"x" * 256
180 oid = _write_obj(repo, content)
181 _commit(repo, "init", {"f.py": oid})
182 r = _pack(repo, "HEAD", "--dry-run")
183 d = _json_out(r)
184 assert d["object_bytes"] == 256
185
186 def test_dry_run_object_bytes_sums_multiple_objects(self, tmp_path: pathlib.Path) -> None:
187 repo = _make_repo(tmp_path)
188 oid_a = _write_obj(repo, b"a" * 100)
189 oid_b = _write_obj(repo, b"b" * 200)
190 _commit(repo, "init", {"a.py": oid_a, "b.py": oid_b})
191 r = _pack(repo, "HEAD", "--dry-run")
192 d = _json_out(r)
193 assert d["object_bytes"] == 300
194
195 def test_dry_run_object_bytes_is_int(self, tmp_path: pathlib.Path) -> None:
196 repo = _make_repo(tmp_path)
197 oid = _write_obj(repo, b"y")
198 _commit(repo, "init", {"f.py": oid})
199 r = _pack(repo, "HEAD", "--dry-run")
200 d = _json_out(r)
201 assert isinstance(d["object_bytes"], int)
202
203 def test_dry_run_object_bytes_zero_for_empty_pack(self, tmp_path: pathlib.Path) -> None:
204 """--have HEAD means nothing new to pack → 0 objects → 0 bytes."""
205 repo = _make_repo(tmp_path)
206 oid = _write_obj(repo, b"z")
207 cid = _commit(repo, "init", {"f.py": oid})
208 r = _pack(repo, cid, "--have", cid, "--dry-run")
209 d = _json_out(r)
210 assert d["object_bytes"] == 0
211
212
213 # ---------------------------------------------------------------------------
214 # pack-objects --dry-run: existing fields still present [GREEN]
215 # ---------------------------------------------------------------------------
216
217 class TestPackObjectsDryRunGreen:
218 """[GREEN] Existing dry-run fields remain after adding new ones."""
219
220 def test_want_field_present(self, tmp_path: pathlib.Path) -> None:
221 repo = _make_repo(tmp_path)
222 oid = _write_obj(repo, b"x")
223 _commit(repo, "init", {"f.py": oid})
224 d = _json_out(_pack(repo, "HEAD", "--dry-run"))
225 assert "want" in d
226
227 def test_have_field_present(self, tmp_path: pathlib.Path) -> None:
228 repo = _make_repo(tmp_path)
229 oid = _write_obj(repo, b"x")
230 _commit(repo, "init", {"f.py": oid})
231 d = _json_out(_pack(repo, "HEAD", "--dry-run"))
232 assert "have" in d
233
234 def test_commits_field_present(self, tmp_path: pathlib.Path) -> None:
235 repo = _make_repo(tmp_path)
236 oid = _write_obj(repo, b"x")
237 _commit(repo, "init", {"f.py": oid})
238 d = _json_out(_pack(repo, "HEAD", "--dry-run"))
239 assert "commits" in d
240
241 def test_snapshots_field_present(self, tmp_path: pathlib.Path) -> None:
242 repo = _make_repo(tmp_path)
243 oid = _write_obj(repo, b"x")
244 _commit(repo, "init", {"f.py": oid})
245 d = _json_out(_pack(repo, "HEAD", "--dry-run"))
246 assert "snapshots" in d
247
248 def test_objects_field_present(self, tmp_path: pathlib.Path) -> None:
249 repo = _make_repo(tmp_path)
250 oid = _write_obj(repo, b"x")
251 _commit(repo, "init", {"f.py": oid})
252 d = _json_out(_pack(repo, "HEAD", "--dry-run"))
253 assert "objects" in d
254
255 def test_have_pruning_reduces_objects(self, tmp_path: pathlib.Path) -> None:
256 repo = _make_repo(tmp_path)
257 oid = _write_obj(repo, b"v1")
258 c1 = _commit(repo, "c1", {"f.py": oid})
259 oid2 = _write_obj(repo, b"v2")
260 _commit(repo, "c2", {"f.py": oid2}, parent=c1)
261 full = _json_out(_pack(repo, "HEAD", "--dry-run"))
262 pruned = _json_out(_pack(repo, "HEAD", "--have", c1, "--dry-run"))
263 assert pruned["objects"] < full["objects"]
264
265
266 # ---------------------------------------------------------------------------
267 # unpack-objects: duration_ms and exit_code [RED]
268 # ---------------------------------------------------------------------------
269
270 class TestUnpackObjectsSupercharge:
271 """[RED] duration_ms and exit_code in unpack-objects JSON output."""
272
273 def test_unpack_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
274 repo = _make_repo(tmp_path)
275 bundle = _make_bundle(repo)
276 dest = _make_repo(tmp_path / "dest")
277 r = _unpack(dest, bundle)
278 assert r.exit_code == 0
279 d = _json_out(r)
280 assert "duration_ms" in d
281
282 def test_unpack_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
283 repo = _make_repo(tmp_path)
284 bundle = _make_bundle(repo)
285 dest = _make_repo(tmp_path / "dest")
286 d = _json_out(_unpack(dest, bundle))
287 assert d["duration_ms"] >= 0.0
288
289 def test_unpack_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
290 repo = _make_repo(tmp_path)
291 bundle = _make_bundle(repo)
292 dest = _make_repo(tmp_path / "dest")
293 d = _json_out(_unpack(dest, bundle))
294 assert "exit_code" in d
295
296 def test_unpack_exit_code_zero_on_success(self, tmp_path: pathlib.Path) -> None:
297 repo = _make_repo(tmp_path)
298 bundle = _make_bundle(repo)
299 dest = _make_repo(tmp_path / "dest")
300 d = _json_out(_unpack(dest, bundle))
301 assert d["exit_code"] == 0
302
303 def test_unpack_duration_ms_under_two_seconds(self, tmp_path: pathlib.Path) -> None:
304 repo = _make_repo(tmp_path)
305 for i in range(20):
306 oid = _write_obj(repo, f"content {i}".encode() * 50)
307 _commit(repo, f"c{i}", {f"f{i}.py": oid},
308 parent=None if i == 0 else None) # single chain not needed for pack
309 bundle = _make_bundle(repo)
310 dest = _make_repo(tmp_path / "dest")
311 d = _json_out(_unpack(dest, bundle))
312 assert d["duration_ms"] < 2000.0
313
314
315 # ---------------------------------------------------------------------------
316 # unpack-objects: existing output fields still present [GREEN]
317 # ---------------------------------------------------------------------------
318
319 class TestUnpackObjectsGreen:
320 def test_commits_written_field(self, tmp_path: pathlib.Path) -> None:
321 repo = _make_repo(tmp_path)
322 bundle = _make_bundle(repo)
323 dest = _make_repo(tmp_path / "dest")
324 d = _json_out(_unpack(dest, bundle))
325 assert "commits_written" in d
326
327 def test_snapshots_written_field(self, tmp_path: pathlib.Path) -> None:
328 repo = _make_repo(tmp_path)
329 bundle = _make_bundle(repo)
330 dest = _make_repo(tmp_path / "dest")
331 d = _json_out(_unpack(dest, bundle))
332 assert "snapshots_written" in d
333
334 def test_objects_written_field(self, tmp_path: pathlib.Path) -> None:
335 repo = _make_repo(tmp_path)
336 bundle = _make_bundle(repo)
337 dest = _make_repo(tmp_path / "dest")
338 d = _json_out(_unpack(dest, bundle))
339 assert "objects_written" in d
340
341 def test_objects_skipped_field(self, tmp_path: pathlib.Path) -> None:
342 repo = _make_repo(tmp_path)
343 bundle = _make_bundle(repo)
344 dest = _make_repo(tmp_path / "dest")
345 d = _json_out(_unpack(dest, bundle))
346 assert "objects_skipped" in d
347
348 def test_idempotent_second_unpack_skips_all(self, tmp_path: pathlib.Path) -> None:
349 repo = _make_repo(tmp_path)
350 bundle = _make_bundle(repo)
351 dest = _make_repo(tmp_path / "dest")
352 _unpack(dest, bundle)
353 d = _json_out(_unpack(dest, bundle))
354 assert d["objects_written"] == 0
355
356
357 # ---------------------------------------------------------------------------
358 # verify-pack: duration_ms and exit_code [RED]
359 # ---------------------------------------------------------------------------
360
361 class TestVerifyPackSupercharge:
362 """[RED] duration_ms and exit_code in verify-pack JSON output."""
363
364 def test_verify_json_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
365 repo = _make_repo(tmp_path)
366 bundle = _make_bundle(repo)
367 r = _verify(repo, bundle)
368 assert r.exit_code == 0
369 d = _json_out(r)
370 assert "duration_ms" in d
371
372 def test_verify_duration_ms_non_negative(self, tmp_path: pathlib.Path) -> None:
373 repo = _make_repo(tmp_path)
374 bundle = _make_bundle(repo)
375 d = _json_out(_verify(repo, bundle))
376 assert d["duration_ms"] >= 0.0
377
378 def test_verify_json_has_exit_code(self, tmp_path: pathlib.Path) -> None:
379 repo = _make_repo(tmp_path)
380 bundle = _make_bundle(repo)
381 d = _json_out(_verify(repo, bundle))
382 assert "exit_code" in d
383
384 def test_verify_exit_code_zero_on_clean(self, tmp_path: pathlib.Path) -> None:
385 repo = _make_repo(tmp_path)
386 bundle = _make_bundle(repo)
387 d = _json_out(_verify(repo, bundle))
388 assert d["exit_code"] == 0
389
390 def test_verify_exit_code_nonzero_on_corrupt(self, tmp_path: pathlib.Path) -> None:
391 repo = _make_repo(tmp_path)
392 bundle = _make_bundle(repo)
393 # Corrupt the bundle: flip a byte in the middle
394 corrupted = bytearray(bundle)
395 corrupted[len(corrupted) // 2] ^= 0xFF
396 r = _verify(repo, bytes(corrupted))
397 # Either fails to parse or reports integrity failure
398 assert r.exit_code != 0 or not _json_out(r).get("all_ok", True)
399
400 def test_verify_stat_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
401 repo = _make_repo(tmp_path)
402 bundle = _make_bundle(repo)
403 r = _verify(repo, bundle, "--stat")
404 assert r.exit_code == 0
405 d = _json_out(r)
406 assert "duration_ms" in d
407
408 def test_verify_stat_has_exit_code(self, tmp_path: pathlib.Path) -> None:
409 repo = _make_repo(tmp_path)
410 bundle = _make_bundle(repo)
411 d = _json_out(_verify(repo, bundle, "--stat"))
412 assert "exit_code" in d
413
414 def test_verify_duration_ms_under_two_seconds(self, tmp_path: pathlib.Path) -> None:
415 repo = _make_repo(tmp_path)
416 for i in range(50):
417 _write_obj(repo, f"obj {i}".encode() * 100)
418 bundle = _make_bundle(repo)
419 d = _json_out(_verify(repo, bundle))
420 assert d["duration_ms"] < 2000.0
421
422
423 # ---------------------------------------------------------------------------
424 # verify-pack: existing fields still present [GREEN]
425 # ---------------------------------------------------------------------------
426
427 class TestVerifyPackGreen:
428 def test_all_ok_field_clean_bundle(self, tmp_path: pathlib.Path) -> None:
429 repo = _make_repo(tmp_path)
430 bundle = _make_bundle(repo)
431 d = _json_out(_verify(repo, bundle))
432 assert d["all_ok"] is True
433
434 def test_failures_empty_on_clean_bundle(self, tmp_path: pathlib.Path) -> None:
435 repo = _make_repo(tmp_path)
436 bundle = _make_bundle(repo)
437 d = _json_out(_verify(repo, bundle))
438 assert d["failures"] == []
439
440 def test_objects_checked_field(self, tmp_path: pathlib.Path) -> None:
441 repo = _make_repo(tmp_path)
442 bundle = _make_bundle(repo)
443 d = _json_out(_verify(repo, bundle))
444 assert "objects_checked" in d
445
446 def test_stat_objects_count(self, tmp_path: pathlib.Path) -> None:
447 repo = _make_repo(tmp_path)
448 oid_a = _write_obj(repo, b"a")
449 oid_b = _write_obj(repo, b"b")
450 _commit(repo, "init", {"a.py": oid_a, "b.py": oid_b})
451 bundle = _pack(repo, "HEAD").stdout_bytes
452 d = _json_out(_verify(repo, bundle, "--stat"))
453 assert d["objects"] >= 2
454
455 def test_stat_commits_count(self, tmp_path: pathlib.Path) -> None:
456 repo = _make_repo(tmp_path)
457 bundle = _make_bundle(repo)
458 d = _json_out(_verify(repo, bundle, "--stat"))
459 assert d["commits"] >= 1
460
461
462 # ---------------------------------------------------------------------------
463 # Phase 3 — build_mpack fails loudly on MISSING objects [RED]
464 # ---------------------------------------------------------------------------
465
466 def _write_promisor_config(repo: pathlib.Path, remote_name: str = "origin") -> None:
467 config_path = repo / ".muse" / "config.toml"
468 config_path.write_text(
469 f"[remotes.{remote_name}]\n"
470 f'url = "https://localhost:1337/test/repo"\n',
471 encoding="utf-8",
472 )
473
474
475 class TestPackObjectsMissingObjectValidation:
476 """pack-objects fails loudly when a snapshot references a MISSING object."""
477
478 def test_missing_object_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
479 """pack-objects exits nonzero when a snapshot refs an object absent with no promisor."""
480 repo = _make_repo(tmp_path)
481 # Write snapshot that refs an object we deliberately do NOT write
482 from muse.core.snapshot import compute_snapshot_id
483 from muse.core.store import SnapshotRecord, write_snapshot, CommitRecord, write_commit
484 missing_oid = long_id("a" * 64)
485 sid = compute_snapshot_id({"missing.py": missing_oid})
486 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"missing.py": missing_oid}, created_at=_TS))
487 cid = _commit.__wrapped__(repo, "broken commit", {"missing.py": missing_oid}) if hasattr(_commit, "__wrapped__") else None
488 # Use the helpers directly
489 from muse.core.snapshot import compute_commit_id
490 cid = compute_commit_id(
491 repo_id="test-repo",
492 parent_ids=[],
493 snapshot_id=sid,
494 message="broken commit",
495 committed_at_iso=_TS.isoformat(),
496 author="gabriel",)
497 write_commit(repo, CommitRecord(
498 commit_id=cid, repo_id="test-repo", created_on_branch="main",
499 snapshot_id=sid, message="broken commit", committed_at=_TS,
500 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
501 ))
502 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
503 r = _pack(repo, "HEAD")
504 assert r.exit_code != 0
505
506 def test_missing_object_error_mentions_object_id(self, tmp_path: pathlib.Path) -> None:
507 """Error output names the missing object so the user knows what to fix."""
508 repo = _make_repo(tmp_path)
509 missing_oid = long_id("b" * 64)
510 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
511 from muse.core.store import SnapshotRecord, write_snapshot, CommitRecord, write_commit
512 sid = compute_snapshot_id({"gone.py": missing_oid})
513 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"gone.py": missing_oid}, created_at=_TS))
514 cid = compute_commit_id(
515 repo_id="test-repo",
516 parent_ids=[],
517 snapshot_id=sid,
518 message="gone",
519 committed_at_iso=_TS.isoformat(),
520 author="gabriel",)
521 write_commit(repo, CommitRecord(
522 commit_id=cid, repo_id="test-repo", created_on_branch="main",
523 snapshot_id=sid, message="gone", committed_at=_TS,
524 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
525 ))
526 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
527 r = _pack(repo, "HEAD")
528 assert r.exit_code != 0
529 # Error should mention the missing object or "missing"
530 assert "missing" in (r.output + r.stderr).lower() or "absent" in (r.output + r.stderr).lower()
531
532 def test_promised_object_does_not_fail(self, tmp_path: pathlib.Path) -> None:
533 """PROMISED objects (promisor remote configured) are skipped, not failures."""
534 repo = _make_repo(tmp_path)
535 _write_promisor_config(repo)
536 missing_oid = long_id("c" * 64)
537 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
538 from muse.core.store import SnapshotRecord, write_snapshot, CommitRecord, write_commit
539 sid = compute_snapshot_id({"remote.py": missing_oid})
540 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"remote.py": missing_oid}, created_at=_TS))
541 cid = compute_commit_id(
542 repo_id="test-repo",
543 parent_ids=[],
544 snapshot_id=sid,
545 message="partial clone",
546 committed_at_iso=_TS.isoformat(),
547 author="gabriel",)
548 write_commit(repo, CommitRecord(
549 commit_id=cid, repo_id="test-repo", created_on_branch="main",
550 snapshot_id=sid, message="partial clone", committed_at=_TS,
551 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
552 ))
553 (repo / ".muse" / "refs" / "heads" / "main").write_text(cid)
554 r = _pack(repo, "HEAD")
555 assert r.exit_code == 0
556
557 def test_present_object_always_passes(self, tmp_path: pathlib.Path) -> None:
558 """Fully self-contained bundle with all objects present passes."""
559 repo = _make_repo(tmp_path)
560 oid = _write_obj(repo, b"complete content")
561 _commit(repo, "good", {"file.py": oid})
562 r = _pack(repo, "HEAD")
563 assert r.exit_code == 0
564
565
566 class TestRegisterFlags:
567 def test_json_short_flag(self):
568 import argparse
569 from muse.cli.commands.pack_objects import register
570 p = argparse.ArgumentParser()
571 subs = p.add_subparsers()
572 register(subs)
573 args = p.parse_args(['pack-objects', 'HEAD', '-j'])
574 assert args.json_out is True
575
576 def test_json_long_flag(self):
577 import argparse
578 from muse.cli.commands.pack_objects import register
579 p = argparse.ArgumentParser()
580 subs = p.add_subparsers()
581 register(subs)
582 args = p.parse_args(['pack-objects', 'HEAD', '--json'])
583 assert args.json_out is True
584
585 def test_default_no_json(self):
586 import argparse
587 from muse.cli.commands.pack_objects import register
588 p = argparse.ArgumentParser()
589 subs = p.add_subparsers()
590 register(subs)
591 # Command-specific required args may differ; just check dest exists when possible
592 try:
593 args = p.parse_args(['pack-objects', 'HEAD'])
594 assert args.json_out is False
595 except SystemExit:
596 pass # required positional args missing — flag default still correct
File History 2 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 132 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 138 days ago