gabriel / muse public
test_pack_objects_supercharge.py python
617 lines 23.5 KB
Raw
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 23 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.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_objects_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 "objects" in d
260
261 def test_have_pruning_reduces_objects(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["objects"] < full["objects"]
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_objects_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 "objects_written" in d
346
347 def test_objects_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 "objects_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["objects_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: flip a byte in the middle
400 corrupted = bytearray(mpack)
401 corrupted[len(corrupted) // 2] ^= 0xFF
402 r = _verify(repo, bytes(corrupted))
403 # Either fails to parse or reports integrity failure
404 assert r.exit_code != 0 or not _json_out(r).get("all_ok", True)
405
406 def test_verify_stat_has_duration_ms(self, tmp_path: pathlib.Path) -> None:
407 repo = _make_repo(tmp_path)
408 mpack = _make_bundle(repo)
409 r = _verify(repo, mpack, "--stat")
410 assert r.exit_code == 0
411 d = _json_out(r)
412 assert "duration_ms" in d
413
414 def test_verify_stat_has_exit_code(self, tmp_path: pathlib.Path) -> None:
415 repo = _make_repo(tmp_path)
416 mpack = _make_bundle(repo)
417 d = _json_out(_verify(repo, mpack, "--stat"))
418 assert "exit_code" in d
419
420 def test_verify_duration_ms_under_two_seconds(self, tmp_path: pathlib.Path) -> None:
421 repo = _make_repo(tmp_path)
422 for i in range(50):
423 _write_obj(repo, f"obj {i}".encode() * 100)
424 mpack = _make_bundle(repo)
425 d = _json_out(_verify(repo, mpack))
426 assert d["duration_ms"] < 2000.0
427
428
429 # ---------------------------------------------------------------------------
430 # verify-pack: existing fields still present [GREEN]
431 # ---------------------------------------------------------------------------
432
433 class TestVerifyPackGreen:
434 def test_all_ok_field_clean_bundle(self, tmp_path: pathlib.Path) -> None:
435 repo = _make_repo(tmp_path)
436 mpack = _make_bundle(repo)
437 d = _json_out(_verify(repo, mpack))
438 assert d["all_ok"] is True
439
440 def test_failures_empty_on_clean_bundle(self, tmp_path: pathlib.Path) -> None:
441 repo = _make_repo(tmp_path)
442 mpack = _make_bundle(repo)
443 d = _json_out(_verify(repo, mpack))
444 assert d["failures"] == []
445
446 def test_objects_checked_field(self, tmp_path: pathlib.Path) -> None:
447 repo = _make_repo(tmp_path)
448 mpack = _make_bundle(repo)
449 d = _json_out(_verify(repo, mpack))
450 assert "objects_checked" in d
451
452 def test_stat_objects_count(self, tmp_path: pathlib.Path) -> None:
453 repo = _make_repo(tmp_path)
454 oid_a = _write_obj(repo, b"a")
455 oid_b = _write_obj(repo, b"b")
456 _commit(repo, "init", {"a.py": oid_a, "b.py": oid_b})
457 mpack = _pack(repo, "HEAD").stdout_bytes
458 d = _json_out(_verify(repo, mpack, "--stat"))
459 assert d["objects"] >= 2
460
461 def test_stat_commits_count(self, tmp_path: pathlib.Path) -> None:
462 repo = _make_repo(tmp_path)
463 mpack = _make_bundle(repo)
464 d = _json_out(_verify(repo, mpack, "--stat"))
465 assert d["commits"] >= 1
466
467
468 # ---------------------------------------------------------------------------
469 # Phase 3 — build_mpack fails loudly on MISSING objects [RED]
470 # ---------------------------------------------------------------------------
471
472 def _write_promisor_config(repo: pathlib.Path, remote_name: str = "origin") -> None:
473 config_path = config_toml_path(repo)
474 config_path.write_text(
475 f"[remotes.{remote_name}]\n"
476 f'url = "https://localhost:1337/test/repo"\n',
477 encoding="utf-8",
478 )
479
480
481 class TestPackObjectsMissingObjectValidation:
482 """pack-objects fails loudly when a snapshot references a MISSING object."""
483
484 def test_missing_object_exits_nonzero(self, tmp_path: pathlib.Path) -> None:
485 """pack-objects exits nonzero when a snapshot refs an object absent with no promisor."""
486 repo = _make_repo(tmp_path)
487 # Write snapshot that refs an object we deliberately do NOT write
488 from muse.core.ids import hash_snapshot as compute_snapshot_id
489 from muse.core.commits import (
490 CommitRecord,
491 write_commit,
492 )
493 from muse.core.snapshots import (
494 SnapshotRecord,
495 write_snapshot,
496 )
497 missing_oid = long_id("a" * 64)
498 sid = compute_snapshot_id({"missing.py": missing_oid})
499 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"missing.py": missing_oid}, created_at=_TS))
500 cid = _commit.__wrapped__(repo, "broken commit", {"missing.py": missing_oid}) if hasattr(_commit, "__wrapped__") else None
501 # Use the helpers directly
502 from muse.core.ids import hash_commit as compute_commit_id
503 cid = compute_commit_id( parent_ids=[],
504 snapshot_id=sid,
505 message="broken commit",
506 committed_at_iso=_TS.isoformat(),
507 author="gabriel",)
508 write_commit(repo, CommitRecord(
509 commit_id=cid, branch="main",
510 snapshot_id=sid, message="broken commit", committed_at=_TS,
511 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
512 ))
513 (heads_dir(repo) / "main").write_text(cid)
514 r = _pack(repo, "HEAD")
515 assert r.exit_code != 0
516
517 def test_missing_object_error_mentions_object_id(self, tmp_path: pathlib.Path) -> None:
518 """Error output names the missing object so the user knows what to fix."""
519 repo = _make_repo(tmp_path)
520 missing_oid = long_id("b" * 64)
521 from muse.core.ids import hash_snapshot as compute_snapshot_id, hash_commit as compute_commit_id
522 from muse.core.commits import (
523 CommitRecord,
524 write_commit,
525 )
526 from muse.core.snapshots import (
527 SnapshotRecord,
528 write_snapshot,
529 )
530 sid = compute_snapshot_id({"gone.py": missing_oid})
531 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"gone.py": missing_oid}, created_at=_TS))
532 cid = compute_commit_id( parent_ids=[],
533 snapshot_id=sid,
534 message="gone",
535 committed_at_iso=_TS.isoformat(),
536 author="gabriel",)
537 write_commit(repo, CommitRecord(
538 commit_id=cid, branch="main",
539 snapshot_id=sid, message="gone", committed_at=_TS,
540 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
541 ))
542 (heads_dir(repo) / "main").write_text(cid)
543 r = _pack(repo, "HEAD")
544 assert r.exit_code != 0
545 # Error should mention the missing object or "missing"
546 assert "missing" in (r.output + r.stderr).lower() or "absent" in (r.output + r.stderr).lower()
547
548 def test_promised_object_does_not_fail(self, tmp_path: pathlib.Path) -> None:
549 """PROMISED objects (promisor remote configured) are skipped, not failures."""
550 repo = _make_repo(tmp_path)
551 _write_promisor_config(repo)
552 missing_oid = long_id("c" * 64)
553 from muse.core.ids import hash_snapshot as compute_snapshot_id, hash_commit as compute_commit_id
554 from muse.core.commits import (
555 CommitRecord,
556 write_commit,
557 )
558 from muse.core.snapshots import (
559 SnapshotRecord,
560 write_snapshot,
561 )
562 sid = compute_snapshot_id({"remote.py": missing_oid})
563 write_snapshot(repo, SnapshotRecord(snapshot_id=sid, manifest={"remote.py": missing_oid}, created_at=_TS))
564 cid = compute_commit_id( parent_ids=[],
565 snapshot_id=sid,
566 message="partial clone",
567 committed_at_iso=_TS.isoformat(),
568 author="gabriel",)
569 write_commit(repo, CommitRecord(
570 commit_id=cid, branch="main",
571 snapshot_id=sid, message="partial clone", committed_at=_TS,
572 author="gabriel", parent_commit_id=None, parent2_commit_id=None,
573 ))
574 (heads_dir(repo) / "main").write_text(cid)
575 r = _pack(repo, "HEAD")
576 assert r.exit_code == 0
577
578 def test_present_object_always_passes(self, tmp_path: pathlib.Path) -> None:
579 """Fully self-contained mpack with all objects present passes."""
580 repo = _make_repo(tmp_path)
581 oid = _write_obj(repo, b"complete content")
582 _commit(repo, "good", {"file.py": oid})
583 r = _pack(repo, "HEAD")
584 assert r.exit_code == 0
585
586
587 class TestRegisterFlags:
588 def test_json_short_flag(self) -> None:
589 import argparse
590 from muse.cli.commands.pack_objects import register
591 p = argparse.ArgumentParser()
592 subs = p.add_subparsers()
593 register(subs)
594 args = p.parse_args(['pack-objects', 'HEAD', '-j'])
595 assert args.json_out is True
596
597 def test_json_long_flag(self) -> None:
598 import argparse
599 from muse.cli.commands.pack_objects import register
600 p = argparse.ArgumentParser()
601 subs = p.add_subparsers()
602 register(subs)
603 args = p.parse_args(['pack-objects', 'HEAD', '--json'])
604 assert args.json_out is True
605
606 def test_default_no_json(self) -> None:
607 import argparse
608 from muse.cli.commands.pack_objects import register
609 p = argparse.ArgumentParser()
610 subs = p.add_subparsers()
611 register(subs)
612 # Command-specific required args may differ; just check dest exists when possible
613 try:
614 args = p.parse_args(['pack-objects', 'HEAD'])
615 assert args.json_out is False
616 except SystemExit:
617 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 23 days ago