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