gabriel / muse public

test_cli_plumbing.py file-level

at sha256:6 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 test: validate staging push and MP workflow for aaronrene · · Jul 7, 2026
1 """Tests for all Tier 1 plumbing commands under ``muse plumbing …``.
2
3 Each plumbing command is tested via the Typer CliRunner so tests exercise the
4 full CLI stack including argument parsing, error handling, and JSON output
5 format. All commands are accessed through the ``plumbing`` sub-namespace.
6
7 The ``MUSE_REPO_ROOT`` env-var is used to point repo-discovery at the test
8 fixture without requiring ``os.chdir``.
9 """
10
11 from __future__ import annotations
12
13 import datetime
14 import json
15 import pathlib
16
17 import msgpack
18 import pytest
19 from tests.cli_test_helper import CliRunner
20
21 cli = None # argparse migration — CliRunner ignores this arg
22 from muse.core.errors import ExitCode
23 from muse.core.object_store import write_object
24 from muse.core.pack import build_pack
25 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
26
27 from muse.core._types import Manifest
28 from muse.core.store import (
29 CommitRecord,
30 SnapshotRecord,
31 write_commit,
32 write_snapshot,
33 )
34
35 runner = CliRunner()
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 def _init_repo(path: pathlib.Path) -> pathlib.Path:
43 """Create a minimal .muse/ directory structure."""
44 muse = path / ".muse"
45 (muse / "commits").mkdir(parents=True)
46 (muse / "snapshots").mkdir(parents=True)
47 (muse / "objects").mkdir(parents=True)
48 (muse / "refs" / "heads").mkdir(parents=True)
49 muse.joinpath("HEAD").write_text("ref: refs/heads/main")
50 muse.joinpath("repo.json").write_text(
51 json.dumps({"repo_id": "test-repo-id", "domain": "generic"})
52 )
53 return path
54
55
56 def _make_object(repo: pathlib.Path, content: bytes) -> str:
57 import hashlib
58
59 oid = hashlib.sha256(content).hexdigest()
60 write_object(repo, oid, content)
61 return oid
62
63
64 def _make_snapshot(
65 repo: pathlib.Path, manifest: Manifest
66 ) -> SnapshotRecord:
67 """Write a SnapshotRecord whose ID is content-addressed from *manifest*."""
68 snap_id = compute_snapshot_id(manifest)
69 snap = SnapshotRecord(
70 snapshot_id=snap_id,
71 manifest=manifest,
72 created_at=datetime.datetime(2026, 3, 18, tzinfo=datetime.timezone.utc),
73 )
74 write_snapshot(repo, snap)
75 return snap
76
77
78 def _make_commit(
79 repo: pathlib.Path,
80 snapshot_id: str,
81 *,
82 branch: str = "main",
83 parent_commit_id: str | None = None,
84 message: str = "test commit",
85 ) -> CommitRecord:
86 """Write a CommitRecord whose ID is content-addressed from its fields."""
87 committed_at = datetime.datetime(2026, 3, 18, tzinfo=datetime.timezone.utc)
88 parent_ids = [parent_commit_id] if parent_commit_id else []
89 commit_id = compute_commit_id(parent_ids, snapshot_id, message, committed_at.isoformat())
90 rec = CommitRecord(
91 commit_id=commit_id,
92 repo_id="test-repo-id",
93 branch=branch,
94 snapshot_id=snapshot_id,
95 message=message,
96 committed_at=committed_at,
97 author="tester",
98 parent_commit_id=parent_commit_id,
99 )
100 write_commit(repo, rec)
101 return rec
102
103
104 def _set_head(repo: pathlib.Path, branch: str, commit_id: str) -> None:
105 ref = repo / ".muse" / "refs" / "heads" / branch
106 ref.parent.mkdir(parents=True, exist_ok=True)
107 ref.write_text(commit_id)
108
109
110 def _repo_env(repo: pathlib.Path) -> Manifest:
111 """Return env dict that sets MUSE_REPO_ROOT to the given path."""
112 return {"MUSE_REPO_ROOT": str(repo)}
113
114
115 # ---------------------------------------------------------------------------
116 # hash-object
117 # ---------------------------------------------------------------------------
118
119
120 class TestHashObject:
121 def test_hash_file_json_output(self, tmp_path: pathlib.Path) -> None:
122 f = tmp_path / "test.txt"
123 f.write_bytes(b"hello world")
124 result = runner.invoke(cli, ["hash-object", str(f)])
125 assert result.exit_code == 0, result.output
126 data = json.loads(result.stdout)
127 assert "object_id" in data
128 assert len(data["object_id"]) == 64
129 assert data["stored"] is False
130
131 def test_hash_file_text_format(self, tmp_path: pathlib.Path) -> None:
132 f = tmp_path / "data.bin"
133 f.write_bytes(b"test bytes")
134 result = runner.invoke(
135 cli, ["hash-object", "--format", "text", str(f)]
136 )
137 assert result.exit_code == 0, result.output
138 assert len(result.stdout.strip()) == 64
139
140 def test_hash_and_write(self, tmp_path: pathlib.Path) -> None:
141 repo = _init_repo(tmp_path / "repo")
142 f = repo / "sample.txt"
143 f.write_bytes(b"write me")
144 result = runner.invoke(
145 cli,
146 ["hash-object", "--write", str(f)],
147 env=_repo_env(repo),
148 catch_exceptions=False,
149 )
150 assert result.exit_code == 0, result.output
151 data = json.loads(result.stdout)
152 assert data["stored"] is True
153
154 def test_missing_file_errors(self, tmp_path: pathlib.Path) -> None:
155 result = runner.invoke(cli, ["hash-object", str(tmp_path / "no.txt")])
156 assert result.exit_code == ExitCode.USER_ERROR
157
158 def test_directory_errors(self, tmp_path: pathlib.Path) -> None:
159 result = runner.invoke(cli, ["hash-object", str(tmp_path)])
160 assert result.exit_code == ExitCode.USER_ERROR
161
162
163 # ---------------------------------------------------------------------------
164 # cat-object
165 # ---------------------------------------------------------------------------
166
167
168 class TestCatObject:
169 def test_cat_raw_bytes(self, tmp_path: pathlib.Path) -> None:
170 repo = _init_repo(tmp_path)
171 content = b"raw content data"
172 oid = _make_object(repo, content)
173 result = runner.invoke(
174 cli, ["cat-object", oid],
175 env=_repo_env(repo),
176 catch_exceptions=False,
177 )
178 assert result.exit_code == 0, result.output
179 assert result.stdout_bytes == content
180
181 def test_cat_info_format(self, tmp_path: pathlib.Path) -> None:
182 repo = _init_repo(tmp_path)
183 content = b"info content"
184 oid = _make_object(repo, content)
185 result = runner.invoke(
186 cli, ["cat-object", "--format", "info", oid],
187 env=_repo_env(repo),
188 )
189 assert result.exit_code == 0, result.output
190 data = json.loads(result.stdout)
191 assert data["object_id"] == oid
192 assert data["present"] is True
193 assert data["size_bytes"] == len(content)
194
195 def test_missing_object_errors(self, tmp_path: pathlib.Path) -> None:
196 repo = _init_repo(tmp_path)
197 result = runner.invoke(
198 cli, ["cat-object", "a" * 64],
199 env=_repo_env(repo),
200 )
201 assert result.exit_code == ExitCode.USER_ERROR
202
203 def test_missing_object_info_format(self, tmp_path: pathlib.Path) -> None:
204 repo = _init_repo(tmp_path)
205 oid = "b" * 64
206 result = runner.invoke(
207 cli, ["cat-object", "--format", "info", oid],
208 env=_repo_env(repo),
209 )
210 assert result.exit_code == ExitCode.USER_ERROR
211 data = json.loads(result.stdout)
212 assert data["present"] is False
213
214
215 # ---------------------------------------------------------------------------
216 # rev-parse
217 # ---------------------------------------------------------------------------
218
219
220 class TestRevParse:
221 def test_resolve_branch(self, tmp_path: pathlib.Path) -> None:
222 repo = _init_repo(tmp_path)
223 oid = _make_object(repo, b"data")
224 snap = _make_snapshot(repo, {"f": oid})
225 commit = _make_commit(repo, snap.snapshot_id)
226 _set_head(repo, "main", commit.commit_id)
227
228 result = runner.invoke(
229 cli, ["rev-parse", "main"],
230 env=_repo_env(repo),
231 )
232 assert result.exit_code == 0, result.output
233 data = json.loads(result.stdout)
234 assert data["commit_id"] == commit.commit_id
235 assert data["ref"] == "main"
236
237 def test_resolve_head(self, tmp_path: pathlib.Path) -> None:
238 repo = _init_repo(tmp_path)
239 oid = _make_object(repo, b"data")
240 snap = _make_snapshot(repo, {"f": oid})
241 commit = _make_commit(repo, snap.snapshot_id, message="resolve head")
242 _set_head(repo, "main", commit.commit_id)
243
244 result = runner.invoke(
245 cli, ["rev-parse", "HEAD"],
246 env=_repo_env(repo),
247 )
248 assert result.exit_code == 0, result.output
249 data = json.loads(result.stdout)
250 assert data["commit_id"] == commit.commit_id
251
252 def test_resolve_text_format(self, tmp_path: pathlib.Path) -> None:
253 repo = _init_repo(tmp_path)
254 oid = _make_object(repo, b"data")
255 snap = _make_snapshot(repo, {"f": oid})
256 commit = _make_commit(repo, snap.snapshot_id, message="text format")
257 _set_head(repo, "main", commit.commit_id)
258
259 result = runner.invoke(
260 cli, ["rev-parse", "--format", "text", "main"],
261 env=_repo_env(repo),
262 )
263 assert result.exit_code == 0, result.output
264 assert result.stdout.strip() == commit.commit_id
265
266 def test_unknown_ref_errors(self, tmp_path: pathlib.Path) -> None:
267 repo = _init_repo(tmp_path)
268 result = runner.invoke(
269 cli, ["rev-parse", "nonexistent"],
270 env=_repo_env(repo),
271 )
272 assert result.exit_code == ExitCode.USER_ERROR
273 data = json.loads(result.stdout)
274 assert data["commit_id"] is None
275
276
277 # ---------------------------------------------------------------------------
278 # ls-files
279 # ---------------------------------------------------------------------------
280
281
282 class TestLsFiles:
283 def test_lists_files_json(self, tmp_path: pathlib.Path) -> None:
284 repo = _init_repo(tmp_path)
285 oid = _make_object(repo, b"track data")
286 snap = _make_snapshot(repo, {"tracks/drums.mid": oid})
287 commit = _make_commit(repo, snap.snapshot_id)
288 _set_head(repo, "main", commit.commit_id)
289
290 result = runner.invoke(
291 cli, ["ls-files"],
292 env=_repo_env(repo),
293 )
294 assert result.exit_code == 0, result.output
295 data = json.loads(result.stdout)
296 assert data["file_count"] == 1
297 assert data["files"][0]["path"] == "tracks/drums.mid"
298 assert data["files"][0]["object_id"] == oid
299
300 def test_lists_files_text(self, tmp_path: pathlib.Path) -> None:
301 repo = _init_repo(tmp_path)
302 oid = _make_object(repo, b"data")
303 snap = _make_snapshot(repo, {"a.txt": oid})
304 commit = _make_commit(repo, snap.snapshot_id, message="ls files text")
305 _set_head(repo, "main", commit.commit_id)
306
307 result = runner.invoke(
308 cli, ["ls-files", "--format", "text"],
309 env=_repo_env(repo),
310 )
311 assert result.exit_code == 0, result.output
312 assert "a.txt" in result.stdout
313
314 def test_with_explicit_commit(self, tmp_path: pathlib.Path) -> None:
315 repo = _init_repo(tmp_path)
316 oid = _make_object(repo, b"data")
317 snap = _make_snapshot(repo, {"x.mid": oid})
318 commit = _make_commit(repo, snap.snapshot_id, message="explicit commit")
319
320 result = runner.invoke(
321 cli, ["ls-files", "--commit", commit.commit_id],
322 env=_repo_env(repo),
323 )
324 assert result.exit_code == 0, result.output
325 data = json.loads(result.stdout)
326 assert data["commit_id"] == commit.commit_id
327
328 def test_no_commits_errors(self, tmp_path: pathlib.Path) -> None:
329 repo = _init_repo(tmp_path)
330 result = runner.invoke(
331 cli, ["ls-files"],
332 env=_repo_env(repo),
333 )
334 assert result.exit_code == ExitCode.USER_ERROR
335
336
337 # ---------------------------------------------------------------------------
338 # read-commit
339 # ---------------------------------------------------------------------------
340
341
342 class TestReadCommit:
343 def test_reads_commit_json(self, tmp_path: pathlib.Path) -> None:
344 repo = _init_repo(tmp_path)
345 oid = _make_object(repo, b"data")
346 snap = _make_snapshot(repo, {"f": oid})
347 commit = _make_commit(repo, snap.snapshot_id, message="my message")
348
349 result = runner.invoke(
350 cli, ["read-commit", commit.commit_id],
351 env=_repo_env(repo),
352 catch_exceptions=False,
353 )
354 assert result.exit_code == 0, result.output
355 data = json.loads(result.stdout)
356 assert data["commit_id"] == commit.commit_id
357 assert data["message"] == "my message"
358 assert data["snapshot_id"] == snap.snapshot_id
359
360 def test_missing_commit_errors(self, tmp_path: pathlib.Path) -> None:
361 repo = _init_repo(tmp_path)
362 result = runner.invoke(
363 cli, ["read-commit", "z" * 64],
364 env=_repo_env(repo),
365 )
366 assert result.exit_code == ExitCode.USER_ERROR
367 data = json.loads(result.stdout)
368 assert "error" in data
369
370
371 # ---------------------------------------------------------------------------
372 # read-snapshot
373 # ---------------------------------------------------------------------------
374
375
376 class TestReadSnapshot:
377 def test_reads_snapshot_json(self, tmp_path: pathlib.Path) -> None:
378 repo = _init_repo(tmp_path)
379 oid = _make_object(repo, b"snap data")
380 snap = _make_snapshot(repo, {"track.mid": oid})
381
382 result = runner.invoke(
383 cli, ["read-snapshot", snap.snapshot_id],
384 env=_repo_env(repo),
385 catch_exceptions=False,
386 )
387 assert result.exit_code == 0, result.output
388 data = json.loads(result.stdout)
389 assert data["snapshot_id"] == snap.snapshot_id
390 assert data["file_count"] == 1
391 assert "track.mid" in data["manifest"]
392
393 def test_missing_snapshot_errors(self, tmp_path: pathlib.Path) -> None:
394 repo = _init_repo(tmp_path)
395 result = runner.invoke(
396 cli, ["read-snapshot", "nothere"],
397 env=_repo_env(repo),
398 )
399 assert result.exit_code == ExitCode.USER_ERROR
400
401
402 # ---------------------------------------------------------------------------
403 # commit-tree
404 # ---------------------------------------------------------------------------
405
406
407 class TestCommitTree:
408 def test_creates_commit_from_snapshot(self, tmp_path: pathlib.Path) -> None:
409 repo = _init_repo(tmp_path)
410 oid = _make_object(repo, b"content")
411 snap = _make_snapshot(repo, {"file.txt": oid})
412
413 result = runner.invoke(
414 cli,
415 [
416 "commit-tree",
417 "--snapshot", snap.snapshot_id,
418 "--message", "plumbing commit",
419 "--author", "bot",
420 ],
421 env=_repo_env(repo),
422 catch_exceptions=False,
423 )
424 assert result.exit_code == 0, result.output
425 data = json.loads(result.stdout)
426 assert "commit_id" in data
427 assert len(data["commit_id"]) == 64
428
429 def test_with_parent(self, tmp_path: pathlib.Path) -> None:
430 repo = _init_repo(tmp_path)
431 oid = _make_object(repo, b"data")
432 snap1 = _make_snapshot(repo, {"a": oid})
433 parent = _make_commit(repo, snap1.snapshot_id, message="parent commit")
434
435 snap2 = _make_snapshot(repo, {"b": oid})
436 result = runner.invoke(
437 cli,
438 [
439 "commit-tree",
440 "--snapshot", snap2.snapshot_id,
441 "--parent", parent.commit_id,
442 "--message", "child",
443 ],
444 env=_repo_env(repo),
445 catch_exceptions=False,
446 )
447 assert result.exit_code == 0, result.output
448 data = json.loads(result.stdout)
449 assert "commit_id" in data
450
451 def test_missing_snapshot_errors(self, tmp_path: pathlib.Path) -> None:
452 repo = _init_repo(tmp_path)
453 result = runner.invoke(
454 cli,
455 ["commit-tree", "--snapshot", "nosuch"],
456 env=_repo_env(repo),
457 )
458 assert result.exit_code == ExitCode.USER_ERROR
459
460
461 # ---------------------------------------------------------------------------
462 # update-ref
463 # ---------------------------------------------------------------------------
464
465
466 class TestUpdateRef:
467 def test_creates_branch_ref(self, tmp_path: pathlib.Path) -> None:
468 repo = _init_repo(tmp_path)
469 oid = _make_object(repo, b"x")
470 snap = _make_snapshot(repo, {"x": oid})
471 commit = _make_commit(repo, snap.snapshot_id, message="create ref")
472
473 result = runner.invoke(
474 cli,
475 ["update-ref", "feature", commit.commit_id],
476 env=_repo_env(repo),
477 catch_exceptions=False,
478 )
479 assert result.exit_code == 0, result.output
480 data = json.loads(result.stdout)
481 assert data["branch"] == "feature"
482 assert data["commit_id"] == commit.commit_id
483 ref = repo / ".muse" / "refs" / "heads" / "feature"
484 assert ref.read_text() == commit.commit_id
485
486 def test_updates_existing_ref(self, tmp_path: pathlib.Path) -> None:
487 repo = _init_repo(tmp_path)
488 oid = _make_object(repo, b"y")
489 snap = _make_snapshot(repo, {"y": oid})
490 commit1 = _make_commit(repo, snap.snapshot_id, message="first")
491 commit2 = _make_commit(repo, snap.snapshot_id, message="second", parent_commit_id=commit1.commit_id)
492 _set_head(repo, "main", commit1.commit_id)
493
494 result = runner.invoke(
495 cli,
496 ["update-ref", "main", commit2.commit_id],
497 env=_repo_env(repo),
498 )
499 assert result.exit_code == 0, result.output
500 data = json.loads(result.stdout)
501 assert data["previous"] == commit1.commit_id
502 assert data["commit_id"] == commit2.commit_id
503
504 def test_delete_ref(self, tmp_path: pathlib.Path) -> None:
505 repo = _init_repo(tmp_path)
506 _set_head(repo, "todelete", "x" * 64)
507 result = runner.invoke(
508 cli,
509 ["update-ref", "--delete", "todelete"],
510 env=_repo_env(repo),
511 )
512 assert result.exit_code == 0, result.output
513 data = json.loads(result.stdout)
514 assert data["deleted"] is True
515 ref = repo / ".muse" / "refs" / "heads" / "todelete"
516 assert not ref.exists()
517
518 def test_verify_commit_not_found_errors(self, tmp_path: pathlib.Path) -> None:
519 repo = _init_repo(tmp_path)
520 result = runner.invoke(
521 cli,
522 ["update-ref", "main", "0" * 64],
523 env=_repo_env(repo),
524 )
525 assert result.exit_code == ExitCode.USER_ERROR
526
527 def test_no_verify_skips_commit_check(self, tmp_path: pathlib.Path) -> None:
528 repo = _init_repo(tmp_path)
529 result = runner.invoke(
530 cli,
531 ["update-ref", "--no-verify", "feature", "9" * 64],
532 env=_repo_env(repo),
533 )
534 assert result.exit_code == 0, result.output
535 ref = repo / ".muse" / "refs" / "heads" / "feature"
536 assert ref.read_text() == "9" * 64
537
538
539 # ---------------------------------------------------------------------------
540 # commit-graph
541 # ---------------------------------------------------------------------------
542
543
544 class TestCommitGraph:
545 def test_linear_graph(self, tmp_path: pathlib.Path) -> None:
546 repo = _init_repo(tmp_path)
547 oid = _make_object(repo, b"data")
548 snap = _make_snapshot(repo, {"f": oid})
549 c1 = _make_commit(repo, snap.snapshot_id, message="first")
550 c2 = _make_commit(repo, snap.snapshot_id, message="second", parent_commit_id=c1.commit_id)
551 _set_head(repo, "main", c2.commit_id)
552
553 result = runner.invoke(
554 cli, ["commit-graph"],
555 env=_repo_env(repo),
556 catch_exceptions=False,
557 )
558 assert result.exit_code == 0, result.output
559 data = json.loads(result.stdout)
560 assert data["count"] == 2
561 commit_ids = [c["commit_id"] for c in data["commits"]]
562 assert c2.commit_id in commit_ids
563 assert c1.commit_id in commit_ids
564
565 def test_text_format(self, tmp_path: pathlib.Path) -> None:
566 repo = _init_repo(tmp_path)
567 oid = _make_object(repo, b"data")
568 snap = _make_snapshot(repo, {"f": oid})
569 commit = _make_commit(repo, snap.snapshot_id, message="text format commit")
570 _set_head(repo, "main", commit.commit_id)
571
572 result = runner.invoke(
573 cli, ["commit-graph", "--format", "text"],
574 env=_repo_env(repo),
575 )
576 assert result.exit_code == 0, result.output
577 assert commit.commit_id in result.stdout
578
579 def test_explicit_tip(self, tmp_path: pathlib.Path) -> None:
580 repo = _init_repo(tmp_path)
581 oid = _make_object(repo, b"data")
582 snap = _make_snapshot(repo, {"f": oid})
583 commit = _make_commit(repo, snap.snapshot_id, message="explicit tip commit")
584
585 result = runner.invoke(
586 cli, ["commit-graph", "--tip", commit.commit_id],
587 env=_repo_env(repo),
588 )
589 assert result.exit_code == 0, result.output
590 data = json.loads(result.stdout)
591 assert data["tip"] == commit.commit_id
592
593 def test_no_commits_errors(self, tmp_path: pathlib.Path) -> None:
594 repo = _init_repo(tmp_path)
595 result = runner.invoke(
596 cli, ["commit-graph"],
597 env=_repo_env(repo),
598 )
599 assert result.exit_code == ExitCode.USER_ERROR
600
601
602 # ---------------------------------------------------------------------------
603 # pack-objects
604 # ---------------------------------------------------------------------------
605
606
607 class TestPackObjects:
608 def test_packs_head(self, tmp_path: pathlib.Path) -> None:
609 repo = _init_repo(tmp_path)
610 oid = _make_object(repo, b"pack me")
611 snap = _make_snapshot(repo, {"f.mid": oid})
612 commit = _make_commit(repo, snap.snapshot_id, message="pack head")
613 _set_head(repo, "main", commit.commit_id)
614
615 result = runner.invoke(
616 cli, ["pack-objects", "HEAD"],
617 env=_repo_env(repo),
618 catch_exceptions=False,
619 )
620 assert result.exit_code == 0, result.output
621 data = msgpack.unpackb(result.stdout_bytes, raw=False)
622 assert "commits" in data
623 assert len(data["commits"]) >= 1
624
625 def test_packs_explicit_commit(self, tmp_path: pathlib.Path) -> None:
626 repo = _init_repo(tmp_path)
627 oid = _make_object(repo, b"explicit")
628 snap = _make_snapshot(repo, {"g": oid})
629 commit = _make_commit(repo, snap.snapshot_id, message="pack explicit")
630
631 result = runner.invoke(
632 cli, ["pack-objects", commit.commit_id],
633 env=_repo_env(repo),
634 catch_exceptions=False,
635 )
636 assert result.exit_code == 0, result.output
637 data = msgpack.unpackb(result.stdout_bytes, raw=False)
638 commit_ids = [c["commit_id"] for c in data["commits"]]
639 assert commit.commit_id in commit_ids
640
641 def test_no_commits_on_head_errors(self, tmp_path: pathlib.Path) -> None:
642 repo = _init_repo(tmp_path)
643 result = runner.invoke(
644 cli, ["pack-objects", "HEAD"],
645 env=_repo_env(repo),
646 )
647 assert result.exit_code == ExitCode.USER_ERROR
648
649
650 # ---------------------------------------------------------------------------
651 # unpack-objects
652 # ---------------------------------------------------------------------------
653
654
655 class TestUnpackObjects:
656 def test_unpacks_valid_bundle(self, tmp_path: pathlib.Path) -> None:
657 source = _init_repo(tmp_path / "src")
658 dest = _init_repo(tmp_path / "dst")
659
660 oid = _make_object(source, b"unpack me")
661 snap = _make_snapshot(source, {"h.mid": oid})
662 commit = _make_commit(source, snap.snapshot_id, message="unpack bundle")
663
664 bundle = build_pack(source, [commit.commit_id])
665 bundle_bytes = msgpack.packb(bundle, use_bin_type=True)
666
667 result = runner.invoke(
668 cli,
669 ["unpack-objects"],
670 input=bundle_bytes,
671 env=_repo_env(dest),
672 catch_exceptions=False,
673 )
674 assert result.exit_code == 0, result.output
675 data = json.loads(result.stdout)
676 assert "objects_written" in data
677 assert data["commits_written"] == 1
678 assert data["objects_written"] == 1
679
680 def test_invalid_msgpack_errors(self, tmp_path: pathlib.Path) -> None:
681 repo = _init_repo(tmp_path)
682 result = runner.invoke(
683 cli,
684 ["unpack-objects"],
685 input=b"\xff\xff NOT VALID MSGPACK",
686 env=_repo_env(repo),
687 )
688 assert result.exit_code == ExitCode.USER_ERROR
689
690 def test_idempotent_unpack(self, tmp_path: pathlib.Path) -> None:
691 repo = _init_repo(tmp_path)
692 oid = _make_object(repo, b"idempotent")
693 snap = _make_snapshot(repo, {"i.txt": oid})
694 commit = _make_commit(repo, snap.snapshot_id, message="idempotent unpack")
695
696 bundle = build_pack(repo, [commit.commit_id])
697 bundle_bytes = msgpack.packb(bundle, use_bin_type=True)
698
699 result1 = runner.invoke(
700 cli, ["unpack-objects"],
701 input=bundle_bytes,
702 env=_repo_env(repo),
703 )
704 assert result1.exit_code == 0, result1.output
705
706 result2 = runner.invoke(
707 cli, ["unpack-objects"],
708 input=bundle_bytes,
709 env=_repo_env(repo),
710 )
711 assert result2.exit_code == 0, result2.output
712 data = json.loads(result2.stdout)
713 assert data["objects_written"] == 0
714 assert data["objects_skipped"] == 1
715
716
717 # ---------------------------------------------------------------------------
718 # ls-remote (moved to plumbing namespace)
719 # ---------------------------------------------------------------------------
720
721
722 class TestLsRemote:
723 def test_bare_url_transport_error(self) -> None:
724 """Bare URL to a non-existent server produces exit code INTERNAL_ERROR."""
725 result = runner.invoke(
726 cli,
727 ["ls-remote", "https://localhost:0/no-such-server"],
728 )
729 assert result.exit_code == ExitCode.INTERNAL_ERROR
730
731 def test_non_url_non_remote_errors(self, tmp_path: pathlib.Path) -> None:
732 """A non-URL, non-configured remote name exits with code USER_ERROR."""
733 repo = _init_repo(tmp_path)
734 result = runner.invoke(
735 cli,
736 ["ls-remote", "not-a-url-or-remote"],
737 env=_repo_env(repo),
738 )
739 assert result.exit_code == ExitCode.USER_ERROR