gabriel / muse public
test_cmd_shelf.py python
1,735 lines 70.0 KB
Raw
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 3 days ago
1 """Comprehensive tests for ``muse shelf``.
2
3 Covers:
4 - Unit: _load_shelf / _save_shelf atomic write + guards, _resolve_entry,
5 _compute_shelf_id, _generate_name, _apply_shelf_snapshot,
6 _verify_snapshot_objects
7 - Integration: save, list, read, apply, pop, drop, diff — JSON schemas,
8 text output, filters, agent fields
9 - End-to-end: full CLI round-trips via CliRunner
10 - Stress: many entries, concurrent isolated repos, repeated save/load
11 - Data integrity: already-current detection, snapshot completeness,
12 content-address stability
13 - Performance: save + pop under 5 s
14 - Security: symlink guard, size limit, ANSI injection in names / intent,
15 invalid --format exits 1
16
17 Test categories
18 ---------------
19 - unit : pure helper functions, no repo needed
20 - integration : programmatic API + JSON schema validation
21 - e2e : CliRunner full round-trips
22 - stress : volume and concurrency
23 - data-integrity: already-current detection, manifest correctness
24 - performance : timing assertions
25 - security : injection, path-traversal, oversized file guards
26 - docstrings : public API coverage
27 """
28
29 from __future__ import annotations
30 from collections.abc import Mapping
31
32 import argparse
33 import datetime
34 import inspect
35 import json
36 import os
37 import pathlib
38 import threading
39 import time
40 from typing import Any
41
42 import pytest
43 from tests.cli_test_helper import CliRunner
44 from muse.core.types import long_id, fake_id, split_id, blob_id
45 from muse.core.object_store import object_path
46 from muse.core.paths import muse_dir, ref_path, shelf_dir
47
48 cli = None # argparse migration — CliRunner ignores this arg
49 runner = CliRunner()
50
51
52 # ---------------------------------------------------------------------------
53 # Shared helpers
54 # ---------------------------------------------------------------------------
55
56
57 def _env(root: pathlib.Path) -> Mapping[str, str]:
58 return {"MUSE_REPO_ROOT": str(root)}
59
60
61 def _init_repo(tmp_path: pathlib.Path, branch: str = "main") -> tuple[pathlib.Path, str]:
62 """Create a minimal Muse repo structure on disk."""
63 dot_muse = muse_dir(tmp_path)
64 dot_muse.mkdir()
65 repo_id = fake_id("repo")
66 (dot_muse / "repo.json").write_text(json.dumps({
67 "repo_id": repo_id,
68 "domain": "code",
69 "default_branch": branch,
70 "created_at": "2025-01-01T00:00:00+00:00",
71 }), encoding="utf-8")
72 (dot_muse / "HEAD").write_text(f"ref: refs/heads/{branch}", encoding="utf-8")
73 (dot_muse / "refs" / "heads").mkdir(parents=True)
74 (dot_muse / "snapshots").mkdir()
75 (dot_muse / "commits").mkdir()
76 (dot_muse / "objects").mkdir()
77 return tmp_path, repo_id
78
79
80 def _make_commit(
81 root: pathlib.Path,
82 repo_id: str,
83 message: str = "init",
84 branch: str = "main",
85 manifest: dict[str, str] | None = None,
86 ) -> str:
87 """Write a commit to the repo with the given manifest."""
88 from muse.core.commits import (
89 CommitRecord,
90 write_commit,
91 )
92 from muse.core.snapshots import (
93 SnapshotRecord,
94 write_snapshot,
95 )
96 from muse.core.ids import hash_snapshot, hash_commit
97
98 ref_file = ref_path(root, branch)
99 parent_id = ref_file.read_text().strip() if ref_file.exists() else None
100 m: dict[str, str] = manifest or {}
101 snap_id = hash_snapshot(m)
102 committed_at = datetime.datetime.now(datetime.timezone.utc)
103 commit_id = hash_commit( parent_ids=[parent_id] if parent_id else [],
104 snapshot_id=snap_id, message=message,
105 committed_at_iso=committed_at.isoformat(),
106 )
107 write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=m))
108 write_commit(root, CommitRecord(
109 commit_id=commit_id, branch=branch,
110 snapshot_id=snap_id, message=message, committed_at=committed_at,
111 parent_commit_id=parent_id,
112 ))
113 ref_file.parent.mkdir(parents=True, exist_ok=True)
114 ref_file.write_text(commit_id, encoding="utf-8")
115 return commit_id
116
117
118 def _write_object(root: pathlib.Path, content: bytes) -> str:
119 """Write content to the object store, returning the sha256:-prefixed ID."""
120 from muse.core.object_store import write_object
121 obj_id = blob_id(content)
122 write_object(root, obj_id, content)
123 return obj_id
124
125
126 def _make_shelf_entry(
127 name: str = "dev/000",
128 branch: str = "main",
129 snapshot: dict[str, str] | None = None,
130 deleted: list[str] | None = None,
131 intent_type: str = "checkpoint",
132 intent: str | None = None,
133 resumable: bool = False,
134 tags: list[str] | None = None,
135 created_by: str = "human",
136 ) -> Mapping[str, object]:
137 """Build a raw shelf-entry dict (no id field) suitable for _compute_shelf_id."""
138 return {
139 "name": name,
140 "snapshot": snapshot or {},
141 "deleted": deleted or [],
142 "snapshot_id": long_id("a" * 64),
143 "parent_commit": long_id("b" * 64),
144 "branch": branch,
145 "created_at": "2025-01-01T00:00:00+00:00",
146 "created_by": created_by,
147 "intent_type": intent_type,
148 "intent": intent,
149 "resumable": resumable,
150 "tags": tags or [],
151 "expires_at": None,
152 "domain_state": {},
153 }
154
155
156 # ---------------------------------------------------------------------------
157 # Fixtures
158 # ---------------------------------------------------------------------------
159
160
161 @pytest.fixture()
162 def repo(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
163 """Fresh repo with one committed file (a.py) and one dirty file (b.py)."""
164 monkeypatch.chdir(tmp_path)
165 monkeypatch.setenv("MUSE_REPO_ROOT", str(tmp_path))
166 r = runner.invoke(cli, ["init"], env=_env(tmp_path), catch_exceptions=False)
167 assert r.exit_code == 0, r.output
168 (tmp_path / "a.py").write_text("x = 1\n")
169 r = runner.invoke(cli, ["commit", "-m", "base"], env=_env(tmp_path), catch_exceptions=False)
170 assert r.exit_code == 0, r.output
171 (tmp_path / "b.py").write_text("y = 2\n")
172 return tmp_path
173
174
175 @pytest.fixture()
176 def shelved_repo(repo: pathlib.Path) -> pathlib.Path:
177 """repo fixture with one shelf entry already saved."""
178 r = runner.invoke(
179 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
180 )
181 assert r.exit_code == 0, r.output
182 return repo
183
184
185 # ---------------------------------------------------------------------------
186 # Unit — _compute_shelf_id
187 # ---------------------------------------------------------------------------
188
189
190 class TestComputeShelfId:
191 """Unit tests for content-addressed ID generation."""
192
193 def test_id_starts_with_sha256(self) -> None:
194 from muse.cli.commands.shelf import _compute_shelf_id
195 entry = _make_shelf_entry()
196 shelf_id = _compute_shelf_id(entry)
197 assert shelf_id.startswith("sha256:")
198
199 def test_id_is_64_hex_after_prefix(self) -> None:
200 from muse.cli.commands.shelf import _compute_shelf_id
201 entry = _make_shelf_entry()
202 shelf_id = _compute_shelf_id(entry)
203 _, hex_part = split_id(shelf_id)
204 assert len(hex_part) == 64
205 assert all(c in "0123456789abcdef" for c in hex_part)
206
207 def test_same_content_same_id(self) -> None:
208 from muse.cli.commands.shelf import _compute_shelf_id
209 e1 = _make_shelf_entry(name="mywork", branch="dev")
210 e2 = _make_shelf_entry(name="mywork", branch="dev")
211 assert _compute_shelf_id(e1) == _compute_shelf_id(e2)
212
213 def test_different_content_different_id(self) -> None:
214 from muse.cli.commands.shelf import _compute_shelf_id
215 e1 = _make_shelf_entry(name="mywork")
216 e2 = _make_shelf_entry(name="otherwork")
217 assert _compute_shelf_id(e1) != _compute_shelf_id(e2)
218
219 def test_snapshot_diff_changes_id(self) -> None:
220 from muse.cli.commands.shelf import _compute_shelf_id
221 e1 = _make_shelf_entry(snapshot={"a.py": long_id("a" * 64)})
222 e2 = _make_shelf_entry(snapshot={"a.py": long_id("b" * 64)})
223 assert _compute_shelf_id(e1) != _compute_shelf_id(e2)
224
225 def test_id_stable_across_calls(self) -> None:
226 from muse.cli.commands.shelf import _compute_shelf_id
227 entry = _make_shelf_entry(name="stable", intent="doing work")
228 ids = [_compute_shelf_id(entry) for _ in range(10)]
229 assert len(set(ids)) == 1
230
231
232 # ---------------------------------------------------------------------------
233 # Unit — _generate_name
234 # ---------------------------------------------------------------------------
235
236
237 class TestGenerateName:
238 def test_first_entry_is_000(self) -> None:
239 from muse.cli.commands.shelf import _generate_name
240 assert _generate_name("dev", set()) == "dev/000"
241
242 def test_increments_when_conflict(self) -> None:
243 from muse.cli.commands.shelf import _generate_name
244 existing = {"dev/000", "dev/001"}
245 assert _generate_name("dev", existing) == "dev/002"
246
247 def test_branch_with_special_chars_sanitized(self) -> None:
248 from muse.cli.commands.shelf import _generate_name
249 name = _generate_name("feat/[email protected]!", set())
250 assert "/" in name # one slash is OK (branch/NNN)
251 assert "@" not in name
252 assert "!" not in name
253
254 def test_empty_branch_fallback(self) -> None:
255 from muse.cli.commands.shelf import _generate_name
256 name = _generate_name("", set())
257 assert name.endswith("/000")
258
259 def test_zero_padded_to_three_digits(self) -> None:
260 from muse.cli.commands.shelf import _generate_name
261 name = _generate_name("main", set())
262 assert name.endswith("/000")
263
264 def test_large_n_zero_padded(self) -> None:
265 from muse.cli.commands.shelf import _generate_name
266 existing = {f"main/{i:03d}" for i in range(10)}
267 name = _generate_name("main", existing)
268 assert name == "main/010"
269
270
271 # ---------------------------------------------------------------------------
272 # Unit — _load_shelf / _save_shelf
273 # ---------------------------------------------------------------------------
274
275
276 class TestLoadSaveShelf:
277 def test_load_empty_when_no_file(self, tmp_path: pathlib.Path) -> None:
278 root, _ = _init_repo(tmp_path)
279 from muse.cli.commands.shelf import _load_shelf
280 assert _load_shelf(root) == []
281
282 def test_save_creates_entry_file(self, tmp_path: pathlib.Path) -> None:
283 root, _ = _init_repo(tmp_path)
284 from muse.cli.commands.shelf import _load_shelf, ShelfEntry
285 from muse.cli.commands.shelf import _compute_shelf_id
286 from muse.core.shelf import write_shelf_entry
287 raw = _make_shelf_entry(name="test/000")
288 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
289 write_shelf_entry(root, entry)
290 assert (shelf_dir(root) / "sha256").is_dir()
291 loaded = _load_shelf(root)
292 assert len(loaded) == 1
293 assert loaded[0]["name"] == "test/000"
294
295 def test_roundtrip_preserves_all_fields(self, tmp_path: pathlib.Path) -> None:
296 root, _ = _init_repo(tmp_path)
297 from muse.cli.commands.shelf import _load_shelf, ShelfEntry
298 from muse.cli.commands.shelf import _compute_shelf_id
299 from muse.core.shelf import write_shelf_entry
300 raw = _make_shelf_entry(
301 name="wip/000",
302 branch="dev",
303 snapshot={"src/foo.py": long_id("c" * 64)},
304 deleted=["old.py"],
305 intent_type="handoff",
306 intent="50% done",
307 resumable=True,
308 tags=["auth", "refactor"],
309 created_by="agent-42",
310 )
311 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
312 write_shelf_entry(root, entry)
313 loaded = _load_shelf(root)
314 e = loaded[0]
315 assert e["name"] == "wip/000"
316 assert e["branch"] == "dev"
317 assert e["snapshot"] == {"src/foo.py": long_id("c" * 64)}
318 assert e["deleted"] == ["old.py"]
319 assert e["intent_type"] == "handoff"
320 assert e["intent"] == "50% done"
321 assert e["resumable"] is True
322 assert e["tags"] == ["auth", "refactor"]
323 assert e["created_by"] == "agent-42"
324
325 def test_save_is_atomic_no_temp_files(self, tmp_path: pathlib.Path) -> None:
326 root, _ = _init_repo(tmp_path)
327 from muse.core.shelf import write_shelf_entry
328 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id
329 raw = _make_shelf_entry(name="main/000")
330 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
331 write_shelf_entry(root, entry)
332 tmp_files = list((shelf_dir(root) / "sha256").glob(".muse-tmp-*"))
333 assert tmp_files == []
334
335 def test_load_ignores_oversized_file(self, tmp_path: pathlib.Path) -> None:
336 root, _ = _init_repo(tmp_path)
337 shelf_path = muse_dir(root) / "shelf.json"
338 shelf_path.write_bytes(b"x" * (65 * 1024 * 1024)) # 65 MiB > 64 MiB limit
339 from muse.cli.commands.shelf import _load_shelf
340 assert _load_shelf(root) == []
341
342 def test_load_ignores_malformed_json(self, tmp_path: pathlib.Path) -> None:
343 root, _ = _init_repo(tmp_path)
344 (muse_dir(root) / "shelf.json").write_text("not-json-at-all", encoding="utf-8")
345 from muse.cli.commands.shelf import _load_shelf
346 assert _load_shelf(root) == []
347
348 def test_load_ignores_non_list_json(self, tmp_path: pathlib.Path) -> None:
349 root, _ = _init_repo(tmp_path)
350 (muse_dir(root) / "shelf.json").write_text(json.dumps({"key": "val"}), encoding="utf-8")
351 from muse.cli.commands.shelf import _load_shelf
352 assert _load_shelf(root) == []
353
354 def test_load_skips_entries_without_snapshot(self, tmp_path: pathlib.Path) -> None:
355 root, _ = _init_repo(tmp_path)
356 (muse_dir(root) / "shelf.json").write_text(
357 json.dumps([{"name": "bad", "deleted": []}]), # no snapshot key
358 encoding="utf-8",
359 )
360 from muse.cli.commands.shelf import _load_shelf
361 assert _load_shelf(root) == []
362
363 def test_load_skips_non_dict_entries(self, tmp_path: pathlib.Path) -> None:
364 root, _ = _init_repo(tmp_path)
365 (muse_dir(root) / "shelf.json").write_text(
366 json.dumps(["string", 42, None]),
367 encoding="utf-8",
368 )
369 from muse.cli.commands.shelf import _load_shelf
370 assert _load_shelf(root) == []
371
372 def test_fsync_called_in_write(self) -> None:
373 """write_shelf_entry in shelf.py must use fsync/F_BARRIERFSYNC for durability."""
374 from muse.core.shelf import write_shelf_entry
375 src = inspect.getsource(write_shelf_entry)
376 # The actual fsync is in _write_shelf_header_atomic which write_shelf_entry calls.
377 assert "_write_shelf_header_atomic" in src
378
379 def test_symlink_guard_in_write(self) -> None:
380 """write_shelf_entry must reject a symlinked .muse/shelf/ directory."""
381 from muse.core.shelf import write_shelf_entry
382 src = inspect.getsource(write_shelf_entry)
383 assert "symlink" in src.lower()
384
385 def test_multiple_entries_sorted_newest_first(self, tmp_path: pathlib.Path) -> None:
386 root, _ = _init_repo(tmp_path)
387 from muse.cli.commands.shelf import _load_shelf, ShelfEntry
388 from muse.cli.commands.shelf import _compute_shelf_id
389 from muse.core.shelf import write_shelf_entry
390 for i in range(3):
391 raw = _make_shelf_entry(name=f"dev/{i:03d}")
392 raw["created_at"] = f"2025-01-0{i+1}T00:00:00+00:00"
393 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
394 write_shelf_entry(root, entry)
395 loaded = _load_shelf(root)
396 # Newest (2025-01-03) first.
397 assert [e["name"] for e in loaded] == ["dev/002", "dev/001", "dev/000"]
398
399
400 # ---------------------------------------------------------------------------
401 # Unit — _resolve_entry
402 # ---------------------------------------------------------------------------
403
404
405 class TestResolveEntry:
406 def _entries(self, names: list[str]) -> list["ShelfEntry"]:
407 from muse.cli.commands.shelf import ShelfEntry
408 from muse.cli.commands.shelf import _compute_shelf_id
409 result = []
410 for name in names:
411 raw = _make_shelf_entry(name=name)
412 result.append(ShelfEntry(id=_compute_shelf_id(raw), **raw)) # type: ignore[misc]
413 return result
414
415 def test_none_returns_default_0(self) -> None:
416 from muse.cli.commands.shelf import _resolve_entry
417 entries = self._entries(["alpha", "beta", "gamma"])
418 idx, e = _resolve_entry(entries, None)
419 assert idx == 0
420 assert e["name"] == "alpha"
421
422 def test_integer_string_resolves(self) -> None:
423 from muse.cli.commands.shelf import _resolve_entry
424 entries = self._entries(["alpha", "beta", "gamma"])
425 idx, e = _resolve_entry(entries, "2")
426 assert idx == 2
427 assert e["name"] == "gamma"
428
429 def test_name_lookup_exact(self) -> None:
430 from muse.cli.commands.shelf import _resolve_entry
431 entries = self._entries(["alpha", "beta", "gamma"])
432 idx, e = _resolve_entry(entries, "beta")
433 assert idx == 1
434 assert e["name"] == "beta"
435
436 def test_empty_list_raises(self) -> None:
437 from muse.cli.commands.shelf import _resolve_entry
438 with pytest.raises(ValueError, match="No shelf entries"):
439 _resolve_entry([], None)
440
441 def test_out_of_range_raises(self) -> None:
442 from muse.cli.commands.shelf import _resolve_entry
443 entries = self._entries(["alpha"])
444 with pytest.raises(ValueError, match="out of range"):
445 _resolve_entry(entries, "5")
446
447 def test_negative_index_raises(self) -> None:
448 from muse.cli.commands.shelf import _resolve_entry
449 entries = self._entries(["alpha", "beta"])
450 with pytest.raises(ValueError, match="out of range"):
451 _resolve_entry(entries, "-1")
452
453 def test_unknown_name_raises(self) -> None:
454 from muse.cli.commands.shelf import _resolve_entry
455 entries = self._entries(["alpha"])
456 with pytest.raises(ValueError, match="No shelf entry"):
457 _resolve_entry(entries, "nonexistent")
458
459
460 # ---------------------------------------------------------------------------
461 # Unit — _apply_shelf_snapshot / _verify_snapshot_objects
462 # ---------------------------------------------------------------------------
463
464
465 class TestApplyShelfSnapshot:
466 def test_restored_count_correct(self, tmp_path: pathlib.Path) -> None:
467 root, repo_id = _init_repo(tmp_path)
468 obj_id = _write_object(root, b"hello world")
469 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id, _apply_shelf_snapshot
470 raw = _make_shelf_entry(snapshot={"src/foo.py": obj_id})
471 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
472 counts = _apply_shelf_snapshot(root, entry, head_manifest={})
473 assert counts["restored"] == 1
474 assert counts["already_current"] == 0
475 assert (root / "src" / "foo.py").read_bytes() == b"hello world"
476
477 def test_already_current_not_rewritten(self, tmp_path: pathlib.Path) -> None:
478 root, _ = _init_repo(tmp_path)
479 obj_id = _write_object(root, b"same content")
480 (tmp_path / "file.py").write_bytes(b"same content")
481 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id, _apply_shelf_snapshot
482 raw = _make_shelf_entry(snapshot={"file.py": obj_id})
483 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
484 # HEAD manifest already has the same object for this path
485 counts = _apply_shelf_snapshot(root, entry, head_manifest={"file.py": obj_id})
486 assert counts["restored"] == 0
487 assert counts["already_current"] == 1
488
489 def test_deleted_paths_removed(self, tmp_path: pathlib.Path) -> None:
490 root, _ = _init_repo(tmp_path)
491 (tmp_path / "gone.py").write_text("old\n")
492 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id, _apply_shelf_snapshot
493 raw = _make_shelf_entry(snapshot={}, deleted=["gone.py"])
494 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
495 counts = _apply_shelf_snapshot(root, entry, head_manifest={})
496 assert counts["deleted"] == 1
497 assert not (tmp_path / "gone.py").exists()
498
499 def test_deleted_already_gone_is_idempotent(self, tmp_path: pathlib.Path) -> None:
500 root, _ = _init_repo(tmp_path)
501 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id, _apply_shelf_snapshot
502 raw = _make_shelf_entry(snapshot={}, deleted=["nonexistent.py"])
503 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
504 counts = _apply_shelf_snapshot(root, entry, head_manifest={})
505 assert counts["deleted"] == 0
506
507 def test_mixed_restored_and_already_current(self, tmp_path: pathlib.Path) -> None:
508 root, _ = _init_repo(tmp_path)
509 obj_same = _write_object(root, b"same")
510 obj_diff = _write_object(root, b"different")
511 # a.py already has the shelf content on disk → already_current
512 (root / "a.py").write_bytes(b"same")
513 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id, _apply_shelf_snapshot
514 raw = _make_shelf_entry(snapshot={"a.py": obj_same, "b.py": obj_diff})
515 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
516 counts = _apply_shelf_snapshot(root, entry, head_manifest={"a.py": obj_same})
517 assert counts["restored"] == 1
518 assert counts["already_current"] == 1
519
520
521 class TestVerifySnapshotObjects:
522 def test_all_present_returns_empty(self, tmp_path: pathlib.Path) -> None:
523 root, _ = _init_repo(tmp_path)
524 obj_id = _write_object(root, b"data")
525 from muse.cli.commands.shelf import _verify_snapshot_objects
526 missing = _verify_snapshot_objects(root, {"file.py": obj_id})
527 assert missing == []
528
529 def test_missing_object_returned(self, tmp_path: pathlib.Path) -> None:
530 root, _ = _init_repo(tmp_path)
531 from muse.cli.commands.shelf import _verify_snapshot_objects
532 missing_obj_id = long_id("f" * 64)
533 missing = _verify_snapshot_objects(root, {"file.py": missing_obj_id})
534 assert "file.py" in missing
535
536 def test_empty_snapshot_returns_empty(self, tmp_path: pathlib.Path) -> None:
537 root, _ = _init_repo(tmp_path)
538 from muse.cli.commands.shelf import _verify_snapshot_objects
539 assert _verify_snapshot_objects(root, {}) == []
540
541
542 # ---------------------------------------------------------------------------
543 # Unit — register / parser flags
544 # ---------------------------------------------------------------------------
545
546
547 class TestRegisterFlags:
548 def _parse(self, *args: str) -> argparse.Namespace:
549 import muse.cli.commands.shelf as m
550 p = argparse.ArgumentParser()
551 sub = p.add_subparsers()
552 m.register(sub)
553 return p.parse_args(["shelf", *args])
554
555 def test_save_intent_short(self) -> None:
556 ns = self._parse("save", "-m", "WIP auth")
557 assert ns.intent == "WIP auth"
558
559 def test_save_intent_long(self) -> None:
560 ns = self._parse("save", "--intent", "WIP auth")
561 assert ns.intent == "WIP auth"
562
563 def test_save_intent_default_none(self) -> None:
564 ns = self._parse("save")
565 assert ns.intent is None
566
567 def test_save_intent_type_default(self) -> None:
568 ns = self._parse("save")
569 assert ns.intent_type == "checkpoint"
570
571 def test_save_intent_type_handoff(self) -> None:
572 ns = self._parse("save", "--intent-type", "handoff")
573 assert ns.intent_type == "handoff"
574
575 def test_save_resumable_flag(self) -> None:
576 ns = self._parse("save", "--resumable")
577 assert ns.resumable is True
578
579 def test_save_resumable_default_false(self) -> None:
580 ns = self._parse("save")
581 assert ns.resumable is False
582
583 def test_save_tag_repeatable(self) -> None:
584 ns = self._parse("save", "--tag", "auth", "--tag", "refactor")
585 assert "auth" in ns.tags
586 assert "refactor" in ns.tags
587
588 def test_save_json_shorthand(self) -> None:
589 ns = self._parse("save", "--json")
590 assert ns.json_out is True
591
592 def test_pop_entry_arg(self) -> None:
593 ns = self._parse("pop", "my-work")
594 assert ns.entry == "my-work"
595
596 def test_pop_entry_default_none(self) -> None:
597 ns = self._parse("pop")
598 assert ns.entry is None
599
600 def test_drop_entry_arg(self) -> None:
601 ns = self._parse("drop", "2")
602 assert ns.entry == "2"
603
604 def test_apply_entry_arg(self) -> None:
605 ns = self._parse("apply", "main/000")
606 assert ns.entry == "main/000"
607
608 def test_list_branch_filter(self) -> None:
609 ns = self._parse("list", "--branch", "dev")
610 assert ns.branch == "dev"
611
612 def test_list_resumable_filter(self) -> None:
613 ns = self._parse("list", "--resumable")
614 assert ns.resumable is True
615
616 def test_list_by_filter(self) -> None:
617 ns = self._parse("list", "--by", "agent-42")
618 assert ns.created_by == "agent-42"
619
620 def test_diff_entry_arg(self) -> None:
621 ns = self._parse("diff", "0")
622 assert ns.entry == "0"
623
624
625 # ---------------------------------------------------------------------------
626 # Integration — save JSON schema
627 # ---------------------------------------------------------------------------
628
629
630 class TestSaveJsonSchema:
631 _REQUIRED = {
632 "status", "id", "name", "snapshot_id", "parent_commit", "branch",
633 "created_at", "created_by", "intent_type", "intent", "resumable",
634 "tags", "files_count", "shelf_size",
635 }
636
637 def test_schema_complete(self, repo: pathlib.Path) -> None:
638 r = runner.invoke(
639 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
640 )
641 assert r.exit_code == 0, r.output
642 d = json.loads(r.output)
643 assert self._REQUIRED <= d.keys()
644
645 def test_status_shelved(self, repo: pathlib.Path) -> None:
646 r = runner.invoke(
647 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
648 )
649 assert json.loads(r.output)["status"] == "shelved"
650
651 def test_id_is_sha256(self, repo: pathlib.Path) -> None:
652 r = runner.invoke(
653 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
654 )
655 d = json.loads(r.output)
656 assert d["id"].startswith("sha256:")
657
658 def test_files_count_positive(self, repo: pathlib.Path) -> None:
659 r = runner.invoke(
660 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
661 )
662 assert json.loads(r.output)["files_count"] > 0
663
664 def test_intent_default_null(self, repo: pathlib.Path) -> None:
665 r = runner.invoke(
666 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
667 )
668 assert json.loads(r.output)["intent"] is None
669
670 def test_intent_with_flag(self, repo: pathlib.Path) -> None:
671 r = runner.invoke(
672 cli, ["shelf", "save", "-m", "updating tests", "--json"],
673 env=_env(repo), catch_exceptions=False,
674 )
675 assert json.loads(r.output)["intent"] == "updating tests"
676
677 def test_intent_type_default_checkpoint(self, repo: pathlib.Path) -> None:
678 r = runner.invoke(
679 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
680 )
681 assert json.loads(r.output)["intent_type"] == "checkpoint"
682
683 def test_intent_type_custom(self, repo: pathlib.Path) -> None:
684 r = runner.invoke(
685 cli, ["shelf", "save", "--intent-type", "handoff", "--json"],
686 env=_env(repo), catch_exceptions=False,
687 )
688 assert json.loads(r.output)["intent_type"] == "handoff"
689
690 def test_resumable_flag_stored(self, repo: pathlib.Path) -> None:
691 r = runner.invoke(
692 cli, ["shelf", "save", "--resumable", "--json"],
693 env=_env(repo), catch_exceptions=False,
694 )
695 assert json.loads(r.output)["resumable"] is True
696
697 def test_tags_stored(self, repo: pathlib.Path) -> None:
698 r = runner.invoke(
699 cli, ["shelf", "save", "--tag", "auth", "--tag", "wip", "--json"],
700 env=_env(repo), catch_exceptions=False,
701 )
702 d = json.loads(r.output)
703 assert "auth" in d["tags"]
704 assert "wip" in d["tags"]
705
706 def test_nothing_to_shelf_schema_complete(self, repo: pathlib.Path) -> None:
707 """nothing_to_shelf must emit same keys with null id/name."""
708 (repo / "b.py").unlink(missing_ok=True) # make tree match HEAD
709 r = runner.invoke(
710 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
711 )
712 d = json.loads(r.output)
713 assert self._REQUIRED <= d.keys()
714 assert d["status"] == "nothing_to_shelf"
715 assert d["id"] is None
716 assert d["name"] is None
717
718 def test_named_save(self, repo: pathlib.Path) -> None:
719 r = runner.invoke(
720 cli, ["shelf", "save", "my-feature", "--json"],
721 env=_env(repo), catch_exceptions=False,
722 )
723 assert json.loads(r.output)["name"] == "my-feature"
724
725 def test_duplicate_name_exits_1(self, repo: pathlib.Path) -> None:
726 runner.invoke(
727 cli, ["shelf", "save", "dup-test"], env=_env(repo), catch_exceptions=False
728 )
729 # Write another dirty file so there's something to shelf
730 (repo / "c.py").write_text("z = 3\n")
731 r = runner.invoke(cli, ["shelf", "save", "dup-test"], env=_env(repo))
732 assert r.exit_code == 1
733
734 def test_shelf_size_increments(self, repo: pathlib.Path) -> None:
735 r1 = runner.invoke(
736 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
737 )
738 d1 = json.loads(r1.output)
739 (repo / "c.py").write_text("z = 3\n")
740 r2 = runner.invoke(
741 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
742 )
743 d2 = json.loads(r2.output)
744 assert d2["shelf_size"] == d1["shelf_size"] + 1
745
746
747 # ---------------------------------------------------------------------------
748 # Integration — list JSON schema
749 # ---------------------------------------------------------------------------
750
751
752 class TestListJsonSchema:
753 _ENTRY_REQUIRED = {
754 "index", "id", "name", "snapshot_id", "branch", "created_at",
755 "created_by", "intent_type", "intent", "resumable", "tags", "files_count",
756 }
757
758 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
759 r = runner.invoke(
760 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
761 )
762 assert r.exit_code == 0, r.output
763 entries = json.loads(r.output)["entries"]
764 assert len(entries) >= 1
765 assert self._ENTRY_REQUIRED <= entries[0].keys()
766
767 def test_empty_returns_empty_array(self, repo: pathlib.Path) -> None:
768 r = runner.invoke(
769 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
770 )
771 assert r.exit_code == 0
772 assert json.loads(r.output)["entries"] == []
773
774 def test_files_count_positive(self, shelved_repo: pathlib.Path) -> None:
775 r = runner.invoke(
776 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
777 )
778 entries = json.loads(r.output)["entries"]
779 assert entries[0]["files_count"] > 0
780
781 def test_filter_branch(self, repo: pathlib.Path) -> None:
782 runner.invoke(
783 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
784 )
785 r = runner.invoke(
786 cli, ["shelf", "list", "--branch", "main", "--json"], env=_env(repo)
787 )
788 entries = json.loads(r.output)["entries"]
789 assert all(e["branch"] == "main" for e in entries)
790
791 def test_filter_branch_no_match_empty(self, shelved_repo: pathlib.Path) -> None:
792 r = runner.invoke(
793 cli, ["shelf", "list", "--branch", "nonexistent-branch", "--json"],
794 env=_env(shelved_repo),
795 )
796 assert json.loads(r.output)["entries"] == []
797
798 def test_filter_resumable(self, repo: pathlib.Path) -> None:
799 runner.invoke(
800 cli, ["shelf", "save", "--resumable", "--json"],
801 env=_env(repo), catch_exceptions=False,
802 )
803 (repo / "c.py").write_text("z = 3\n")
804 runner.invoke(
805 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
806 )
807 r = runner.invoke(
808 cli, ["shelf", "list", "--resumable", "--json"], env=_env(repo)
809 )
810 entries = json.loads(r.output)["entries"]
811 assert all(e["resumable"] for e in entries)
812
813 def test_filter_by_creator(self, repo: pathlib.Path) -> None:
814 runner.invoke(
815 cli, ["shelf", "save", "--by", "agent-99", "--json"],
816 env=_env(repo), catch_exceptions=False,
817 )
818 r = runner.invoke(
819 cli, ["shelf", "list", "--by", "agent-99", "--json"], env=_env(repo)
820 )
821 entries = json.loads(r.output)["entries"]
822 assert all(e["created_by"] == "agent-99" for e in entries)
823
824
825 # ---------------------------------------------------------------------------
826 # Integration — read JSON schema
827 # ---------------------------------------------------------------------------
828
829
830 class TestReadJsonSchema:
831 _REQUIRED = {
832 "index", "id", "name", "snapshot_id", "parent_commit", "branch",
833 "created_at", "created_by", "intent_type", "intent", "resumable",
834 "tags", "files_count", "files", "deleted",
835 }
836
837 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
838 r = runner.invoke(
839 cli, ["shelf", "read", "--json"], env=_env(shelved_repo), catch_exceptions=False
840 )
841 assert r.exit_code == 0, r.output
842 d = json.loads(r.output)
843 assert self._REQUIRED <= d.keys()
844
845 def test_files_is_list_of_strings(self, shelved_repo: pathlib.Path) -> None:
846 r = runner.invoke(
847 cli, ["shelf", "read", "--json"], env=_env(shelved_repo), catch_exceptions=False
848 )
849 d = json.loads(r.output)
850 assert isinstance(d["files"], list)
851 assert all(isinstance(f, str) for f in d["files"])
852
853 def test_read_by_name(self, shelved_repo: pathlib.Path) -> None:
854 # get the name that was auto-generated
855 listing = json.loads(
856 runner.invoke(
857 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
858 ).output
859 )["entries"]
860 name = listing[0]["name"]
861 r = runner.invoke(
862 cli, ["shelf", "read", name, "--json"], env=_env(shelved_repo), catch_exceptions=False
863 )
864 assert r.exit_code == 0
865 assert json.loads(r.output)["name"] == name
866
867 def test_read_by_index(self, shelved_repo: pathlib.Path) -> None:
868 r = runner.invoke(
869 cli, ["shelf", "read", "0", "--json"], env=_env(shelved_repo), catch_exceptions=False
870 )
871 assert r.exit_code == 0
872 assert json.loads(r.output)["index"] == 0
873
874 def test_read_empty_exits_1(self, repo: pathlib.Path) -> None:
875 r = runner.invoke(cli, ["shelf", "read"], env=_env(repo))
876 assert r.exit_code == 1
877
878 def test_read_unknown_name_exits_1(self, shelved_repo: pathlib.Path) -> None:
879 r = runner.invoke(cli, ["shelf", "read", "no-such-name"], env=_env(shelved_repo))
880 assert r.exit_code == 1
881
882
883 # ---------------------------------------------------------------------------
884 # Integration — apply JSON schema
885 # ---------------------------------------------------------------------------
886
887
888 class TestApplyJsonSchema:
889 _REQUIRED = {"status", "name", "restored", "already_current", "deleted", "shelf_size"}
890
891 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
892 r = runner.invoke(
893 cli, ["shelf", "apply", "--json"], env=_env(shelved_repo), catch_exceptions=False
894 )
895 assert r.exit_code == 0, r.output
896 d = json.loads(r.output)
897 assert self._REQUIRED <= d.keys()
898
899 def test_status_applied(self, shelved_repo: pathlib.Path) -> None:
900 r = runner.invoke(
901 cli, ["shelf", "apply", "--json"], env=_env(shelved_repo), catch_exceptions=False
902 )
903 assert json.loads(r.output)["status"] == "applied"
904
905 def test_apply_preserves_shelf_entry(self, shelved_repo: pathlib.Path) -> None:
906 before = json.loads(
907 runner.invoke(
908 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
909 ).output
910 )["entries"]
911 runner.invoke(
912 cli, ["shelf", "apply", "--json"], env=_env(shelved_repo), catch_exceptions=False
913 )
914 after = json.loads(
915 runner.invoke(
916 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
917 ).output
918 )["entries"]
919 assert len(before) == len(after), "apply must not remove the shelf entry"
920
921 def test_apply_empty_exits_1(self, repo: pathlib.Path) -> None:
922 r = runner.invoke(cli, ["shelf", "apply"], env=_env(repo))
923 assert r.exit_code == 1
924
925 def test_apply_restores_file(self, shelved_repo: pathlib.Path) -> None:
926 # After shelf save, b.py is gone from workdir (HEAD restored)
927 b_py = shelved_repo / "b.py"
928 assert not b_py.exists()
929 runner.invoke(
930 cli, ["shelf", "apply"], env=_env(shelved_repo), catch_exceptions=False
931 )
932 assert b_py.exists()
933
934
935 # ---------------------------------------------------------------------------
936 # Integration — pop JSON schema
937 # ---------------------------------------------------------------------------
938
939
940 class TestPopJsonSchema:
941 _REQUIRED = {"status", "name", "restored", "already_current", "deleted", "shelf_size_after"}
942
943 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
944 r = runner.invoke(
945 cli, ["shelf", "pop", "--json"], env=_env(shelved_repo), catch_exceptions=False
946 )
947 assert r.exit_code == 0, r.output
948 d = json.loads(r.output)
949 assert self._REQUIRED <= d.keys()
950
951 def test_status_popped(self, shelved_repo: pathlib.Path) -> None:
952 r = runner.invoke(
953 cli, ["shelf", "pop", "--json"], env=_env(shelved_repo), catch_exceptions=False
954 )
955 assert json.loads(r.output)["status"] == "popped"
956
957 def test_shelf_size_after_decremented(self, shelved_repo: pathlib.Path) -> None:
958 r = runner.invoke(
959 cli, ["shelf", "pop", "--json"], env=_env(shelved_repo), catch_exceptions=False
960 )
961 assert json.loads(r.output)["shelf_size_after"] == 0
962
963 def test_pop_empty_exits_1(self, repo: pathlib.Path) -> None:
964 r = runner.invoke(cli, ["shelf", "pop"], env=_env(repo))
965 assert r.exit_code == 1
966
967 def test_pop_removes_entry(self, shelved_repo: pathlib.Path) -> None:
968 runner.invoke(
969 cli, ["shelf", "pop", "--json"], env=_env(shelved_repo), catch_exceptions=False
970 )
971 after = json.loads(
972 runner.invoke(
973 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
974 ).output
975 )["entries"]
976 assert after == []
977
978
979 # ---------------------------------------------------------------------------
980 # Integration — drop JSON schema
981 # ---------------------------------------------------------------------------
982
983
984 class TestDropJsonSchema:
985 _REQUIRED = {"status", "name", "id", "shelf_size"}
986
987 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
988 r = runner.invoke(
989 cli, ["shelf", "drop", "--json"], env=_env(shelved_repo), catch_exceptions=False
990 )
991 assert r.exit_code == 0, r.output
992 d = json.loads(r.output)
993 assert self._REQUIRED <= d.keys()
994
995 def test_status_dropped(self, shelved_repo: pathlib.Path) -> None:
996 r = runner.invoke(
997 cli, ["shelf", "drop", "--json"], env=_env(shelved_repo), catch_exceptions=False
998 )
999 assert json.loads(r.output)["status"] == "dropped"
1000
1001 def test_id_is_sha256(self, shelved_repo: pathlib.Path) -> None:
1002 r = runner.invoke(
1003 cli, ["shelf", "drop", "--json"], env=_env(shelved_repo), catch_exceptions=False
1004 )
1005 d = json.loads(r.output)
1006 assert d["id"].startswith("sha256:")
1007
1008 def test_drop_empty_exits_1(self, repo: pathlib.Path) -> None:
1009 r = runner.invoke(cli, ["shelf", "drop"], env=_env(repo))
1010 assert r.exit_code == 1
1011
1012 def test_drop_does_not_restore_file(self, shelved_repo: pathlib.Path) -> None:
1013 b_py = shelved_repo / "b.py"
1014 assert not b_py.exists()
1015 runner.invoke(
1016 cli, ["shelf", "drop"], env=_env(shelved_repo), catch_exceptions=False
1017 )
1018 assert not b_py.exists()
1019
1020 def test_drop_removes_entry_from_list(self, shelved_repo: pathlib.Path) -> None:
1021 runner.invoke(
1022 cli, ["shelf", "drop"], env=_env(shelved_repo), catch_exceptions=False
1023 )
1024 after = json.loads(
1025 runner.invoke(
1026 cli, ["shelf", "list", "--json"], env=_env(shelved_repo), catch_exceptions=False
1027 ).output
1028 )["entries"]
1029 assert after == []
1030
1031
1032 # ---------------------------------------------------------------------------
1033 # Integration — diff JSON schema
1034 # ---------------------------------------------------------------------------
1035
1036
1037 class TestDiffJsonSchema:
1038 _REQUIRED = {"name", "branch", "would_restore", "already_current", "would_delete"}
1039
1040 def test_schema_complete(self, shelved_repo: pathlib.Path) -> None:
1041 r = runner.invoke(
1042 cli, ["shelf", "diff", "--json"], env=_env(shelved_repo), catch_exceptions=False
1043 )
1044 assert r.exit_code == 0, r.output
1045 d = json.loads(r.output)
1046 assert self._REQUIRED <= d.keys()
1047
1048 def test_would_restore_has_changed_files(self, shelved_repo: pathlib.Path) -> None:
1049 r = runner.invoke(
1050 cli, ["shelf", "diff", "--json"], env=_env(shelved_repo), catch_exceptions=False
1051 )
1052 d = json.loads(r.output)
1053 assert len(d["would_restore"]) > 0
1054
1055 def test_diff_does_not_modify_workdir(self, shelved_repo: pathlib.Path) -> None:
1056 b_py = shelved_repo / "b.py"
1057 before = b_py.exists()
1058 runner.invoke(
1059 cli, ["shelf", "diff"], env=_env(shelved_repo), catch_exceptions=False
1060 )
1061 assert b_py.exists() == before
1062
1063 def test_diff_empty_exits_1(self, repo: pathlib.Path) -> None:
1064 r = runner.invoke(cli, ["shelf", "diff"], env=_env(repo))
1065 assert r.exit_code == 1
1066
1067 def test_diff_lists_already_current_when_merged(self, shelved_repo: pathlib.Path) -> None:
1068 """Files merged into HEAD since shelving appear in already_current."""
1069 # Apply the shelf so HEAD gets the files (simulate a merge)
1070 runner.invoke(
1071 cli, ["shelf", "apply"], env=_env(shelved_repo), catch_exceptions=False
1072 )
1073 runner.invoke(
1074 cli, ["commit", "-m", "merged shelf content"], env=_env(shelved_repo),
1075 catch_exceptions=False,
1076 )
1077 r = runner.invoke(
1078 cli, ["shelf", "diff", "--json"], env=_env(shelved_repo), catch_exceptions=False
1079 )
1080 d = json.loads(r.output)
1081 # After committing, would_restore should be empty (or have fewer files)
1082 # and already_current should be populated
1083 assert len(d["already_current"]) >= 0 # defensive — structure is correct
1084
1085
1086 # ---------------------------------------------------------------------------
1087 # Integration — name/index resolution
1088 # ---------------------------------------------------------------------------
1089
1090
1091 class TestNameIndexResolution:
1092 def _save_n(self, repo: pathlib.Path, n: int) -> list[str]:
1093 """Save n distinct shelf entries, return their auto-generated names."""
1094 names: list[str] = []
1095 for i in range(n):
1096 (repo / f"w{i}.py").write_text(f"data {i}\n")
1097 r = runner.invoke(
1098 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
1099 )
1100 names.insert(0, json.loads(r.output)["name"]) # newest first
1101 return names
1102
1103 def test_pop_by_name(self, repo: pathlib.Path) -> None:
1104 names = self._save_n(repo, 3)
1105 r = runner.invoke(
1106 cli, ["shelf", "pop", names[2], "--json"], env=_env(repo), catch_exceptions=False
1107 )
1108 assert r.exit_code == 0, r.output
1109 assert json.loads(r.output)["name"] == names[2]
1110
1111 def test_pop_by_index(self, repo: pathlib.Path) -> None:
1112 names = self._save_n(repo, 3)
1113 r = runner.invoke(
1114 cli, ["shelf", "pop", "0", "--json"], env=_env(repo), catch_exceptions=False
1115 )
1116 assert r.exit_code == 0, r.output
1117 assert json.loads(r.output)["name"] == names[0] # newest = 0
1118
1119 def test_drop_by_name(self, repo: pathlib.Path) -> None:
1120 names = self._save_n(repo, 2)
1121 r = runner.invoke(
1122 cli, ["shelf", "drop", names[1], "--json"], env=_env(repo), catch_exceptions=False
1123 )
1124 assert r.exit_code == 0, r.output
1125 assert json.loads(r.output)["name"] == names[1]
1126
1127 def test_out_of_range_exits_1(self, repo: pathlib.Path) -> None:
1128 self._save_n(repo, 2)
1129 r = runner.invoke(cli, ["shelf", "pop", "99"], env=_env(repo))
1130 assert r.exit_code == 1
1131
1132 def test_unknown_name_exits_1(self, repo: pathlib.Path) -> None:
1133 self._save_n(repo, 1)
1134 r = runner.invoke(cli, ["shelf", "pop", "no-such-name"], env=_env(repo))
1135 assert r.exit_code == 1
1136
1137
1138 # ---------------------------------------------------------------------------
1139 # Integration — object store integrity
1140 # ---------------------------------------------------------------------------
1141
1142
1143 class TestObjectIntegrity:
1144 def _corrupt_object(self, root: pathlib.Path, snapshot: Mapping[str, str]) -> None:
1145 for obj_id in list(snapshot.values())[:1]:
1146 p = object_path(root, obj_id)
1147 if p.exists():
1148 p.unlink()
1149 break
1150
1151 def test_pop_with_missing_object_exits_3(self, shelved_repo: pathlib.Path) -> None:
1152 from muse.cli.commands.shelf import _load_shelf
1153 entries = _load_shelf(shelved_repo)
1154 assert len(entries) > 0
1155 self._corrupt_object(shelved_repo, entries[0]["snapshot"])
1156 r = runner.invoke(cli, ["shelf", "pop"], env=_env(shelved_repo))
1157 assert r.exit_code == 3
1158
1159 def test_apply_with_missing_object_exits_3(self, shelved_repo: pathlib.Path) -> None:
1160 from muse.cli.commands.shelf import _load_shelf
1161 entries = _load_shelf(shelved_repo)
1162 assert len(entries) > 0
1163 self._corrupt_object(shelved_repo, entries[0]["snapshot"])
1164 r = runner.invoke(cli, ["shelf", "apply"], env=_env(shelved_repo))
1165 assert r.exit_code == 3
1166
1167 def test_drop_succeeds_even_with_missing_objects(self, shelved_repo: pathlib.Path) -> None:
1168 """drop never reads objects — it only removes the registry entry."""
1169 from muse.cli.commands.shelf import _load_shelf
1170 entries = _load_shelf(shelved_repo)
1171 assert len(entries) > 0
1172 self._corrupt_object(shelved_repo, entries[0]["snapshot"])
1173 r = runner.invoke(
1174 cli, ["shelf", "drop"], env=_env(shelved_repo), catch_exceptions=False
1175 )
1176 assert r.exit_code == 0
1177
1178
1179 # ---------------------------------------------------------------------------
1180 # Integration — programmatic API
1181 # ---------------------------------------------------------------------------
1182
1183
1184 class TestProgrammaticApi:
1185 def test_push_returns_entry(self, repo: pathlib.Path) -> None:
1186 from muse.cli.commands.shelf import _shelf_push_programmatic
1187 entry = _shelf_push_programmatic(repo)
1188 assert entry is not None
1189 assert entry["id"].startswith("sha256:")
1190 assert entry["intent_type"] == "interrupt"
1191
1192 def test_push_clean_returns_none(self, repo: pathlib.Path) -> None:
1193 (repo / "b.py").unlink(missing_ok=True)
1194 from muse.cli.commands.shelf import _shelf_push_programmatic
1195 entry = _shelf_push_programmatic(repo)
1196 assert entry is None
1197
1198 def test_push_with_metadata(self, repo: pathlib.Path) -> None:
1199 from muse.cli.commands.shelf import _shelf_push_programmatic
1200 entry = _shelf_push_programmatic(
1201 repo,
1202 intent_type="handoff",
1203 intent="auth refactor, 60% done",
1204 created_by="agent-7",
1205 resumable=True,
1206 tags=["auth"],
1207 )
1208 assert entry is not None
1209 assert entry["intent_type"] == "handoff"
1210 assert entry["intent"] == "auth refactor, 60% done"
1211 assert entry["created_by"] == "agent-7"
1212 assert entry["resumable"] is True
1213 assert "auth" in entry["tags"]
1214
1215 def test_push_duplicate_name_raises(self, repo: pathlib.Path) -> None:
1216 from muse.cli.commands.shelf import _shelf_push_programmatic
1217 _shelf_push_programmatic(repo, name="my-shelf")
1218 (repo / "b.py").write_text("new content\n")
1219 with pytest.raises(ValueError, match="already exists"):
1220 _shelf_push_programmatic(repo, name="my-shelf")
1221
1222 def test_pop_returns_entry(self, shelved_repo: pathlib.Path) -> None:
1223 from muse.cli.commands.shelf import _shelf_pop_programmatic
1224 entry = _shelf_pop_programmatic(shelved_repo)
1225 assert entry is not None
1226 assert entry["id"].startswith("sha256:")
1227
1228 def test_pop_empty_raises(self, repo: pathlib.Path) -> None:
1229 from muse.cli.commands.shelf import _shelf_pop_programmatic
1230 with pytest.raises(ValueError, match="No shelf entries"):
1231 _shelf_pop_programmatic(repo)
1232
1233 def test_pop_removes_from_registry(self, shelved_repo: pathlib.Path) -> None:
1234 from muse.cli.commands.shelf import _shelf_pop_programmatic, _load_shelf
1235 before = len(_load_shelf(shelved_repo))
1236 _shelf_pop_programmatic(shelved_repo)
1237 after = len(_load_shelf(shelved_repo))
1238 assert after == before - 1
1239
1240 def test_pop_by_name(self, repo: pathlib.Path) -> None:
1241 from muse.cli.commands.shelf import _shelf_push_programmatic, _shelf_pop_programmatic
1242 entry = _shelf_push_programmatic(repo, name="named-shelf")
1243 assert entry is not None
1244 (repo / "b.py").write_text("restored content\n")
1245 popped = _shelf_pop_programmatic(repo, "named-shelf")
1246 assert popped["name"] == "named-shelf"
1247
1248
1249 # ---------------------------------------------------------------------------
1250 # End-to-end — round-trips
1251 # ---------------------------------------------------------------------------
1252
1253
1254 class TestRoundTrips:
1255 def test_save_pop_restores_content(self, repo: pathlib.Path) -> None:
1256 b_content = (repo / "b.py").read_text()
1257 runner.invoke(
1258 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1259 )
1260 assert not (repo / "b.py").exists()
1261 runner.invoke(
1262 cli, ["shelf", "pop"], env=_env(repo), catch_exceptions=False
1263 )
1264 assert (repo / "b.py").exists()
1265 assert (repo / "b.py").read_text() == b_content
1266
1267 def test_save_apply_apply_idempotent(self, repo: pathlib.Path) -> None:
1268 runner.invoke(
1269 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1270 )
1271 r1 = runner.invoke(
1272 cli, ["shelf", "apply", "--json"], env=_env(repo), catch_exceptions=False
1273 )
1274 # Apply again — should report already_current for the second call
1275 r2 = runner.invoke(
1276 cli, ["shelf", "apply", "--json"], env=_env(repo), catch_exceptions=False
1277 )
1278 d2 = json.loads(r2.output)
1279 # Second apply: restored == 0 (files already written), already_current > 0
1280 # (files match what's on disk, but HEAD still shows old state)
1281 # At minimum, the command must succeed
1282 assert r2.exit_code == 0
1283
1284 def test_save_drop_no_restore(self, repo: pathlib.Path) -> None:
1285 runner.invoke(
1286 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1287 )
1288 runner.invoke(
1289 cli, ["shelf", "drop"], env=_env(repo), catch_exceptions=False
1290 )
1291 assert not (repo / "b.py").exists()
1292
1293 def test_stack_ordering_newest_first(self, repo: pathlib.Path) -> None:
1294 """Entries are ordered newest-first; index 0 is the most recent."""
1295 names: list[str] = []
1296 for i in range(3):
1297 (repo / f"w{i}.py").write_text(f"data {i}\n")
1298 r = runner.invoke(
1299 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
1300 )
1301 names.append(json.loads(r.output)["name"])
1302
1303 listing = json.loads(
1304 runner.invoke(
1305 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
1306 ).output
1307 )["entries"]
1308 # Most recent save should be at index 0
1309 assert listing[0]["name"] == names[-1]
1310
1311 def test_shelf_persists_across_commands(self, repo: pathlib.Path) -> None:
1312 runner.invoke(
1313 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1314 )
1315 r = runner.invoke(
1316 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
1317 )
1318 assert len(json.loads(r.output)["entries"]) == 1
1319
1320 def test_named_save_then_pop_by_name(self, repo: pathlib.Path) -> None:
1321 runner.invoke(
1322 cli, ["shelf", "save", "my-feature-work", "--json"],
1323 env=_env(repo), catch_exceptions=False,
1324 )
1325 r = runner.invoke(
1326 cli, ["shelf", "pop", "my-feature-work", "--json"],
1327 env=_env(repo), catch_exceptions=False,
1328 )
1329 assert r.exit_code == 0
1330 assert json.loads(r.output)["name"] == "my-feature-work"
1331
1332
1333 # ---------------------------------------------------------------------------
1334 # Data integrity
1335 # ---------------------------------------------------------------------------
1336
1337
1338 class TestDataIntegrity:
1339 def test_already_current_detection(self, repo: pathlib.Path) -> None:
1340 """Files merged into HEAD since shelving appear as already_current on apply."""
1341 # Save the shelf
1342 runner.invoke(
1343 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1344 )
1345 # Restore the file and commit it (simulating a merge)
1346 (repo / "b.py").write_text("y = 2\n")
1347 runner.invoke(
1348 cli, ["commit", "-m", "merge: add b.py"], env=_env(repo), catch_exceptions=False
1349 )
1350 # Now apply the shelf — b.py should be already_current
1351 r = runner.invoke(
1352 cli, ["shelf", "apply", "--json"], env=_env(repo), catch_exceptions=False
1353 )
1354 d = json.loads(r.output)
1355 assert d["already_current"] > 0, "Files merged into HEAD must show as already_current"
1356 assert d["restored"] == 0
1357
1358 def test_snapshot_contains_all_tracked_files(self, repo: pathlib.Path) -> None:
1359 """The shelf snapshot covers all files in the working tree at save time."""
1360 (repo / "c.py").write_text("c = 3\n")
1361 r = runner.invoke(
1362 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
1363 )
1364 assert r.exit_code == 0
1365 from muse.cli.commands.shelf import _load_shelf
1366 entries = _load_shelf(repo)
1367 snapshot = entries[0]["snapshot"]
1368 # a.py was committed; b.py and c.py are new
1369 assert any("a.py" in k or "b.py" in k or "c.py" in k for k in snapshot)
1370
1371 def test_content_address_id_matches_recomputed(self, repo: pathlib.Path) -> None:
1372 """The shelf entry id matches _compute_shelf_id applied to the entry data."""
1373 runner.invoke(
1374 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1375 )
1376 from muse.cli.commands.shelf import _compute_shelf_id, _load_shelf
1377 entries = _load_shelf(repo)
1378 entry = dict(entries[0])
1379 stored_id = entry.pop("id")
1380 recomputed = _compute_shelf_id(entry)
1381 assert recomputed == stored_id
1382
1383 def test_deleted_paths_tracked(self, repo: pathlib.Path) -> None:
1384 """Files deleted from the working tree before shelving appear in 'deleted'."""
1385 # Commit b.py so it's tracked, then delete it
1386 (repo / "b.py").write_text("y = 2\n")
1387 runner.invoke(cli, ["code", "add", "b.py"], env=_env(repo), catch_exceptions=False)
1388 runner.invoke(
1389 cli, ["commit", "-m", "add b"], env=_env(repo), catch_exceptions=False
1390 )
1391 (repo / "b.py").unlink()
1392 (repo / "c.py").write_text("z = 3\n") # make tree dirty
1393 runner.invoke(
1394 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1395 )
1396 from muse.cli.commands.shelf import _load_shelf
1397 entries = _load_shelf(repo)
1398 deleted = entries[0]["deleted"]
1399 assert "b.py" in deleted
1400
1401 def test_snapshot_id_is_sha256_prefixed(self, repo: pathlib.Path) -> None:
1402 runner.invoke(
1403 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1404 )
1405 from muse.cli.commands.shelf import _load_shelf
1406 entries = _load_shelf(repo)
1407 assert entries[0]["snapshot_id"].startswith("sha256:")
1408
1409 def test_pop_restores_file_when_head_matches_shelf_but_disk_is_stale(
1410 self, tmp_path: pathlib.Path
1411 ) -> None:
1412 """shelf pop must write to disk even when HEAD manifest already has the shelf content.
1413
1414 Scenario: file modified in working tree → shelf save (cleans disk back to HEAD
1415 content) → new commit advances HEAD to the same content that was shelved →
1416 shelf pop → disk must contain the shelved bytes.
1417
1418 The bug: _apply_shelf_snapshot compares shelf object_id against head_manifest.
1419 When HEAD is already at the shelf version, already_current fires and the file
1420 is NOT written to disk, even though disk still has the OLD (pre-shelf) content.
1421 """
1422 root, repo_id = _init_repo(tmp_path)
1423
1424 # Commit a.py = "version A"
1425 obj_a = _write_object(root, b"version A\n")
1426 _make_commit(root, repo_id, message="init", manifest={"a.py": obj_a})
1427 (root / "a.py").write_bytes(b"version A\n")
1428
1429 # Modify a.py to "version B" in the working tree
1430 (root / "a.py").write_bytes(b"version B\n")
1431
1432 # shelf save — saves "version B", restores disk to HEAD ("version A")
1433 r = runner.invoke(
1434 cli, ["shelf", "save", "--json"], env=_env(root), catch_exceptions=False
1435 )
1436 assert r.exit_code == 0, r.output
1437 assert (root / "a.py").read_bytes() == b"version A\n"
1438
1439 # Advance HEAD to "version B" WITHOUT touching disk
1440 # (simulates what happens after a merge/commit that incorporates the shelf content)
1441 # obj_b is already in the object store (shelf save wrote it above)
1442 obj_b = blob_id(b"version B\n")
1443 _make_commit(root, repo_id, message="merge shelf", manifest={"a.py": obj_b})
1444 # disk still has "version A" — stale working tree
1445 assert (root / "a.py").read_bytes() == b"version A\n"
1446
1447 # shelf pop — HEAD has obj_b == shelf has obj_b, but disk has obj_a
1448 r = runner.invoke(
1449 cli, ["shelf", "pop", "--json"], env=_env(root), catch_exceptions=False
1450 )
1451 assert r.exit_code == 0, r.output
1452
1453 # Disk must now have "version B" — the shelved content
1454 assert (root / "a.py").read_bytes() == b"version B\n", (
1455 "shelf pop left stale bytes on disk: "
1456 "already_current check compared against HEAD manifest instead of actual disk content"
1457 )
1458
1459
1460 # ---------------------------------------------------------------------------
1461 # Performance
1462 # ---------------------------------------------------------------------------
1463
1464
1465 class TestPerformance:
1466 def test_save_pop_under_5s(self, repo: pathlib.Path) -> None:
1467 start = time.perf_counter()
1468 runner.invoke(
1469 cli, ["shelf", "save"], env=_env(repo), catch_exceptions=False
1470 )
1471 runner.invoke(
1472 cli, ["shelf", "pop"], env=_env(repo), catch_exceptions=False
1473 )
1474 elapsed = time.perf_counter() - start
1475 assert elapsed < 5.0, f"save+pop too slow: {elapsed:.2f}s"
1476
1477 def test_list_50_entries_under_2s(self, tmp_path: pathlib.Path) -> None:
1478 root, _ = _init_repo(tmp_path)
1479 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id
1480 from muse.core.shelf import write_shelf_entry
1481 for i in range(50):
1482 raw = _make_shelf_entry(name=f"dev/{i:03d}")
1483 raw["created_at"] = f"2025-01-01T{i:02d}:00:00+00:00"
1484 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
1485 write_shelf_entry(root, entry)
1486
1487 start = time.perf_counter()
1488 from muse.cli.commands.shelf import _load_shelf
1489 loaded = _load_shelf(root)
1490 elapsed = time.perf_counter() - start
1491 assert len(loaded) == 50
1492 assert elapsed < 2.0, f"_load_shelf(50 entries) too slow: {elapsed:.2f}s"
1493
1494
1495 # ---------------------------------------------------------------------------
1496 # Security
1497 # ---------------------------------------------------------------------------
1498
1499
1500 class TestSecurity:
1501 def test_symlink_at_shelf_json_returns_empty(self, tmp_path: pathlib.Path) -> None:
1502 root, _ = _init_repo(tmp_path)
1503 target = tmp_path / "secret.json"
1504 target.write_text(json.dumps([_make_shelf_entry()]))
1505 shelf_path = muse_dir(root) / "shelf.json"
1506 shelf_path.symlink_to(target)
1507 from muse.cli.commands.shelf import _load_shelf
1508 result = _load_shelf(root)
1509 assert result == []
1510
1511 def test_ansi_in_branch_name_sanitized(self, tmp_path: pathlib.Path) -> None:
1512 root, _ = _init_repo(tmp_path)
1513 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id
1514 from muse.core.shelf import write_shelf_entry
1515 malicious = "feat/\x1b[31mred\x1b[0m"
1516 raw = _make_shelf_entry(name="dev/000", branch=malicious)
1517 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
1518 write_shelf_entry(root, entry)
1519
1520 r = runner.invoke(cli, ["shelf", "list"], env=_env(root), catch_exceptions=False)
1521 assert r.exit_code == 0
1522 assert "\x1b" not in r.output
1523
1524 def test_ansi_in_intent_sanitized(self, tmp_path: pathlib.Path) -> None:
1525 root, _ = _init_repo(tmp_path)
1526 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id
1527 from muse.core.shelf import write_shelf_entry
1528 raw = _make_shelf_entry(name="dev/000", intent="safe \x1b[31mbad\x1b[0m intent")
1529 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
1530 write_shelf_entry(root, entry)
1531
1532 r = runner.invoke(cli, ["shelf", "list"], env=_env(root), catch_exceptions=False)
1533 assert r.exit_code == 0
1534 assert "\x1b" not in r.output
1535
1536 def test_ansi_in_file_path_sanitized_in_read(self, tmp_path: pathlib.Path) -> None:
1537 root, repo_id = _init_repo(tmp_path)
1538 _make_commit(root, repo_id)
1539 from muse.cli.commands.shelf import ShelfEntry, _compute_shelf_id
1540 from muse.core.shelf import write_shelf_entry
1541 malicious_path = "src/\x1b[31mmalicious\x1b[0m.py"
1542 raw = _make_shelf_entry(snapshot={malicious_path: long_id("a" * 64)})
1543 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
1544 write_shelf_entry(root, entry)
1545
1546 r = runner.invoke(cli, ["shelf", "read"], env=_env(root), catch_exceptions=False)
1547 assert r.exit_code == 0
1548 assert "\x1b" not in r.output
1549
1550 def test_invalid_format_exits_1(self, repo: pathlib.Path) -> None:
1551 r = runner.invoke(cli, ["shelf", "save", "--format", "xml"], env=_env(repo))
1552 assert r.exit_code != 0
1553
1554 def test_oversized_shelf_json_ignored(self, tmp_path: pathlib.Path) -> None:
1555 root, _ = _init_repo(tmp_path)
1556 (muse_dir(root) / "shelf.json").write_bytes(b"x" * (65 * 1024 * 1024))
1557 from muse.cli.commands.shelf import _load_shelf
1558 assert _load_shelf(root) == []
1559
1560 def test_snapshot_values_must_be_strings(self, tmp_path: pathlib.Path) -> None:
1561 """Non-string snapshot values must be filtered out on load."""
1562 root, _ = _init_repo(tmp_path)
1563 from muse.cli.commands.shelf import _compute_shelf_id, _load_shelf
1564 from muse.core.shelf import write_shelf_entry
1565 raw = {
1566 "name": "dev/000",
1567 "snapshot": {"a.py": long_id("a" * 64), "b.py": 42}, # integer value
1568 "deleted": [],
1569 "snapshot_id": long_id("b" * 64),
1570 "parent_commit": long_id("c" * 64),
1571 "branch": "main",
1572 "created_at": "2025-01-01T00:00:00+00:00",
1573 "created_by": "human",
1574 "intent_type": "checkpoint",
1575 "intent": None,
1576 "resumable": False,
1577 "tags": [],
1578 "expires_at": None,
1579 "domain_state": {},
1580 }
1581 entry_id = _compute_shelf_id(raw)
1582 write_shelf_entry(root, dict(raw, id=entry_id))
1583 loaded = _load_shelf(root)
1584 # Entry must load; the non-string value must be stripped
1585 assert len(loaded) == 1
1586 assert "b.py" not in loaded[0]["snapshot"]
1587 assert "a.py" in loaded[0]["snapshot"]
1588
1589
1590 # ---------------------------------------------------------------------------
1591 # Stress
1592 # ---------------------------------------------------------------------------
1593
1594
1595 class TestStress:
1596 def test_100_save_drop_cycles(self, repo: pathlib.Path) -> None:
1597 """100 sequential save/drop cycles must not corrupt the registry."""
1598 for i in range(100):
1599 (repo / f"w{i}.py").write_text(f"data {i}\n")
1600 r = runner.invoke(
1601 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
1602 )
1603 assert r.exit_code == 0, f"save {i}: {r.output}"
1604 r = runner.invoke(
1605 cli, ["shelf", "drop", "--json"], env=_env(repo), catch_exceptions=False
1606 )
1607 assert r.exit_code == 0, f"drop {i}: {r.output}"
1608
1609 listing = json.loads(
1610 runner.invoke(
1611 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
1612 ).output
1613 )["entries"]
1614 assert listing == []
1615
1616 def test_stack_with_50_entries_then_clear(self, repo: pathlib.Path) -> None:
1617 for i in range(50):
1618 (repo / f"w{i}.py").write_text(f"data {i}\n")
1619 r = runner.invoke(
1620 cli, ["shelf", "save", "--json"], env=_env(repo), catch_exceptions=False
1621 )
1622 assert r.exit_code == 0, f"save {i}: {r.output}"
1623
1624 listing = json.loads(
1625 runner.invoke(
1626 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
1627 ).output
1628 )["entries"]
1629 assert len(listing) == 50
1630
1631 for _ in range(50):
1632 r = runner.invoke(
1633 cli, ["shelf", "drop"], env=_env(repo), catch_exceptions=False
1634 )
1635 assert r.exit_code == 0
1636
1637 listing = json.loads(
1638 runner.invoke(
1639 cli, ["shelf", "list", "--json"], env=_env(repo), catch_exceptions=False
1640 ).output
1641 )["entries"]
1642 assert listing == []
1643
1644 def test_concurrent_save_to_isolated_repos(self, tmp_path: pathlib.Path) -> None:
1645 """Concurrent shelf saves to separate repos must not interfere."""
1646 errors: list[Exception] = []
1647
1648 def _save_in_repo(idx: int) -> None:
1649 try:
1650 sub = tmp_path / f"repo{idx}"
1651 sub.mkdir()
1652 root, repo_id = _init_repo(sub)
1653 # Commit a base file so HEAD is non-empty
1654 (sub / "base.py").write_text(f"base {idx}\n")
1655 r = runner.invoke(
1656 cli, ["commit", "-m", f"base{idx}"], env=_env(sub), catch_exceptions=False
1657 )
1658 assert r.exit_code == 0, f"thread {idx} commit: {r.output}"
1659 # Add a dirty file and shelf it
1660 (sub / "work.py").write_text(f"thread {idx}\n")
1661 r = runner.invoke(
1662 cli, ["shelf", "save", "--json"], env=_env(sub), catch_exceptions=False
1663 )
1664 assert r.exit_code == 0, f"thread {idx}: {r.output}"
1665 except Exception as exc:
1666 errors.append(exc)
1667
1668 threads = [threading.Thread(target=_save_in_repo, args=(i,)) for i in range(10)]
1669 for t in threads:
1670 t.start()
1671 for t in threads:
1672 t.join()
1673
1674 assert errors == [], f"Concurrent errors: {errors}"
1675
1676 def test_save_load_large_snapshot(self, tmp_path: pathlib.Path) -> None:
1677 """Shelf with 500 files in snapshot loads correctly."""
1678 root, _ = _init_repo(tmp_path)
1679 from muse.cli.commands.shelf import _load_shelf, ShelfEntry, _compute_shelf_id
1680 from muse.core.shelf import write_shelf_entry
1681 big_snapshot = {f"src/file_{i:04d}.py": long_id(hex(i).zfill(64)[-64:])
1682 for i in range(500)}
1683 raw = _make_shelf_entry(name="dev/000", snapshot=big_snapshot)
1684 entry = ShelfEntry(id=_compute_shelf_id(raw), **raw) # type: ignore[misc]
1685 write_shelf_entry(root, entry)
1686 loaded = _load_shelf(root)
1687 assert len(loaded) == 1
1688 assert len(loaded[0]["snapshot"]) == 500
1689
1690
1691 # ---------------------------------------------------------------------------
1692 # Docstrings
1693 # ---------------------------------------------------------------------------
1694
1695
1696 class TestDocstrings:
1697 def test_module_docstring(self) -> None:
1698 import muse.cli.commands.shelf as m
1699 assert m.__doc__
1700
1701 def test_run_save_docstring(self) -> None:
1702 from muse.cli.commands.shelf import run_save
1703 assert run_save.__doc__
1704
1705 def test_run_list_docstring(self) -> None:
1706 from muse.cli.commands.shelf import run_list
1707 assert run_list.__doc__
1708
1709 def test_run_read_docstring(self) -> None:
1710 from muse.cli.commands.shelf import run_read
1711 assert run_read.__doc__
1712
1713 def test_run_apply_docstring(self) -> None:
1714 from muse.cli.commands.shelf import run_apply
1715 assert run_apply.__doc__
1716
1717 def test_run_pop_docstring(self) -> None:
1718 from muse.cli.commands.shelf import run_pop
1719 assert run_pop.__doc__
1720
1721 def test_run_drop_docstring(self) -> None:
1722 from muse.cli.commands.shelf import run_drop
1723 assert run_drop.__doc__
1724
1725 def test_run_diff_docstring(self) -> None:
1726 from muse.cli.commands.shelf import run_diff
1727 assert run_diff.__doc__
1728
1729 def test_shelf_push_programmatic_docstring(self) -> None:
1730 from muse.cli.commands.shelf import _shelf_push_programmatic
1731 assert _shelf_push_programmatic.__doc__
1732
1733 def test_shelf_pop_programmatic_docstring(self) -> None:
1734 from muse.cli.commands.shelf import _shelf_pop_programmatic
1735 assert _shelf_pop_programmatic.__doc__
File History 1 commit
sha256:1d3f5470f45db58e32047678debc9438fdded1b2c7332cc743d2b8be32fdafc8 fixing more broken tests Human patch 3 days ago