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