gabriel / muse public
test_cli_plumbing.py python
745 lines 25.7 KB
86000da9 fix: replace typer CliRunner with argparse-compatible test helper Gabriel Cardona <gabriel@tellurstori.com> 1d 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 tests.cli_test_helper import CliRunner
19
20 cli = None # argparse migration — CliRunner ignores this arg
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