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