gabriel / muse public
test_write_commit_snapshot_hash_verify.py python
599 lines 24.7 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """
2 Tests for two compounding data-integrity bugs in the store write path.
3
4 === BUG 1: write_commit skips hash verification of existing records ===
5
6 Root cause (muse/core/store.py::write_commit):
7
8 existing = CommitRecord.from_msgpack(_read_msgpack_dict(path))
9 if existing.commit_id != commit.commit_id:
10 raise OSError(...) # checks stored field — not a recomputed hash
11 return # ← skips if commit_id field matches
12
13 The idempotency check compares the raw commit_id *field* stored in the msgpack
14 record, not a recomputed hash of the core fields. A bit flip in snapshot_id,
15 message, or parent_commit_id leaves commit_id intact, passes the check, and
16 causes write_commit to silently skip the repair. The commit becomes
17 permanently unreadable via read_commit (which DOES call _verify_commit_id).
18
19 === BUG 2: write_snapshot skips ALL validation of existing records ===
20
21 Root cause (muse/core/store.py::write_snapshot):
22
23 if path.exists():
24 logger.debug("⚠️ Snapshot %s already exists — skipped", ...)
25 return # ← no parsing, no hash check, nothing
26
27 write_snapshot does not parse or verify the existing file at all. Any
28 corruption in the snapshot manifest — wrong object ID for a file, extra
29 entries, missing entries — is silently skipped. read_snapshot always calls
30 _verify_snapshot_id, which recomputes the manifest hash and raises on mismatch.
31 The snapshot is permanently unreadable with no repair path.
32
33 === Why this matters ===
34
35 Every commit references exactly one snapshot_id. If the snapshot at that ID is
36 corrupt and unrepaired, these operations all fail for that commit:
37
38 muse checkout <branch> — cannot build working tree
39 muse show <commit> — cannot display file contents
40 muse diff <commit> — cannot compute diff
41 muse log --diff — crashes on affected commits
42 muse push — pushes corrupt snapshot to hub
43 muse pull (on another machine) — imports corrupt snapshot
44
45 === The fix ===
46
47 write_commit: after confirming commit_id field matches, call
48 _verify_commit_id. Wrap its OSError as ValueError so the existing
49 `except Exception` branch triggers repair (overwrite) rather than propagation.
50
51 write_snapshot: before returning early, parse and verify the snapshot.
52 If verification fails, fall through to overwrite.
53
54 === Coverage ===
55
56 Unit — write_commit skips clean existing record (no regression)
57 Unit — write_commit repairs corrupt snapshot_id
58 Unit — write_commit repairs corrupt message
59 Unit — write_commit repairs corrupt parent_commit_id
60 Unit — write_snapshot skips clean existing record (no regression)
61 Unit — write_snapshot repairs corrupt manifest entry
62 Unit — write_snapshot repairs corrupt object ID in manifest
63 Unit — write_snapshot repairs completely empty manifest
64 Data — read_commit returns good record after write_commit repair
65 Data — read_snapshot returns good record after write_snapshot repair
66 Data — commit → snapshot chain readable after both repairs
67 Data — parent chain (A→B→C) survives corruption of middle commit
68 Integration — muse checkout path: snapshot must survive write_snapshot repair
69 Security — corrupt snapshot_id in commit cannot forge a different snapshot
70 Stress — 50 concurrent writes on corrupt commit all repair
71 Stress — 50 concurrent writes on corrupt snapshot all repair
72 Regression — hard integrity violation (commit_id field mismatch) still raises
73 Regression — write_commit normal path (no pre-existing file) still works
74 Regression — write_snapshot normal path still works
75 """
76 from __future__ import annotations
77
78 import datetime
79 import pathlib
80 import threading
81 import tempfile
82
83 import msgpack
84 import pytest
85
86 from muse.core._types import Manifest, MsgpackDict
87
88 # ---------------------------------------------------------------------------
89 # Helpers
90 # ---------------------------------------------------------------------------
91
92
93 def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
94 (tmp_path / ".muse" / "commits").mkdir(parents=True, exist_ok=True)
95 (tmp_path / ".muse" / "snapshots").mkdir(parents=True, exist_ok=True)
96 return tmp_path
97
98
99 def _ts(year: int = 2024) -> str:
100 return f"{year}-01-01T00:00:00+00:00"
101
102
103 def _good_commit(
104 snapshot_id: str | None = None,
105 message: str = "test commit",
106 parent_commit_id: str | None = None,
107 ts: str | None = None,
108 ) -> "CommitRecord":
109 from muse.core.store import CommitRecord
110 from muse.core.snapshot import compute_commit_id
111
112 snap_id = snapshot_id or "b" * 64
113 timestamp = ts or _ts()
114 parent_ids = [parent_commit_id] if parent_commit_id else []
115 commit_id = compute_commit_id(parent_ids, snap_id, message, timestamp)
116 return CommitRecord(
117 commit_id=commit_id,
118 repo_id="test-repo",
119 branch="main",
120 snapshot_id=snap_id,
121 message=message,
122 committed_at=datetime.datetime.fromisoformat(timestamp),
123 parent_commit_id=parent_commit_id,
124 parent2_commit_id=None,
125 author="gabriel",
126 metadata={},
127 )
128
129
130 def _good_snapshot(manifest: Manifest | None = None) -> "SnapshotRecord":
131 from muse.core.store import SnapshotRecord
132 from muse.core.snapshot import compute_snapshot_id
133
134 m = manifest or {"src/main.py": "c" * 64}
135 snapshot_id = compute_snapshot_id(m, {})
136 return SnapshotRecord(snapshot_id=snapshot_id, manifest=m, directories={})
137
138
139 def _write_corrupt_commit(repo: pathlib.Path, good: "CommitRecord", corrupt_field: MsgpackDict) -> None:
140 """Write a commit file that has good commit_id but corrupt content."""
141 base = {
142 "commit_id": good.commit_id,
143 "repo_id": "test-repo",
144 "branch": "main",
145 "snapshot_id": good.snapshot_id,
146 "message": good.message,
147 "committed_at": good.committed_at.isoformat(),
148 "parent_commit_id": good.parent_commit_id,
149 "parent2_commit_id": None,
150 "author": "gabriel",
151 "metadata": {},
152 "format_version": 1,
153 "reviewed_by": [],
154 }
155 base.update(corrupt_field)
156 path = repo / ".muse" / "commits" / f"{good.commit_id}.msgpack"
157 path.write_bytes(msgpack.packb(base, use_bin_type=True))
158
159
160 def _write_corrupt_snapshot(repo: pathlib.Path, good: "SnapshotRecord", corrupt_manifest: Manifest) -> None:
161 """Write a snapshot file with a corrupt manifest."""
162 record = {
163 "snapshot_id": good.snapshot_id,
164 "manifest": corrupt_manifest,
165 "directories": {},
166 "format_version": 1,
167 }
168 path = repo / ".muse" / "snapshots" / f"{good.snapshot_id}.msgpack"
169 path.write_bytes(msgpack.packb(record, use_bin_type=True))
170
171
172 # =============================================================================
173 # 1. UNIT — write_commit must repair corrupt core fields
174 # =============================================================================
175
176
177 class TestWriteCommitHashVerification:
178
179 def test_idempotent_skip_clean_record(self, tmp_path: pathlib.Path) -> None:
180 """Regression: write_commit on a clean existing file still returns fast."""
181 from muse.core.store import write_commit, read_commit
182
183 repo = _make_repo(tmp_path)
184 good = _good_commit()
185 write_commit(repo, good)
186 write_commit(repo, good) # second call: must not raise, must not change data
187 result = read_commit(repo, good.commit_id)
188 assert result is not None
189 assert result.commit_id == good.commit_id
190
191 def test_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None:
192 """
193 A commit file with a corrupt snapshot_id (wrong hash) must be repaired.
194
195 BUG: write_commit compares existing.commit_id == commit.commit_id
196 (stored field, unchanged), sees a match, and silently skips overwrite.
197 The commit then fails read_commit's _verify_commit_id check forever.
198
199 FIX: write_commit must also run _verify_commit_id on the existing record.
200 """
201 from muse.core.store import write_commit, read_commit
202
203 repo = _make_repo(tmp_path)
204 good = _good_commit(snapshot_id="b" * 64)
205 _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64}) # wrong hash
206
207 write_commit(repo, good) # must repair
208 result = read_commit(repo, good.commit_id)
209 assert result is not None, (
210 "BUG: write_commit skipped repair of corrupt snapshot_id.\n"
211 "The idempotency check only compares the commit_id FIELD — not the "
212 "recomputed hash. A corrupt snapshot_id leaves commit_id intact, "
213 "passes the check, and the file is never overwritten.\n"
214 "FIX: write_commit must call _verify_commit_id on the existing record."
215 )
216 assert result.snapshot_id == "b" * 64
217
218 def test_repairs_corrupt_message(self, tmp_path: pathlib.Path) -> None:
219 """A commit with a corrupt message (alters the hash) must be repaired."""
220 from muse.core.store import write_commit, read_commit
221
222 repo = _make_repo(tmp_path)
223 good = _good_commit(message="original message")
224 _write_corrupt_commit(repo, good, {"message": "CORRUPTED MESSAGE"})
225
226 write_commit(repo, good)
227 result = read_commit(repo, good.commit_id)
228 assert result is not None, "write_commit skipped repair of corrupt message"
229 assert result.message == "original message"
230
231 def test_repairs_corrupt_parent_commit_id(self, tmp_path: pathlib.Path) -> None:
232 """A commit with a corrupt parent_commit_id must be repaired."""
233 from muse.core.store import write_commit, read_commit
234
235 repo = _make_repo(tmp_path)
236 good = _good_commit(parent_commit_id=None)
237 # Inject a fake parent — changes the hash
238 _write_corrupt_commit(repo, good, {"parent_commit_id": "d" * 64})
239
240 write_commit(repo, good)
241 result = read_commit(repo, good.commit_id)
242 assert result is not None, "write_commit skipped repair of corrupt parent_commit_id"
243 assert result.parent_commit_id is None
244
245 def test_hard_integrity_violation_still_raises(self, tmp_path: pathlib.Path) -> None:
246 """
247 Regression: when commit_id FIELD in the file mismatches the expected
248 filename/ID, write_commit must still raise OSError (hard violation).
249 """
250 from muse.core.store import write_commit, read_commit
251
252 repo = _make_repo(tmp_path)
253 good_a = _good_commit(message="commit A", ts=_ts(2024))
254 good_b = _good_commit(message="commit B", ts=_ts(2025))
255
256 # Write commit B's data under commit A's path
257 (repo / ".muse" / "commits" / f"{good_a.commit_id}.msgpack").write_bytes(
258 msgpack.packb({
259 "commit_id": good_b.commit_id, # WRONG: different commit_id stored
260 "repo_id": "test-repo", "branch": "main",
261 "snapshot_id": good_b.snapshot_id,
262 "message": good_b.message,
263 "committed_at": good_b.committed_at.isoformat(),
264 "parent_commit_id": None, "parent2_commit_id": None,
265 "author": "gabriel", "metadata": {}, "format_version": 1, "reviewed_by": [],
266 }, use_bin_type=True)
267 )
268
269 with pytest.raises(OSError, match="Store integrity violation"):
270 write_commit(repo, good_a)
271
272
273 # =============================================================================
274 # 2. UNIT — write_snapshot must repair corrupt snapshots
275 # =============================================================================
276
277
278 class TestWriteSnapshotHashVerification:
279
280 def test_idempotent_skip_clean_snapshot(self, tmp_path: pathlib.Path) -> None:
281 """Regression: write_snapshot on a clean existing file still skips correctly."""
282 from muse.core.store import write_snapshot, read_snapshot
283
284 repo = _make_repo(tmp_path)
285 good = _good_snapshot()
286 write_snapshot(repo, good)
287 write_snapshot(repo, good) # second call: idempotent
288 result = read_snapshot(repo, good.snapshot_id)
289 assert result is not None
290 assert result.snapshot_id == good.snapshot_id
291
292 def test_repairs_corrupt_object_id_in_manifest(self, tmp_path: pathlib.Path) -> None:
293 """
294 A snapshot with a wrong object ID for a file must be repaired.
295
296 BUG: write_snapshot sees path.exists() → True → return immediately.
297 No parsing, no verification. read_snapshot then always returns None.
298
299 FIX: write_snapshot must parse and verify the existing file; overwrite
300 if corrupt.
301 """
302 from muse.core.store import write_snapshot, read_snapshot
303
304 repo = _make_repo(tmp_path)
305 good = _good_snapshot({"src/main.py": "c" * 64})
306 _write_corrupt_snapshot(repo, good, {"src/main.py": "0" * 64}) # wrong obj id
307
308 write_snapshot(repo, good)
309 result = read_snapshot(repo, good.snapshot_id)
310 assert result is not None, (
311 "BUG: write_snapshot did not repair corrupt snapshot manifest.\n"
312 "write_snapshot sees path.exists() → True → returns immediately with\n"
313 "no parsing or hash verification. Any corruption in the manifest is\n"
314 "permanent — read_snapshot will always return None for this snapshot.\n"
315 "FIX: write_snapshot must verify the existing file and overwrite if corrupt."
316 )
317 assert result.manifest == {"src/main.py": "c" * 64}
318
319 def test_repairs_extra_manifest_entry(self, tmp_path: pathlib.Path) -> None:
320 """An extra file in the manifest (changes the hash) must be repaired."""
321 from muse.core.store import write_snapshot, read_snapshot
322
323 repo = _make_repo(tmp_path)
324 good = _good_snapshot({"src/main.py": "c" * 64})
325 _write_corrupt_snapshot(repo, good, {
326 "src/main.py": "c" * 64,
327 "INJECTED_FILE.py": "e" * 64, # extra entry → wrong hash
328 })
329
330 write_snapshot(repo, good)
331 result = read_snapshot(repo, good.snapshot_id)
332 assert result is not None, "write_snapshot did not repair snapshot with extra manifest entry"
333 assert "INJECTED_FILE.py" not in result.manifest
334
335 def test_repairs_empty_manifest(self, tmp_path: pathlib.Path) -> None:
336 """A snapshot with an empty manifest (should have files) must be repaired."""
337 from muse.core.store import write_snapshot, read_snapshot
338
339 repo = _make_repo(tmp_path)
340 good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64})
341 _write_corrupt_snapshot(repo, good, {}) # manifest wiped
342
343 write_snapshot(repo, good)
344 result = read_snapshot(repo, good.snapshot_id)
345 assert result is not None, "write_snapshot did not repair empty manifest"
346 assert len(result.manifest) == 2
347
348 def test_repairs_missing_file_in_manifest(self, tmp_path: pathlib.Path) -> None:
349 """A snapshot with a missing file entry must be repaired."""
350 from muse.core.store import write_snapshot, read_snapshot
351
352 repo = _make_repo(tmp_path)
353 good = _good_snapshot({"src/main.py": "c" * 64, "src/utils.py": "d" * 64})
354 _write_corrupt_snapshot(repo, good, {"src/main.py": "c" * 64}) # utils.py missing
355
356 write_snapshot(repo, good)
357 result = read_snapshot(repo, good.snapshot_id)
358 assert result is not None, "write_snapshot did not repair snapshot with missing manifest entry"
359 assert "src/utils.py" in result.manifest
360
361
362 # =============================================================================
363 # 3. DATA INTEGRITY — full commit → snapshot chain
364 # =============================================================================
365
366
367 class TestCommitSnapshotChain:
368
369 def test_commit_and_snapshot_both_readable_after_repair(self, tmp_path: pathlib.Path) -> None:
370 """After repairing both commit and snapshot, the full chain is readable."""
371 from muse.core.store import write_commit, read_commit, write_snapshot, read_snapshot
372
373 repo = _make_repo(tmp_path)
374 manifest = {"src/main.py": "c" * 64}
375 good_snap = _good_snapshot(manifest)
376 good_commit = _good_commit(snapshot_id=good_snap.snapshot_id)
377
378 # Corrupt both
379 _write_corrupt_commit(repo, good_commit, {"snapshot_id": "0" * 64})
380 _write_corrupt_snapshot(repo, good_snap, {"src/main.py": "0" * 64})
381
382 # Repair both
383 write_commit(repo, good_commit)
384 write_snapshot(repo, good_snap)
385
386 # Full chain must be readable
387 commit = read_commit(repo, good_commit.commit_id)
388 assert commit is not None, "commit not readable after repair"
389
390 snap = read_snapshot(repo, commit.snapshot_id)
391 assert snap is not None, "snapshot not readable after repair"
392 assert snap.manifest == manifest
393
394 def test_parent_chain_survives_middle_commit_corruption(self, tmp_path: pathlib.Path) -> None:
395 """A→B→C chain: corrupt B's snapshot_id, repair, verify all three readable."""
396 from muse.core.store import write_commit, read_commit
397
398 repo = _make_repo(tmp_path)
399 commit_a = _good_commit(message="commit A", ts=_ts(2022))
400 commit_b = _good_commit(message="commit B", parent_commit_id=commit_a.commit_id, ts=_ts(2023))
401 commit_c = _good_commit(message="commit C", parent_commit_id=commit_b.commit_id, ts=_ts(2024))
402
403 # Write all three
404 write_commit(repo, commit_a)
405 write_commit(repo, commit_b)
406 write_commit(repo, commit_c)
407
408 # Corrupt B
409 _write_corrupt_commit(repo, commit_b, {"snapshot_id": "0" * 64})
410
411 # Repair B
412 write_commit(repo, commit_b)
413
414 # All three must be readable
415 for c in [commit_a, commit_b, commit_c]:
416 result = read_commit(repo, c.commit_id)
417 assert result is not None, f"Commit '{c.message}' not readable after repair"
418 assert result.commit_id == c.commit_id
419
420 def test_snapshot_repair_does_not_affect_sibling_snapshots(self, tmp_path: pathlib.Path) -> None:
421 """Repairing one snapshot must not corrupt or affect other snapshots."""
422 from muse.core.store import write_snapshot, read_snapshot
423
424 repo = _make_repo(tmp_path)
425 snap_a = _good_snapshot({"a.py": "a" * 64})
426 snap_b = _good_snapshot({"b.py": "b" * 64})
427 snap_c = _good_snapshot({"c.py": "c" * 64})
428
429 write_snapshot(repo, snap_a)
430 write_snapshot(repo, snap_b)
431 write_snapshot(repo, snap_c)
432
433 # Corrupt B
434 _write_corrupt_snapshot(repo, snap_b, {"b.py": "0" * 64})
435
436 # Repair B
437 write_snapshot(repo, snap_b)
438
439 # All three readable
440 for snap in [snap_a, snap_b, snap_c]:
441 result = read_snapshot(repo, snap.snapshot_id)
442 assert result is not None, f"Snapshot {snap.snapshot_id[:8]} not readable"
443
444
445 # =============================================================================
446 # 4. SECURITY — corrupt fields cannot forge content
447 # =============================================================================
448
449
450 class TestSecurityCorruptFields:
451
452 def test_corrupt_snapshot_id_in_commit_cannot_forge_different_snapshot(self, tmp_path: pathlib.Path) -> None:
453 """
454 An attacker who corrupts a commit's snapshot_id cannot make Muse check
455 out arbitrary content — the commit hash verification detects the change.
456 After write_commit repairs the file, the original snapshot_id is restored.
457 """
458 from muse.core.store import write_commit, read_commit
459
460 repo = _make_repo(tmp_path)
461 good = _good_commit(snapshot_id="b" * 64)
462 attacker_snapshot = "e" * 64 # attacker wants to point at different content
463
464 _write_corrupt_commit(repo, good, {"snapshot_id": attacker_snapshot})
465
466 write_commit(repo, good)
467 result = read_commit(repo, good.commit_id)
468 assert result is not None
469 assert result.snapshot_id == "b" * 64, (
470 f"Attacker's snapshot_id {attacker_snapshot[:8]!r} survived repair!"
471 )
472
473 def test_injected_manifest_entry_is_removed_on_snapshot_repair(self, tmp_path: pathlib.Path) -> None:
474 """
475 An injected file in the manifest (hash mismatch) must be removed on repair.
476 """
477 from muse.core.store import write_snapshot, read_snapshot
478
479 repo = _make_repo(tmp_path)
480 good = _good_snapshot({"src/main.py": "c" * 64})
481 _write_corrupt_snapshot(repo, good, {
482 "src/main.py": "c" * 64,
483 "malicious_backdoor.py": "e" * 64,
484 })
485
486 write_snapshot(repo, good)
487 result = read_snapshot(repo, good.snapshot_id)
488 assert result is not None
489 assert "malicious_backdoor.py" not in result.manifest
490
491
492 # =============================================================================
493 # 5. STRESS — concurrent writes all repair correctly
494 # =============================================================================
495
496
497 class TestStressConcurrentRepair:
498
499 def test_concurrent_write_commit_repairs_corrupt_snapshot_id(self, tmp_path: pathlib.Path) -> None:
500 """20 concurrent write_commit calls on a corrupt file all result in a readable commit."""
501 from muse.core.store import write_commit, read_commit
502
503 repo = _make_repo(tmp_path)
504 good = _good_commit()
505 _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64})
506
507 failures = []
508 lock = threading.Lock()
509
510 def worker() -> None:
511 write_commit(repo, good)
512
513 threads = [threading.Thread(target=worker) for _ in range(20)]
514 for t in threads:
515 t.start()
516 for t in threads:
517 t.join()
518
519 result = read_commit(repo, good.commit_id)
520 assert result is not None, (
521 "After 20 concurrent write_commit repairs, commit is still unreadable"
522 )
523 assert result.snapshot_id == good.snapshot_id
524
525 def test_concurrent_write_snapshot_repairs_corrupt_manifest(self, tmp_path: pathlib.Path) -> None:
526 """20 concurrent write_snapshot calls on a corrupt file all result in a readable snapshot."""
527 from muse.core.store import write_snapshot, read_snapshot
528
529 repo = _make_repo(tmp_path)
530 good = _good_snapshot()
531 _write_corrupt_snapshot(repo, good, {}) # empty manifest
532
533 threads = [threading.Thread(target=lambda: write_snapshot(repo, good)) for _ in range(20)]
534 for t in threads:
535 t.start()
536 for t in threads:
537 t.join()
538
539 result = read_snapshot(repo, good.snapshot_id)
540 assert result is not None, (
541 "After 20 concurrent write_snapshot repairs, snapshot is still unreadable"
542 )
543
544 def test_50_sequential_repairs_all_succeed(self, tmp_path: pathlib.Path) -> None:
545 """50 different commits each with corrupt snapshot_id, all repaired."""
546 from muse.core.store import write_commit, read_commit
547
548 for i in range(50):
549 repo = _make_repo(tmp_path / str(i))
550 good = _good_commit(message=f"commit {i}", ts=f"202{i % 10}-01-01T00:00:00+00:00")
551 _write_corrupt_commit(repo, good, {"snapshot_id": "0" * 64})
552 write_commit(repo, good)
553 result = read_commit(repo, good.commit_id)
554 assert result is not None, f"commit {i} not repaired"
555
556
557 # =============================================================================
558 # 6. REGRESSION — normal write paths still work
559 # =============================================================================
560
561
562 class TestRegression:
563
564 def test_write_commit_new_file_works(self, tmp_path: pathlib.Path) -> None:
565 from muse.core.store import write_commit, read_commit
566
567 repo = _make_repo(tmp_path)
568 good = _good_commit()
569 write_commit(repo, good)
570 assert read_commit(repo, good.commit_id) is not None
571
572 def test_write_snapshot_new_file_works(self, tmp_path: pathlib.Path) -> None:
573 from muse.core.store import write_snapshot, read_snapshot
574
575 repo = _make_repo(tmp_path)
576 good = _good_snapshot()
577 write_snapshot(repo, good)
578 assert read_snapshot(repo, good.snapshot_id) is not None
579
580 def test_write_commit_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None:
581 from muse.core.store import write_commit, read_commit
582
583 repo = _make_repo(tmp_path)
584 good = _good_commit()
585 write_commit(repo, good)
586 # Write 10 times — must stay idempotent
587 for _ in range(10):
588 write_commit(repo, good)
589 assert read_commit(repo, good.commit_id) is not None
590
591 def test_write_snapshot_idempotent_on_clean_file(self, tmp_path: pathlib.Path) -> None:
592 from muse.core.store import write_snapshot, read_snapshot
593
594 repo = _make_repo(tmp_path)
595 good = _good_snapshot()
596 write_snapshot(repo, good)
597 for _ in range(10):
598 write_snapshot(repo, good)
599 assert read_snapshot(repo, good.snapshot_id) is not None
File History 5 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago
sha256:50d413a635f57761c3fce1f70251b957b6423821525bf330747ef4007339222e Merge branch 'dev' into main Human 29 days ago
sha256:fb67fed5a4d3e40de84bdd163de94ef1386570bef1dd1a020a732c8a038962ce Merge branch 'dev' into main Human 48 days ago
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 101 days ago