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