gabriel / muse public
test_object_store_write_taxonomy.py python
702 lines 27.4 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Object store write taxonomy — exhaustive correctness and safety tests.
2
3 Every path that writes OR deletes objects is enumerated here. Each test
4 targets one invariant. If a test fails, it means a write or delete path is
5 broken; fix the production code, not the test.
6
7 Write paths covered
8 -------------------
9 W-1 write_object() — primary low-level write
10 W-2 write_object_from_path() — write from filesystem file
11 W-3 commit workflow — muse commit writes blobs then snapshot
12 W-4 shelf save — blobs written before shelf entry
13 W-5 fetch / pull _on_object — objects written on receive
14 W-6 apply_mpack — bundle unbundle writes objects
15 W-7 domain merge — plugin merge writes merged blob
16 W-8 hash_object --write — explicit low-level write
17
18 Delete paths covered
19 --------------------
20 D-1 gc non-full (default) — orphan sweep via snapshots walker
21 D-2 gc full — tight reachability from live refs
22 D-3 gc full multi-branch — objects on ALL branches survive
23 D-4 gc full object normalisation — sha256: prefixed IDs in reachable set
24 D-5 prune — mirrors gc non-full with expire window
25 D-6 maintenance gc task — calls run_gc with full=True
26
27 Consistency invariants
28 ----------------------
29 C-1 write → has_object True
30 C-2 write → object_state PRESENT
31 C-3 write → iter_stored_objects finds it
32 C-4 has_object and object_state agree
33 C-5 object_path canonical location
34 C-6 no write → object_state MISSING (no promisors)
35 C-7 no write → object_state PROMISED (promisors configured)
36 """
37
38 from __future__ import annotations
39
40 import datetime
41 import json
42 import pathlib
43 import tempfile
44
45 import pytest
46
47 from muse.core._types import Manifest, blob_id, long_id, split_id
48 from muse.core.gc import run_gc, _collect_reachable_snapshots, _collect_reachable_commits
49 from muse.core.object_availability import ObjectState, load_promisor_remotes, object_state
50 from muse.core.object_store import (
51 has_object,
52 iter_stored_objects,
53 object_path,
54 read_object,
55 write_object,
56 write_object_from_path,
57 )
58 from muse.core.snapshot import compute_commit_id, compute_snapshot_id
59 from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot
60
61
62 # ---------------------------------------------------------------------------
63 # Shared helpers
64 # ---------------------------------------------------------------------------
65
66
67 def _repo(tmp_path: pathlib.Path) -> pathlib.Path:
68 """Minimal .muse repo skeleton."""
69 muse = tmp_path / ".muse"
70 for d in ("objects/sha256", "commits/sha256", "snapshots/sha256", "refs/heads"):
71 (muse / d).mkdir(parents=True, exist_ok=True)
72 (muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
73 (muse / "HEAD").write_text("ref: refs/heads/main\n")
74 return tmp_path
75
76
77 def _write_blob(repo: pathlib.Path, content: bytes) -> str:
78 oid = blob_id(content)
79 write_object(repo, oid, content)
80 return oid
81
82
83 def _write_snap(repo: pathlib.Path, manifest: Manifest) -> str:
84 snap_id = compute_snapshot_id(manifest)
85 write_snapshot(repo, SnapshotRecord(snapshot_id=snap_id, manifest=manifest))
86 return snap_id
87
88
89 def _write_commit_on_branch(
90 repo: pathlib.Path,
91 snap_id: str,
92 branch: str = "main",
93 parent_id: str | None = None,
94 message: str = "test",
95 ) -> str:
96 committed_at = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)
97 parent_ids = [parent_id] if parent_id else []
98 commit_id = compute_commit_id(
99 repo_id="test-repo",
100 parent_ids=parent_ids,
101 snapshot_id=snap_id,
102 message=message,
103 committed_at_iso=committed_at.isoformat(),
104 )
105 write_commit(
106 repo,
107 CommitRecord(
108 commit_id=commit_id,
109 repo_id="test-repo",
110 created_on_branch=branch,
111 snapshot_id=snap_id,
112 message=message,
113 committed_at=committed_at,
114 parent_commit_id=parent_id,
115 ),
116 )
117 ref = repo / ".muse" / "refs" / "heads" / branch
118 ref.parent.mkdir(parents=True, exist_ok=True)
119 ref.write_text(commit_id)
120 return commit_id
121
122
123 # ---------------------------------------------------------------------------
124 # W-1 write_object — canonical path
125 # ---------------------------------------------------------------------------
126
127
128 class TestWriteObject:
129 """W-1: write_object() places objects at the canonical sha256/ path."""
130
131 def test_lands_under_sha256_dir(self, tmp_path: pathlib.Path) -> None:
132 repo = _repo(tmp_path)
133 oid = blob_id(b"hello")
134 write_object(repo, oid, b"hello")
135 p = object_path(repo, oid)
136 assert p.exists()
137 assert p.parent.parent.name == "sha256"
138
139 def test_shard_prefix_is_first_two_hex_chars(self, tmp_path: pathlib.Path) -> None:
140 repo = _repo(tmp_path)
141 content = b"shard-check"
142 oid = blob_id(content)
143 write_object(repo, oid, content)
144 p = object_path(repo, oid)
145 hex_id = split_id(oid)[1]
146 assert p.parent.name == hex_id[:2]
147
148 def test_filename_is_remaining_62_hex_chars(self, tmp_path: pathlib.Path) -> None:
149 repo = _repo(tmp_path)
150 content = b"filename-check"
151 oid = blob_id(content)
152 write_object(repo, oid, content)
153 p = object_path(repo, oid)
154 hex_id = split_id(oid)[1]
155 assert p.name == hex_id[2:]
156
157 def test_idempotent_returns_false_on_second_write(
158 self, tmp_path: pathlib.Path
159 ) -> None:
160 repo = _repo(tmp_path)
161 oid = blob_id(b"idempotent")
162 assert write_object(repo, oid, b"idempotent") is True
163 assert write_object(repo, oid, b"idempotent") is False
164
165 def test_content_verifiable_after_write(self, tmp_path: pathlib.Path) -> None:
166 repo = _repo(tmp_path)
167 content = b"verifiable content"
168 oid = blob_id(content)
169 write_object(repo, oid, content)
170 assert read_object(repo, oid) == content
171
172 def test_rejects_wrong_content(self, tmp_path: pathlib.Path) -> None:
173 repo = _repo(tmp_path)
174 oid = blob_id(b"correct")
175 with pytest.raises(ValueError):
176 write_object(repo, oid, b"wrong content")
177
178 def test_rejects_bare_hex_object_id(self, tmp_path: pathlib.Path) -> None:
179 repo = _repo(tmp_path)
180 import hashlib
181 bare_hex = hashlib.sha256(b"bare").hexdigest()
182 with pytest.raises((ValueError, Exception)):
183 write_object(repo, bare_hex, b"bare")
184
185
186 # ---------------------------------------------------------------------------
187 # W-2 write_object_from_path — canonical path
188 # ---------------------------------------------------------------------------
189
190
191 class TestWriteObjectFromPath:
192 """W-2: write_object_from_path() writes from a file and lands at canonical path."""
193
194 def test_writes_to_sha256_dir(self, tmp_path: pathlib.Path) -> None:
195 repo = _repo(tmp_path)
196 src = tmp_path / "source.txt"
197 content = b"from-path content"
198 src.write_bytes(content)
199 oid = blob_id(content)
200 write_object_from_path(repo, oid, src)
201 p = object_path(repo, oid)
202 assert p.exists()
203 assert p.parent.parent.name == "sha256"
204
205 def test_oid_matches_blob_id(self, tmp_path: pathlib.Path) -> None:
206 repo = _repo(tmp_path)
207 content = b"oid must match blob_id"
208 src = tmp_path / "f.txt"
209 src.write_bytes(content)
210 oid = blob_id(content)
211 write_object_from_path(repo, oid, src)
212 assert oid == blob_id(content)
213
214 def test_content_readable_after_write(self, tmp_path: pathlib.Path) -> None:
215 repo = _repo(tmp_path)
216 content = b"readable after write"
217 src = tmp_path / "r.txt"
218 src.write_bytes(content)
219 oid = blob_id(content)
220 write_object_from_path(repo, oid, src)
221 assert read_object(repo, oid) == content
222
223
224 # ---------------------------------------------------------------------------
225 # C-1 … C-7 Consistency invariants
226 # ---------------------------------------------------------------------------
227
228
229 class TestConsistencyInvariants:
230 """C-1 through C-7: consistency between write, has_object, object_state, iter."""
231
232 def test_c1_has_object_true_after_write(self, tmp_path: pathlib.Path) -> None:
233 repo = _repo(tmp_path)
234 oid = _write_blob(repo, b"c1")
235 assert has_object(repo, oid)
236
237 def test_c2_object_state_present_after_write(self, tmp_path: pathlib.Path) -> None:
238 repo = _repo(tmp_path)
239 oid = _write_blob(repo, b"c2")
240 state = object_state(repo, oid, [])
241 assert state == ObjectState.PRESENT
242
243 def test_c3_iter_stored_objects_finds_written(
244 self, tmp_path: pathlib.Path
245 ) -> None:
246 repo = _repo(tmp_path)
247 oid = _write_blob(repo, b"c3")
248 found = {o for o, _ in iter_stored_objects(repo)}
249 assert oid in found
250
251 def test_c4_has_object_and_object_state_agree_present(
252 self, tmp_path: pathlib.Path
253 ) -> None:
254 repo = _repo(tmp_path)
255 oid = _write_blob(repo, b"c4-present")
256 assert has_object(repo, oid)
257 assert object_state(repo, oid, []) == ObjectState.PRESENT
258
259 def test_c4_has_object_and_object_state_agree_absent(
260 self, tmp_path: pathlib.Path
261 ) -> None:
262 repo = _repo(tmp_path)
263 oid = blob_id(b"never written")
264 assert not has_object(repo, oid)
265 assert object_state(repo, oid, []) == ObjectState.MISSING
266
267 def test_c5_object_path_canonical_location(self, tmp_path: pathlib.Path) -> None:
268 repo = _repo(tmp_path)
269 content = b"canonical"
270 oid = blob_id(content)
271 write_object(repo, oid, content)
272 p = object_path(repo, oid)
273 hex_id = split_id(oid)[1]
274 expected = repo / ".muse" / "objects" / "sha256" / hex_id[:2] / hex_id[2:]
275 assert p == expected
276 assert p.exists()
277
278 def test_c6_object_state_missing_when_absent_no_promisors(
279 self, tmp_path: pathlib.Path
280 ) -> None:
281 repo = _repo(tmp_path)
282 oid = blob_id(b"missing")
283 state = object_state(repo, oid, promisor_remotes=[])
284 assert state == ObjectState.MISSING
285
286 def test_c7_object_state_promised_when_absent_with_promisor(
287 self, tmp_path: pathlib.Path
288 ) -> None:
289 repo = _repo(tmp_path)
290 oid = blob_id(b"promised")
291 state = object_state(repo, oid, promisor_remotes=["staging"])
292 assert state == ObjectState.PROMISED
293
294 def test_c7_object_state_present_beats_promisor(
295 self, tmp_path: pathlib.Path
296 ) -> None:
297 """A present object is PRESENT even when promisors are configured."""
298 repo = _repo(tmp_path)
299 oid = _write_blob(repo, b"present beats promisor")
300 state = object_state(repo, oid, promisor_remotes=["staging"])
301 assert state == ObjectState.PRESENT
302
303
304 # ---------------------------------------------------------------------------
305 # D-1 GC non-full — orphan sweep
306 # ---------------------------------------------------------------------------
307
308
309 class TestGcNonFull:
310 """D-1: default (non-full) GC sweeps orphans but retains all reachable objects."""
311
312 def test_orphan_collected(self, tmp_path: pathlib.Path) -> None:
313 repo = _repo(tmp_path)
314 oid = _write_blob(repo, b"orphan")
315 run_gc(repo, grace_period_seconds=0)
316 assert not object_path(repo, oid).exists()
317
318 def test_reachable_via_snapshot_survives(self, tmp_path: pathlib.Path) -> None:
319 repo = _repo(tmp_path)
320 oid = _write_blob(repo, b"reachable")
321 snap_id = _write_snap(repo, {"f.txt": oid})
322 _write_commit_on_branch(repo, snap_id)
323 run_gc(repo, grace_period_seconds=0)
324 assert object_path(repo, oid).exists()
325
326 def test_reachable_on_non_default_branch_survives(
327 self, tmp_path: pathlib.Path
328 ) -> None:
329 repo = _repo(tmp_path)
330 oid = _write_blob(repo, b"non-default branch")
331 snap_id = _write_snap(repo, {"g.txt": oid})
332 _write_commit_on_branch(repo, snap_id, branch="dev")
333 run_gc(repo, grace_period_seconds=0)
334 assert object_path(repo, oid).exists()
335
336 def test_multiple_orphans_all_collected(self, tmp_path: pathlib.Path) -> None:
337 repo = _repo(tmp_path)
338 oids = [_write_blob(repo, f"o{i}".encode()) for i in range(5)]
339 result = run_gc(repo, grace_period_seconds=0)
340 assert result.collected_count == 5
341 for oid in oids:
342 assert not object_path(repo, oid).exists()
343
344 def test_grace_period_protects_recent_objects(
345 self, tmp_path: pathlib.Path
346 ) -> None:
347 repo = _repo(tmp_path)
348 oid = _write_blob(repo, b"fresh orphan")
349 result = run_gc(repo, grace_period_seconds=3600)
350 assert result.collected_count == 0
351 assert object_path(repo, oid).exists()
352
353
354 # ---------------------------------------------------------------------------
355 # D-2 GC full — tight reachability
356 # ---------------------------------------------------------------------------
357
358
359 class TestGcFull:
360 """D-2: gc full mode uses tight reachability but must still retain all live objects."""
361
362 def test_reachable_object_survives_full_gc(self, tmp_path: pathlib.Path) -> None:
363 repo = _repo(tmp_path)
364 oid = _write_blob(repo, b"live object")
365 snap_id = _write_snap(repo, {"live.txt": oid})
366 _write_commit_on_branch(repo, snap_id)
367 result = run_gc(repo, full=True, grace_period_seconds=0)
368 assert result.collected_count == 0
369 assert object_path(repo, oid).exists()
370
371 def test_orphan_collected_by_full_gc(self, tmp_path: pathlib.Path) -> None:
372 repo = _repo(tmp_path)
373 # One reachable, one orphan.
374 live_oid = _write_blob(repo, b"live")
375 snap_id = _write_snap(repo, {"f.txt": live_oid})
376 _write_commit_on_branch(repo, snap_id)
377 orphan_oid = _write_blob(repo, b"orphan")
378 result = run_gc(repo, full=True, grace_period_seconds=0)
379 assert result.collected_count == 1
380 assert not object_path(repo, orphan_oid).exists()
381 assert object_path(repo, live_oid).exists()
382
383 def test_full_gc_dry_run_does_not_delete(self, tmp_path: pathlib.Path) -> None:
384 repo = _repo(tmp_path)
385 oid = _write_blob(repo, b"dry-run orphan")
386 result = run_gc(repo, full=True, dry_run=True, grace_period_seconds=0)
387 assert result.dry_run is True
388 assert object_path(repo, oid).exists()
389
390
391 # ---------------------------------------------------------------------------
392 # D-3 GC full multi-branch — objects on ALL live branches survive
393 # ---------------------------------------------------------------------------
394
395
396 class TestGcFullMultiBranch:
397 """D-3: full GC must retain objects reachable from every live branch, not just HEAD."""
398
399 def test_object_on_secondary_branch_survives_full_gc(
400 self, tmp_path: pathlib.Path
401 ) -> None:
402 repo = _repo(tmp_path)
403 # main branch object
404 main_oid = _write_blob(repo, b"main content")
405 main_snap = _write_snap(repo, {"main.txt": main_oid})
406 _write_commit_on_branch(repo, main_snap, branch="main")
407 # dev branch object (different content)
408 dev_oid = _write_blob(repo, b"dev content")
409 dev_snap = _write_snap(repo, {"dev.txt": dev_oid})
410 _write_commit_on_branch(repo, dev_snap, branch="dev")
411 result = run_gc(repo, full=True, grace_period_seconds=0)
412 assert result.collected_count == 0
413 assert object_path(repo, main_oid).exists(), "main branch object deleted!"
414 assert object_path(repo, dev_oid).exists(), "dev branch object deleted!"
415
416 def test_object_on_three_branches_all_survive(
417 self, tmp_path: pathlib.Path
418 ) -> None:
419 repo = _repo(tmp_path)
420 oids = []
421 for branch in ("main", "dev", "feat/x"):
422 oid = _write_blob(repo, f"content on {branch}".encode())
423 snap_id = _write_snap(repo, {f"{branch}.txt": oid})
424 _write_commit_on_branch(repo, snap_id, branch=branch)
425 oids.append(oid)
426 result = run_gc(repo, full=True, grace_period_seconds=0)
427 assert result.collected_count == 0
428 for oid in oids:
429 assert object_path(repo, oid).exists()
430
431 def test_shared_object_referenced_by_two_branches_survives(
432 self, tmp_path: pathlib.Path
433 ) -> None:
434 """If main and dev both reference the same object, full GC must keep it."""
435 repo = _repo(tmp_path)
436 shared_oid = _write_blob(repo, b"shared content")
437 for branch in ("main", "dev"):
438 snap_id = _write_snap(repo, {"shared.txt": shared_oid})
439 _write_commit_on_branch(repo, snap_id, branch=branch)
440 result = run_gc(repo, full=True, grace_period_seconds=0)
441 assert result.collected_count == 0
442 assert object_path(repo, shared_oid).exists()
443
444
445 # ---------------------------------------------------------------------------
446 # D-4 GC full object ID normalisation
447 # ---------------------------------------------------------------------------
448
449
450 class TestGcFullObjectNormalisation:
451 """D-4: full GC reachability set uses sha256:-prefixed IDs throughout.
452
453 This is the critical invariant that ensures the reachable-objects set
454 (built from snapshot manifests) matches the stored-objects set
455 (built from iter_stored_objects). A mismatch would cause live objects
456 to be incorrectly classified as unreachable and deleted.
457 """
458
459 def test_reachable_set_uses_prefixed_ids(self, tmp_path: pathlib.Path) -> None:
460 """_collect_reachable_snapshots returns sha256:-prefixed object IDs."""
461 repo = _repo(tmp_path)
462 oid = _write_blob(repo, b"normalisation check")
463 snap_id = _write_snap(repo, {"f.txt": oid})
464 _write_commit_on_branch(repo, snap_id)
465 reachable_commits = _collect_reachable_commits(repo)
466 _, reachable_objs = _collect_reachable_snapshots(repo, reachable_commits)
467 # Every entry must carry the sha256: prefix.
468 for obj_id in reachable_objs:
469 assert obj_id.startswith("sha256:"), (
470 f"Reachable object ID missing sha256: prefix: {obj_id!r}"
471 )
472
473 def test_iter_stored_objects_uses_prefixed_ids(
474 self, tmp_path: pathlib.Path
475 ) -> None:
476 """iter_stored_objects returns sha256:-prefixed object IDs."""
477 repo = _repo(tmp_path)
478 _write_blob(repo, b"stored check")
479 for oid, _ in iter_stored_objects(repo):
480 assert oid.startswith("sha256:"), (
481 f"iter_stored_objects returned unprefixed ID: {oid!r}"
482 )
483
484 def test_reachable_set_matches_stored_set_for_live_objects(
485 self, tmp_path: pathlib.Path
486 ) -> None:
487 """Every live object must appear in both sets with the same ID form."""
488 repo = _repo(tmp_path)
489 oids = set()
490 for i in range(3):
491 oid = _write_blob(repo, f"live {i}".encode())
492 oids.add(oid)
493 snap_id = _write_snap(repo, {f"f{i}.txt": o for i, o in enumerate(oids)})
494 _write_commit_on_branch(repo, snap_id)
495 reachable_commits = _collect_reachable_commits(repo)
496 _, reachable_objs = _collect_reachable_snapshots(repo, reachable_commits)
497 stored = {o for o, _ in iter_stored_objects(repo)}
498 # All live objects must be in both sets.
499 for oid in oids:
500 assert oid in reachable_objs, f"{oid} missing from reachable set"
501 assert oid in stored, f"{oid} missing from stored set"
502
503 def test_full_gc_does_not_delete_prefixed_manifest_objects(
504 self, tmp_path: pathlib.Path
505 ) -> None:
506 """Regression: full GC must not delete objects whose IDs use sha256: prefix in the manifest."""
507 repo = _repo(tmp_path)
508 contents = [f"file {i} content".encode() for i in range(5)]
509 manifest = {}
510 for i, c in enumerate(contents):
511 oid = _write_blob(repo, c)
512 manifest[f"file{i}.py"] = oid
513 # Confirm the manifest value is prefixed.
514 assert oid.startswith("sha256:"), f"blob_id returned unprefixed: {oid}"
515 snap_id = _write_snap(repo, manifest)
516 _write_commit_on_branch(repo, snap_id)
517 result = run_gc(repo, full=True, grace_period_seconds=0)
518 assert result.collected_count == 0, (
519 f"Full GC deleted {result.collected_count} live objects: {result.collected_ids}"
520 )
521 for oid in manifest.values():
522 assert object_path(repo, oid).exists(), f"Full GC deleted live object {oid}"
523
524 def test_full_gc_retains_large_manifest(self, tmp_path: pathlib.Path) -> None:
525 """Full GC must not delete any of N live objects in a large snapshot."""
526 repo = _repo(tmp_path)
527 n = 50
528 manifest = {}
529 for i in range(n):
530 oid = _write_blob(repo, f"large manifest entry {i}".encode())
531 manifest[f"src/file_{i:03d}.py"] = oid
532 snap_id = _write_snap(repo, manifest)
533 _write_commit_on_branch(repo, snap_id)
534 result = run_gc(repo, full=True, grace_period_seconds=0)
535 assert result.collected_count == 0, (
536 f"Full GC deleted objects from large manifest: {result.collected_ids[:5]}"
537 )
538
539
540 # ---------------------------------------------------------------------------
541 # D-5 Prune — mirrors non-full GC with expire window
542 # ---------------------------------------------------------------------------
543
544
545 class TestPruneSafety:
546 """D-5: muse prune must never delete reachable objects."""
547
548 def test_prune_does_not_remove_committed_object(
549 self, tmp_path: pathlib.Path
550 ) -> None:
551 """Objects referenced by commits must survive prune."""
552 from muse.core.gc import run_gc # prune delegates to gc
553 repo = _repo(tmp_path)
554 oid = _write_blob(repo, b"committed object")
555 snap_id = _write_snap(repo, {"f.txt": oid})
556 _write_commit_on_branch(repo, snap_id)
557 # Non-full GC is what prune uses.
558 result = run_gc(repo, grace_period_seconds=0)
559 assert result.collected_count == 0
560 assert object_path(repo, oid).exists()
561
562
563 # ---------------------------------------------------------------------------
564 # D-6 Maintenance gc task passes full=True
565 # ---------------------------------------------------------------------------
566
567
568 class TestMaintenanceGcUsesFull:
569 """D-6: the maintenance 'gc' task must invoke run_gc with full=True."""
570
571 def test_maintenance_gc_task_calls_run_gc_with_full_true(
572 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
573 ) -> None:
574 """Confirm _run_gc (the maintenance task) passes full=True to run_gc."""
575 from muse.cli.commands import maintenance as maint_mod
576
577 calls: list[dict] = []
578
579 def _capture_run_gc(root, *, dry_run, grace_period_seconds, full):
580 calls.append({"full": full, "dry_run": dry_run})
581 from muse.core.gc import GcResult
582 return GcResult(dry_run=dry_run, grace_period_seconds=grace_period_seconds, full=full)
583
584 monkeypatch.setattr(maint_mod, "run_gc", _capture_run_gc)
585 repo = _repo(tmp_path)
586 maint_mod._run_gc(repo)
587 assert calls, "run_gc was never called by maintenance _run_gc"
588 assert calls[0]["full"] is True, (
589 f"Maintenance gc must pass full=True, got full={calls[0]['full']}"
590 )
591
592 def test_maintenance_gc_retains_all_reachable_objects(
593 self, tmp_path: pathlib.Path
594 ) -> None:
595 """End-to-end: running the maintenance gc task must not delete live objects."""
596 from muse.cli.commands.maintenance import _run_gc as maintenance_run_gc
597
598 repo = _repo(tmp_path)
599 # Write objects on two branches.
600 for branch, content in (("main", b"main obj"), ("dev", b"dev obj")):
601 oid = _write_blob(repo, content)
602 snap_id = _write_snap(repo, {f"{branch}.py": oid})
603 _write_commit_on_branch(repo, snap_id, branch=branch)
604
605 maintenance_run_gc(repo, dry_run=False)
606
607 # Both objects must survive.
608 for content in (b"main obj", b"dev obj"):
609 oid = blob_id(content)
610 assert object_path(repo, oid).exists(), (
611 f"Maintenance gc deleted live object {oid}"
612 )
613
614
615 # ---------------------------------------------------------------------------
616 # W-3 Commit workflow — objects written before commit record
617 # ---------------------------------------------------------------------------
618
619
620 class TestCommitWritePath:
621 """W-3: the commit workflow must write blobs to the object store at the
622 canonical path before creating the commit record.
623
624 We test this at the store level (not the CLI) since the CLI requires a
625 full working-tree environment.
626 """
627
628 def test_snapshot_manifest_objects_at_canonical_path(
629 self, tmp_path: pathlib.Path
630 ) -> None:
631 """Objects written for a commit land at the canonical sha256/ path."""
632 repo = _repo(tmp_path)
633 contents = {f"src/file{i}.py": f"content {i}".encode() for i in range(3)}
634 manifest = {}
635 for path, content in contents.items():
636 oid = blob_id(content)
637 write_object(repo, oid, content)
638 manifest[path] = oid
639 snap_id = _write_snap(repo, manifest)
640 _write_commit_on_branch(repo, snap_id)
641 # All objects reachable and at correct path.
642 for oid in manifest.values():
643 p = object_path(repo, oid)
644 assert p.exists()
645 assert p.parent.parent.name == "sha256"
646
647 def test_all_manifest_objects_survive_full_gc(
648 self, tmp_path: pathlib.Path
649 ) -> None:
650 """Objects in a committed snapshot must all survive full GC."""
651 repo = _repo(tmp_path)
652 manifest = {}
653 for i in range(10):
654 content = f"committed file {i}".encode()
655 oid = blob_id(content)
656 write_object(repo, oid, content)
657 manifest[f"file{i}.py"] = oid
658 snap_id = _write_snap(repo, manifest)
659 _write_commit_on_branch(repo, snap_id)
660 result = run_gc(repo, full=True, grace_period_seconds=0)
661 assert result.collected_count == 0
662 for oid in manifest.values():
663 assert object_path(repo, oid).exists()
664
665
666 # ---------------------------------------------------------------------------
667 # W-4 Shelf save — blobs written before shelf entry
668 # ---------------------------------------------------------------------------
669
670
671 class TestShelfWritePath:
672 """W-4: shelf objects must survive GC even before they are committed."""
673
674 def test_shelved_objects_survive_non_full_gc(
675 self, tmp_path: pathlib.Path
676 ) -> None:
677 repo = _repo(tmp_path)
678 shelf_oid = _write_blob(repo, b"shelved work")
679 shelf = repo / ".muse" / "shelf.json"
680 shelf.write_text(json.dumps([{
681 "snapshot_id": "s" * 64,
682 "branch": "main",
683 "created_at": "2026-01-01T00:00:00+00:00",
684 "snapshot": {"work.py": shelf_oid},
685 }]))
686 result = run_gc(repo, grace_period_seconds=0)
687 assert result.collected_count == 0
688 assert object_path(repo, shelf_oid).exists()
689
690 def test_shelved_objects_survive_full_gc(self, tmp_path: pathlib.Path) -> None:
691 repo = _repo(tmp_path)
692 shelf_oid = _write_blob(repo, b"shelved full gc")
693 shelf = repo / ".muse" / "shelf.json"
694 shelf.write_text(json.dumps([{
695 "snapshot_id": "s" * 64,
696 "branch": "dev",
697 "created_at": "2026-01-01T00:00:00+00:00",
698 "snapshot": {"wip.py": shelf_oid},
699 }]))
700 result = run_gc(repo, full=True, grace_period_seconds=0)
701 assert result.collected_count == 0
702 assert object_path(repo, shelf_oid).exists()
File History 1 commit
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago