gabriel / muse public
test_workdir_integrity.py python
1,206 lines 45.7 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Zero-data-loss workdir integrity tests.
2
3 What these tests cover
4 ----------------------
5 This suite was written after a real incident where the working tree diverged
6 from the committed snapshot. The root cause chain:
7
8 1. ``restore_object`` used ``shutil.copy2(src, dest)`` directly — not
9 atomic. A crash mid-copy could leave a corrupt destination file.
10 2. ``apply_manifest`` ignored the ``False`` return from ``restore_object``
11 when an object was absent from the store. The file was silently left at
12 its old content; no error surfaced.
13 3. ``_checkout_snapshot`` (incremental delta path) printed a warning when
14 an object was missing but continued — same silent data loss.
15 4. No post-operation integrity verification existed to catch any of the
16 above after the fact.
17
18 Fixes applied:
19 * ``restore_object`` — atomic write: temp file → ``os.replace``.
20 * ``apply_manifest`` — raises ``RuntimeError`` listing every missing object.
21 * ``_checkout_snapshot`` — raises ``SystemExit(INTERNAL_ERROR)`` on missing
22 object; never continues with a partial workdir.
23 * ``verify_workdir_integrity`` — new utility: full hash-based post-op audit.
24
25 Test categories
26 ---------------
27 I restore_object atomicity (temp+replace pattern).
28 II apply_manifest — missing object raises, not silently skips.
29 III verify_workdir_integrity — utility correctness.
30 IV checkout → workdir always matches target snapshot.
31 V fast-forward merge → workdir always matches target snapshot.
32 VI checkout aborts hard when an object is missing from the store.
33 VII Editor-cache simulation — status detects stale-cache workdir drift.
34 VIII Stress tests — 500-file repos, deep chains, diamond DAGs.
35 """
36
37 from __future__ import annotations
38
39 import hashlib
40 import json
41 import os
42 import pathlib
43 import shutil
44 import stat
45 import tempfile
46 import uuid
47
48 import pytest
49 from tests.cli_test_helper import CliRunner
50
51 from muse.core.object_store import object_path, restore_object, write_object
52 from muse.core.snapshot import walk_workdir
53 from muse.core.workdir import apply_manifest, verify_workdir_integrity
54 from muse.core._types import Manifest, blob_id, fake_id
55
56 type _EnvMap = dict[str, str]
57
58 runner = CliRunner()
59 cli = None # CliRunner ignores this positional
60
61
62 # ---------------------------------------------------------------------------
63 # Shared helpers
64 # ---------------------------------------------------------------------------
65
66
67 def _sha(content: bytes) -> str:
68 return blob_id(content)
69
70
71 def _env(root: pathlib.Path) -> _EnvMap:
72 return {"MUSE_REPO_ROOT": str(root)}
73
74
75 def _run(root: pathlib.Path, *args: str) -> tuple[int, str]:
76 final = list(args)
77 if final and final[0] == "merge" and "--force" not in final:
78 final.insert(1, "--force")
79 result = runner.invoke(cli, final, env=_env(root), catch_exceptions=False)
80 return result.exit_code, result.output
81
82
83 def _run_unchecked(root: pathlib.Path, *args: str) -> tuple[int, str]:
84 final = list(args)
85 if final and final[0] == "merge" and "--force" not in final:
86 final.insert(1, "--force")
87 result = runner.invoke(cli, final, env=_env(root))
88 return result.exit_code, result.output
89
90
91 def _store_object(root: pathlib.Path, content: bytes) -> str:
92 """Write *content* to the object store, return its object ID."""
93 oid = _sha(content)
94 write_object(root, oid, content)
95 return oid
96
97
98 def _object_path(root: pathlib.Path, oid: str) -> pathlib.Path:
99 return object_path(root, oid)
100
101
102 def _init_repo(tmp_path: pathlib.Path, domain: str = "code") -> tuple[pathlib.Path, str]:
103 muse_dir = tmp_path / ".muse"
104 muse_dir.mkdir()
105 repo_id = fake_id("repo")
106 (muse_dir / "repo.json").write_text(json.dumps({
107 "repo_id": repo_id,
108 "domain": domain,
109 "version": "1.0.0",
110 }))
111 (muse_dir / "refs" / "heads").mkdir(parents=True)
112 (muse_dir / "objects").mkdir()
113 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
114 return tmp_path, repo_id
115
116
117 def _make_commit(
118 root: pathlib.Path,
119 repo_id: str,
120 branch: str,
121 message: str,
122 manifest: Manifest,
123 ) -> str:
124 """Write objects, snapshot, and commit; update branch ref. Returns commit id."""
125 import msgpack
126
127 from muse.core.snapshot import compute_snapshot_id, compute_commit_id
128 from muse.core.store import read_commit
129 from muse.core._types import now_utc_iso
130
131 snap_id = compute_snapshot_id(manifest)
132 snap_path = root / ".muse" / "snapshots" / f"{snap_id}.msgpack"
133 snap_path.parent.mkdir(exist_ok=True)
134 snap_path.write_bytes(msgpack.packb({
135 "snapshot_id": snap_id,
136 "manifest": manifest,
137 }, use_bin_type=True))
138
139 ref_path = root / ".muse" / "refs" / "heads" / branch
140 parent_id: str | None = ref_path.read_text().strip() if ref_path.exists() else None
141
142 committed_at = now_utc_iso()
143 commit_id = compute_commit_id(
144 parent_ids=[parent_id] if parent_id else [],
145 snapshot_id=snap_id,
146 message=message,
147 committed_at_iso=committed_at,
148 )
149
150 commits_dir = root / ".muse" / "commits"
151 commits_dir.mkdir(exist_ok=True)
152 commit_path = commits_dir / f"{commit_id}.msgpack"
153 commit_path.write_bytes(msgpack.packb({
154 "commit_id": commit_id,
155 "repo_id": repo_id,
156 "branch": branch,
157 "snapshot_id": snap_id,
158 "message": message,
159 "committed_at": committed_at,
160 "parent_commit_id": parent_id,
161 "parent2_commit_id": None,
162 "author": "test",
163 "metadata": {},
164 "structured_delta": None,
165 "sem_ver_bump": "none",
166 "breaking_changes": [],
167 "agent_id": "",
168 "model_id": "",
169 "toolchain_id": "",
170 "prompt_hash": "",
171 "signature": "",
172 "signer_key_id": "",
173 }, use_bin_type=True))
174 ref_path.parent.mkdir(parents=True, exist_ok=True)
175 ref_path.write_text(commit_id)
176 return commit_id
177
178
179 def _head_manifest(root: pathlib.Path, branch: str) -> Manifest:
180 from muse.core.store import read_commit, read_snapshot
181 ref = (root / ".muse" / "refs" / "heads" / branch).read_text().strip()
182 cr = read_commit(root, ref)
183 assert cr is not None
184 sr = read_snapshot(root, cr.snapshot_id)
185 assert sr is not None
186 return dict(sr.manifest)
187
188
189 def _write_disk(root: pathlib.Path, rel_path: str, content: bytes) -> str:
190 """Write content to disk AND store it; return object id."""
191 fp = root / rel_path
192 fp.parent.mkdir(parents=True, exist_ok=True)
193 fp.write_bytes(content)
194 return _store_object(root, content)
195
196
197 # ===========================================================================
198 # I restore_object atomicity
199 # ===========================================================================
200
201
202 class TestRestoreObjectAtomicityI:
203 """restore_object must use atomic writes so a crash mid-copy never
204 leaves a partial file at the destination."""
205
206 def test_I1_successful_restore_produces_correct_content(
207 self, tmp_path: pathlib.Path
208 ) -> None:
209 """I1: happy-path restore writes exact bytes to dest."""
210 root, _ = _init_repo(tmp_path)
211 content = b"hello world\n" * 100
212 oid = _store_object(root, content)
213 dest = tmp_path / "out.bin"
214 assert restore_object(root, oid, dest)
215 assert dest.read_bytes() == content
216
217 def test_I2_restore_overwrites_existing_file(self, tmp_path: pathlib.Path) -> None:
218 """I2: restore replaces whatever was at dest (no skip-if-exists)."""
219 root, _ = _init_repo(tmp_path)
220 old = b"old content\n"
221 new = b"new content\n"
222 dest = tmp_path / "f.txt"
223 dest.write_bytes(old)
224
225 oid = _store_object(root, new)
226 assert restore_object(root, oid, dest)
227 assert dest.read_bytes() == new
228
229 def test_I3_restore_missing_object_returns_false_does_not_touch_dest(
230 self, tmp_path: pathlib.Path
231 ) -> None:
232 """I3: missing object → False, pre-existing dest left intact."""
233 root, _ = _init_repo(tmp_path)
234 sentinel = b"sentinel\n"
235 dest = tmp_path / "existing.txt"
236 dest.write_bytes(sentinel)
237
238 fake_oid = _sha(b"nonexistent")
239 assert not restore_object(root, fake_oid, dest)
240 assert dest.read_bytes() == sentinel
241
242 def test_I4_restore_creates_parent_directories(
243 self, tmp_path: pathlib.Path
244 ) -> None:
245 """I4: dest parent dirs are created automatically."""
246 root, _ = _init_repo(tmp_path)
247 content = b"deep\n"
248 oid = _store_object(root, content)
249 dest = tmp_path / "a" / "b" / "c" / "deep.txt"
250 assert not dest.parent.exists()
251 assert restore_object(root, oid, dest)
252 assert dest.read_bytes() == content
253
254 def test_I5_atomic_write_leaves_no_tmp_file_on_success(
255 self, tmp_path: pathlib.Path
256 ) -> None:
257 """I5: after a successful restore no .restore-tmp-* file lingers."""
258 root, _ = _init_repo(tmp_path)
259 content = b"data\n"
260 oid = _store_object(root, content)
261 dest = tmp_path / "target.txt"
262 restore_object(root, oid, dest)
263 tmps = list(tmp_path.glob(".restore-tmp-*"))
264 assert tmps == [], f"Stale tmp files found: {tmps}"
265
266 def test_I6_restore_hash_after_restore_matches_object_id(
267 self, tmp_path: pathlib.Path
268 ) -> None:
269 """I6: the restored file's SHA-256 matches the object_id exactly."""
270 root, _ = _init_repo(tmp_path)
271 content = b"integrity\n" * 1000
272 oid = _store_object(root, content)
273 dest = tmp_path / "verified.bin"
274 restore_object(root, oid, dest)
275 actual = _sha(dest.read_bytes())
276 assert actual == oid, f"Hash mismatch after restore: {actual[:8]} ≠ {oid[:8]}"
277
278 def test_I7_restore_large_file_correct_content(
279 self, tmp_path: pathlib.Path
280 ) -> None:
281 """I7: 10 MiB blob survives a round-trip through the object store."""
282 root, _ = _init_repo(tmp_path)
283 content = os.urandom(10 * 1024 * 1024)
284 oid = _store_object(root, content)
285 dest = tmp_path / "large.bin"
286 assert restore_object(root, oid, dest)
287 assert dest.read_bytes() == content
288
289
290 # ===========================================================================
291 # II apply_manifest — missing object must raise, never silently skip
292 # ===========================================================================
293
294
295 class TestApplyManifestMissingObjectII:
296 """apply_manifest must fail loudly when any object is absent."""
297
298 def test_II1_missing_single_object_raises_runtime_error(
299 self, tmp_path: pathlib.Path
300 ) -> None:
301 """II1: one missing object → RuntimeError, not silent skip."""
302 root, _ = _init_repo(tmp_path)
303 fake_oid = _sha(b"ghost")
304 with pytest.raises(RuntimeError, match="missing from the local store"):
305 apply_manifest(root, {}, {"ghost.txt": fake_oid})
306
307 def test_II2_error_message_names_the_missing_path(
308 self, tmp_path: pathlib.Path
309 ) -> None:
310 """II2: the error message includes the missing path name."""
311 root, _ = _init_repo(tmp_path)
312 fake_oid = _sha(b"abc")
313 with pytest.raises(RuntimeError) as exc_info:
314 apply_manifest(root, {}, {"crucial/file.py": fake_oid})
315 assert "crucial/file.py" in str(exc_info.value)
316
317 def test_II3_partial_manifest_some_missing_raises(
318 self, tmp_path: pathlib.Path
319 ) -> None:
320 """II3: when one of N files is missing the whole call raises."""
321 root, _ = _init_repo(tmp_path)
322 good_oid = _write_disk(root, "exists.txt", b"ok\n")
323 bad_oid = _sha(b"not stored")
324 with pytest.raises(RuntimeError):
325 apply_manifest(root, {}, {"exists.txt": good_oid, "missing.txt": bad_oid})
326
327 def test_II4_all_objects_present_succeeds(
328 self, tmp_path: pathlib.Path
329 ) -> None:
330 """II4: when every object is in the store apply_manifest succeeds."""
331 root, _ = _init_repo(tmp_path)
332 oid_a = _write_disk(root, "a.txt", b"aaa\n")
333 oid_b = _write_disk(root, "b.txt", b"bbb\n")
334 (root / "a.txt").unlink()
335 (root / "b.txt").unlink()
336 apply_manifest(root, {}, {"a.txt": oid_a, "b.txt": oid_b})
337 assert (root / "a.txt").read_bytes() == b"aaa\n"
338 assert (root / "b.txt").read_bytes() == b"bbb\n"
339
340 def test_II5_multiple_missing_reported_in_error(
341 self, tmp_path: pathlib.Path
342 ) -> None:
343 """II5: error message covers multiple missing files."""
344 root, _ = _init_repo(tmp_path)
345 manifest = {f"f{i}.py": _sha(f"fake{i}".encode()) for i in range(10)}
346 with pytest.raises(RuntimeError) as exc_info:
347 apply_manifest(root, {}, manifest)
348 msg = str(exc_info.value)
349 assert "10 object(s)" in msg
350
351 def test_II6_apply_manifest_removes_files_not_in_target(
352 self, tmp_path: pathlib.Path
353 ) -> None:
354 """II6: tracked files absent from target manifest are deleted."""
355 root, _ = _init_repo(tmp_path)
356 keep_oid = _write_disk(root, "keep.txt", b"keep\n")
357 del_oid = _store_object(root, b"delete\n")
358 (root / "delete_me.txt").write_bytes(b"delete\n")
359 # delete_me.txt is in prev_manifest (was tracked) but not in target — must be removed
360 apply_manifest(
361 root,
362 {"keep.txt": keep_oid, "delete_me.txt": del_oid},
363 {"keep.txt": keep_oid},
364 )
365 assert not (root / "delete_me.txt").exists()
366 assert (root / "keep.txt").exists()
367
368 def test_II7_empty_manifest_non_empty_prev_raises_value_error(
369 self, tmp_path: pathlib.Path
370 ) -> None:
371 """II7: data-loss guard — empty target with non-empty prev_manifest raises ValueError."""
372 root, _ = _init_repo(tmp_path)
373 oid = _store_object(root, b"data\n")
374 with pytest.raises(ValueError, match="empty target_manifest"):
375 apply_manifest(root, {"file.txt": oid}, {})
376
377
378 # ===========================================================================
379 # III verify_workdir_integrity — utility correctness
380 # ===========================================================================
381
382
383 class TestVerifyWorkdirIntegrityIII:
384 """verify_workdir_integrity must catch every form of workdir drift."""
385
386 def test_III1_clean_workdir_returns_empty_list(
387 self, tmp_path: pathlib.Path
388 ) -> None:
389 """III1: workdir matches manifest → no mismatches."""
390 root, _ = _init_repo(tmp_path)
391 oid = _write_disk(root, "a.py", b"x = 1\n")
392 mismatches = verify_workdir_integrity(root, {"a.py": oid})
393 assert mismatches == []
394
395 def test_III2_modified_file_detected(self, tmp_path: pathlib.Path) -> None:
396 """III2: externally modified file shows up as mismatch."""
397 root, _ = _init_repo(tmp_path)
398 original = b"original\n"
399 oid = _write_disk(root, "f.py", original)
400
401 (root / "f.py").write_bytes(b"tampered\n")
402 mismatches = verify_workdir_integrity(root, {"f.py": oid})
403 assert len(mismatches) == 1
404 path, expected, actual = mismatches[0]
405 assert path == "f.py"
406 assert expected == oid
407 assert actual != oid
408 assert actual is not None
409
410 def test_III3_missing_file_detected(self, tmp_path: pathlib.Path) -> None:
411 """III3: file present in manifest but absent from disk → mismatch."""
412 root, _ = _init_repo(tmp_path)
413 oid = _write_disk(root, "gone.py", b"gone\n")
414 (root / "gone.py").unlink()
415 mismatches = verify_workdir_integrity(root, {"gone.py": oid})
416 assert any(m[0] == "gone.py" and m[2] is None for m in mismatches)
417
418 def test_III4_extra_tracked_file_detected(
419 self, tmp_path: pathlib.Path
420 ) -> None:
421 """III4: file on disk but not in manifest is also reported."""
422 root, _ = _init_repo(tmp_path)
423 oid = _write_disk(root, "tracked.py", b"ok\n")
424 (root / "extra.py").write_bytes(b"extra\n")
425 mismatches = verify_workdir_integrity(root, {"tracked.py": oid})
426 extras = [m for m in mismatches if m[0] == "extra.py"]
427 assert extras, "Extra file not reported"
428
429 def test_III5_empty_manifest_empty_workdir_clean(
430 self, tmp_path: pathlib.Path
431 ) -> None:
432 """III5: both manifest and workdir empty → clean."""
433 root, _ = _init_repo(tmp_path)
434 assert verify_workdir_integrity(root, {}) == []
435
436 def test_III6_multiple_mismatches_all_reported(
437 self, tmp_path: pathlib.Path
438 ) -> None:
439 """III6: all mismatches returned, not just the first."""
440 root, _ = _init_repo(tmp_path)
441 manifest: Manifest = {}
442 for i in range(20):
443 oid = _write_disk(root, f"f{i}.py", f"v={i}\n".encode())
444 manifest[f"f{i}.py"] = oid
445
446 # Tamper with half the files
447 for i in range(0, 20, 2):
448 (root / f"f{i}.py").write_bytes(b"tampered\n")
449
450 mismatches = verify_workdir_integrity(root, manifest)
451 assert len(mismatches) == 10, f"Expected 10 mismatches, got {len(mismatches)}"
452
453 def test_III7_correct_content_after_apply_manifest(
454 self, tmp_path: pathlib.Path
455 ) -> None:
456 """III7: after apply_manifest, verify_workdir_integrity is clean."""
457 root, _ = _init_repo(tmp_path)
458 oid_a = _write_disk(root, "a.py", b"a = 1\n")
459 oid_b = _write_disk(root, "b.py", b"b = 2\n")
460 (root / "a.py").unlink()
461 (root / "b.py").unlink()
462
463 apply_manifest(root, {}, {"a.py": oid_a, "b.py": oid_b})
464 mismatches = verify_workdir_integrity(root, {"a.py": oid_a, "b.py": oid_b})
465 assert mismatches == [], f"Expected clean after apply_manifest: {mismatches}"
466
467
468 # ===========================================================================
469 # IV checkout → workdir always matches the target snapshot
470 # ===========================================================================
471
472
473 class TestCheckoutWorkdirIntegrityIV:
474 """After muse checkout, the working tree must byte-for-byte match
475 the target branch's committed snapshot. No exceptions."""
476
477 def _full_roundtrip(self, tmp_path: pathlib.Path, n_files: int) -> None:
478 """Create two branches with different content, checkout between them,
479 verify integrity on each switch."""
480 root, repo_id = _init_repo(tmp_path)
481 code, _ = _run(root, "init", str(root))
482
483 main_manifest: Manifest = {}
484 for i in range(n_files):
485 oid = _write_disk(root, f"src/m{i}.py", f"# main {i}\n".encode())
486 main_manifest[f"src/m{i}.py"] = oid
487 code, out = _run(root, "commit", "--allow-empty", "-m", "main files")
488 assert code == 0, out
489
490 code, out = _run(root, "branch", "feat")
491 assert code == 0, out
492 code, out = _run(root, "checkout", "feat")
493 assert code == 0, out
494
495 feat_manifest: Manifest = {}
496 for i in range(n_files):
497 oid = _write_disk(root, f"src/f{i}.py", f"# feat {i}\n".encode())
498 feat_manifest[f"src/f{i}.py"] = oid
499 code, out = _run(root, "commit", "-m", "feat files")
500 assert code == 0, out
501
502 # Switch back to main — verify clean
503 code, out = _run(root, "checkout", "main")
504 assert code == 0, out
505 main_snap = _head_manifest(root, "main")
506 mismatches = verify_workdir_integrity(root, main_snap)
507 assert mismatches == [], (
508 f"DATA LOSS: {len(mismatches)} mismatch(es) after checkout main:\n"
509 + "\n".join(f" {m}" for m in mismatches[:5])
510 )
511
512 # Switch to feat — verify clean
513 code, out = _run(root, "checkout", "feat")
514 assert code == 0, out
515 feat_snap = _head_manifest(root, "feat")
516 mismatches = verify_workdir_integrity(root, feat_snap)
517 assert mismatches == [], (
518 f"DATA LOSS: {len(mismatches)} mismatch(es) after checkout feat:\n"
519 + "\n".join(f" {m}" for m in mismatches[:5])
520 )
521
522 def test_IV1_checkout_10_files_workdir_matches_snapshot(
523 self, tmp_path: pathlib.Path
524 ) -> None:
525 """IV1: 10-file repo checkout — workdir matches target snapshot."""
526 self._full_roundtrip(tmp_path, 10)
527
528 def test_IV2_checkout_50_files_workdir_matches_snapshot(
529 self, tmp_path: pathlib.Path
530 ) -> None:
531 """IV2: 50-file repo checkout — workdir matches target snapshot."""
532 self._full_roundtrip(tmp_path, 50)
533
534 def test_IV3_repeated_checkout_workdir_consistent(
535 self, tmp_path: pathlib.Path
536 ) -> None:
537 """IV3: switching back and forth 10 times never corrupts the workdir."""
538 root, repo_id = _init_repo(tmp_path)
539 _run(root, "init", str(root))
540
541 oid_a = _write_disk(root, "f.py", b"version_a\n")
542 _run(root, "commit", "--allow-empty", "-m", "main")
543 _run(root, "branch", "feat")
544 _run(root, "checkout", "feat")
545 oid_b = _write_disk(root, "f.py", b"version_b\n")
546 _run(root, "commit", "-m", "feat")
547
548 main_snap = _head_manifest(root, "main")
549 feat_snap = _head_manifest(root, "feat")
550
551 for i in range(10):
552 branch = "main" if i % 2 == 0 else "feat"
553 _run(root, "checkout", branch)
554 expected = main_snap if branch == "main" else feat_snap
555 mismatches = verify_workdir_integrity(root, expected)
556 assert mismatches == [], (
557 f"Iteration {i}: mismatch after checkout {branch}: {mismatches}"
558 )
559
560 def test_IV4_checkout_restores_file_modified_between_branches(
561 self, tmp_path: pathlib.Path
562 ) -> None:
563 """IV4: a file modified on one branch is correctly restored when
564 switching to a branch that has the original version."""
565 root, repo_id = _init_repo(tmp_path)
566 _run(root, "init", str(root))
567
568 oid_v1 = _write_disk(root, "shared.py", b"# v1\n")
569 _run(root, "commit", "--allow-empty", "-m", "base")
570 _run(root, "branch", "feat")
571
572 oid_v2 = _write_disk(root, "shared.py", b"# v2\n")
573 _run(root, "commit", "-m", "main v2")
574
575 _run(root, "checkout", "feat")
576 assert (root / "shared.py").read_bytes() == b"# v1\n", (
577 "feat branch should have v1 of shared.py"
578 )
579
580 _run(root, "checkout", "main")
581 assert (root / "shared.py").read_bytes() == b"# v2\n", (
582 "main branch should have v2 of shared.py"
583 )
584
585 def test_IV5_checkout_deletes_files_not_in_target_branch(
586 self, tmp_path: pathlib.Path
587 ) -> None:
588 """IV5: files added on one branch are deleted when switching away."""
589 root, repo_id = _init_repo(tmp_path)
590 _run(root, "init", str(root))
591
592 _write_disk(root, "base.py", b"base\n")
593 _run(root, "commit", "--allow-empty", "-m", "base")
594 _run(root, "branch", "feat")
595 _run(root, "checkout", "feat")
596
597 _write_disk(root, "feat_only.py", b"feat\n")
598 _run(root, "commit", "-m", "feat_only")
599
600 _run(root, "checkout", "main")
601 assert not (root / "feat_only.py").exists(), (
602 "feat-only file should not exist on main branch"
603 )
604
605
606 # ===========================================================================
607 # V fast-forward merge → workdir always matches target snapshot
608 # ===========================================================================
609
610
611 class TestFFMergeWorkdirIntegrityV:
612 """A fast-forward merge must update the working tree to match the
613 incoming branch's snapshot — ALL files, not just the delta."""
614
615 def test_V1_ff_merge_all_files_restored(self, tmp_path: pathlib.Path) -> None:
616 """V1: after FF merge every file in the target manifest is on disk
617 with the correct content."""
618 root, repo_id = _init_repo(tmp_path)
619 _run(root, "init", str(root))
620
621 _write_disk(root, "base.py", b"base\n")
622 _run(root, "commit", "--allow-empty", "-m", "base")
623 _run(root, "branch", "feat")
624 _run(root, "checkout", "feat")
625
626 manifest: Manifest = {}
627 for i in range(30):
628 oid = _write_disk(root, f"f{i}.py", f"# feat {i}\n".encode())
629 manifest[f"f{i}.py"] = oid
630 _run(root, "commit", "-m", "feat 30 files")
631
632 _run(root, "checkout", "main")
633 code, out = _run(root, "merge", "feat")
634 assert code == 0, out
635
636 merged = _head_manifest(root, "main")
637 mismatches = verify_workdir_integrity(root, merged)
638 assert mismatches == [], (
639 f"DATA LOSS after FF merge: {len(mismatches)} mismatch(es)\n"
640 + "\n".join(f" {m}" for m in mismatches[:5])
641 )
642
643 def test_V2_ff_merge_correct_content_not_just_present(
644 self, tmp_path: pathlib.Path
645 ) -> None:
646 """V2: FF merge writes the correct *content*, not just the correct filename."""
647 root, repo_id = _init_repo(tmp_path)
648 _run(root, "init", str(root))
649
650 old_content = b"# old version\n"
651 new_content = b"# new version\n"
652
653 _write_disk(root, "important.py", old_content)
654 _run(root, "commit", "--allow-empty", "-m", "base")
655 _run(root, "branch", "feat")
656 _run(root, "checkout", "feat")
657
658 _write_disk(root, "important.py", new_content)
659 _run(root, "commit", "-m", "update important.py")
660
661 _run(root, "checkout", "main")
662 # File on disk is now old_content
663 assert (root / "important.py").read_bytes() == old_content
664
665 _run(root, "merge", "feat")
666 # File on disk must now be new_content
667 actual = (root / "important.py").read_bytes()
668 assert actual == new_content, (
669 f"FF merge did not restore correct content. "
670 f"Expected {new_content!r}, got {actual!r}"
671 )
672
673 def test_V3_ff_merge_workdir_matches_snapshot_byte_for_byte(
674 self, tmp_path: pathlib.Path
675 ) -> None:
676 """V3: verify_workdir_integrity confirms zero drift after FF merge."""
677 root, repo_id = _init_repo(tmp_path)
678 _run(root, "init", str(root))
679
680 _write_disk(root, "a.py", b"a\n")
681 _run(root, "commit", "--allow-empty", "-m", "base")
682 _run(root, "branch", "feat")
683 _run(root, "checkout", "feat")
684
685 for i in range(20):
686 _write_disk(root, f"feat_{i}.py", f"feat{i}\n".encode())
687 _run(root, "commit", "-m", "feat 20 files")
688
689 _run(root, "checkout", "main")
690 _run(root, "merge", "feat")
691
692 merged_snap = _head_manifest(root, "main")
693 mismatches = verify_workdir_integrity(root, merged_snap)
694 assert mismatches == []
695
696
697 # ===========================================================================
698 # VI checkout aborts hard when an object is missing from the store
699 # ===========================================================================
700
701
702 class TestCheckoutMissingObjectVI:
703 """Checkout must refuse to proceed when an object it needs is absent
704 from the local object store. The partial-checkout silent-data-loss
705 path is now closed."""
706
707 def test_VI1_checkout_to_branch_with_missing_object_aborts(
708 self, tmp_path: pathlib.Path
709 ) -> None:
710 """VI1: if a required object is purged from the store, checkout
711 exits non-zero and does NOT silently leave the workdir in a
712 partially restored state."""
713 root, repo_id = _init_repo(tmp_path)
714 _run(root, "init", str(root))
715
716 _write_disk(root, "base.py", b"base\n")
717 _run(root, "commit", "--allow-empty", "-m", "base")
718 _run(root, "branch", "feat")
719 _run(root, "checkout", "feat")
720
721 content = b"# feat file\n"
722 oid = _write_disk(root, "feat.py", content)
723 _run(root, "commit", "-m", "feat")
724 _run(root, "checkout", "main")
725
726 # Purge the feat.py object from the store — simulates a corruption
727 obj = _object_path(root, oid)
728 obj.unlink()
729
730 code, out = _run_unchecked(root, "checkout", "feat")
731 assert code != 0, (
732 "Checkout should fail when a required object is missing from the store"
733 )
734
735 def test_VI2_apply_manifest_raises_on_missing_object_not_silent(
736 self, tmp_path: pathlib.Path
737 ) -> None:
738 """VI2: apply_manifest raises RuntimeError (not returns None or logs
739 a warning) when an object is missing."""
740 root, _ = _init_repo(tmp_path)
741 ghost_oid = _sha(b"not in store")
742 with pytest.raises(RuntimeError) as exc_info:
743 apply_manifest(root, {}, {"ghost.py": ghost_oid})
744 assert "missing from the local store" in str(exc_info.value)
745
746 def test_VI3_ff_merge_aborts_if_incoming_object_missing(
747 self, tmp_path: pathlib.Path
748 ) -> None:
749 """VI3: FF merge aborts if an object from the target snapshot is
750 not in the local store."""
751 root, repo_id = _init_repo(tmp_path)
752 _run(root, "init", str(root))
753
754 _write_disk(root, "base.py", b"base\n")
755 _run(root, "commit", "--allow-empty", "-m", "base")
756 _run(root, "branch", "feat")
757 _run(root, "checkout", "feat")
758
759 content = b"# critical\n"
760 oid = _write_disk(root, "critical.py", content)
761 _run(root, "commit", "-m", "critical")
762 _run(root, "checkout", "main")
763
764 # Purge the object after committing
765 _object_path(root, oid).unlink()
766
767 code, out = _run_unchecked(root, "merge", "feat")
768 assert code != 0, "FF merge should fail when a target object is missing"
769
770
771 # ===========================================================================
772 # VII Editor-cache simulation — status must detect stale-cache workdir drift
773 # ===========================================================================
774
775
776 class TestEditorCacheSimulationVII:
777 """Simulates the exact incident: the editor had a cached (stale) version
778 of a file. After a merge updated the on-disk file, the editor wrote the
779 stale version back, corrupting the workdir.
780
781 Muse cannot prevent an editor from writing stale data, but it CAN detect
782 the drift via `muse status` (which compares workdir hashes against HEAD)
783 and via `verify_workdir_integrity`.
784 """
785
786 def test_VII1_status_detects_workdir_drift_after_external_write(
787 self, tmp_path: pathlib.Path
788 ) -> None:
789 """VII1: if a file is externally overwritten to an old version,
790 muse status reports it as modified."""
791 root, repo_id = _init_repo(tmp_path)
792 _run(root, "init", str(root))
793
794 old_content = b"# version 1\n"
795 new_content = b"# version 2\n"
796
797 _write_disk(root, "plugin.py", old_content)
798 _run(root, "commit", "--allow-empty", "-m", "v1")
799 _run(root, "branch", "feat")
800 _run(root, "checkout", "feat")
801
802 _write_disk(root, "plugin.py", new_content)
803 _run(root, "commit", "-m", "v2")
804 _run(root, "checkout", "main")
805 _run(root, "merge", "feat")
806
807 # HEAD now says plugin.py = new_content.
808 # Simulate editor writing back the stale old version.
809 (root / "plugin.py").write_bytes(old_content)
810
811 code, out = _run(root, "status")
812 assert code == 0
813 assert "plugin.py" in out, (
814 "muse status must report plugin.py as modified after stale write"
815 )
816
817 def test_VII2_verify_workdir_integrity_catches_stale_editor_write(
818 self, tmp_path: pathlib.Path
819 ) -> None:
820 """VII2: verify_workdir_integrity spots the stale-editor-cache corruption."""
821 root, repo_id = _init_repo(tmp_path)
822 _run(root, "init", str(root))
823
824 old_content = b"# old\n"
825 new_content = b"# new\n"
826
827 oid_new = _write_disk(root, "plugin.py", old_content)
828 _run(root, "commit", "--allow-empty", "-m", "base")
829 _run(root, "branch", "feat")
830 _run(root, "checkout", "feat")
831
832 oid_new = _write_disk(root, "plugin.py", new_content)
833 _run(root, "commit", "-m", "feat")
834 _run(root, "checkout", "main")
835 _run(root, "merge", "feat")
836
837 # Simulate editor stale write
838 (root / "plugin.py").write_bytes(old_content)
839
840 head_snap = _head_manifest(root, "main")
841 mismatches = verify_workdir_integrity(root, head_snap)
842 assert any(m[0] == "plugin.py" for m in mismatches), (
843 "verify_workdir_integrity must detect the stale file"
844 )
845
846 def test_VII3_correct_state_after_reapplying_manifest(
847 self, tmp_path: pathlib.Path
848 ) -> None:
849 """VII3: after detecting stale-editor drift, re-applying the manifest
850 restores correct state and verify_workdir_integrity is clean."""
851 root, repo_id = _init_repo(tmp_path)
852 _run(root, "init", str(root))
853
854 _write_disk(root, "plugin.py", b"# old\n")
855 _run(root, "commit", "--allow-empty", "-m", "base")
856 _run(root, "branch", "feat")
857 _run(root, "checkout", "feat")
858
859 _write_disk(root, "plugin.py", b"# new\n")
860 _run(root, "commit", "-m", "feat")
861 _run(root, "checkout", "main")
862 _run(root, "merge", "feat")
863
864 # Stale write
865 (root / "plugin.py").write_bytes(b"# stale\n")
866
867 head_snap = _head_manifest(root, "main")
868 # Repair by re-applying the manifest
869 apply_manifest(root, head_snap, head_snap)
870 mismatches = verify_workdir_integrity(root, head_snap)
871 assert mismatches == [], "After re-applying manifest, workdir must be clean"
872
873
874 # ===========================================================================
875 # VIII Stress tests
876 # ===========================================================================
877
878
879 class TestStressWorkdirVIII:
880 """High-volume, adversarial scenarios to eliminate the entire class of
881 workdir corruption bugs."""
882
883 def test_VIII1_500_file_checkout_all_match_snapshot(
884 self, tmp_path: pathlib.Path
885 ) -> None:
886 """VIII1: 500-file repo — every file matches the committed snapshot
887 after every checkout."""
888 root, repo_id = _init_repo(tmp_path)
889 _run(root, "init", str(root))
890
891 manifest_main: Manifest = {}
892 for i in range(500):
893 oid = _write_disk(root, f"main_{i:04d}.py", f"# main {i}\n".encode())
894 manifest_main[f"main_{i:04d}.py"] = oid
895 code, out = _run(root, "commit", "--allow-empty", "-m", "main 500")
896 assert code == 0, out
897
898 _run(root, "branch", "feat")
899 _run(root, "checkout", "feat")
900
901 manifest_feat: Manifest = {}
902 for i in range(500):
903 oid = _write_disk(root, f"feat_{i:04d}.py", f"# feat {i}\n".encode())
904 manifest_feat[f"feat_{i:04d}.py"] = oid
905 code, out = _run(root, "commit", "-m", "feat 500")
906 assert code == 0, out
907
908 _run(root, "checkout", "main")
909 snp = _head_manifest(root, "main")
910 mismatches = verify_workdir_integrity(root, snp)
911 assert mismatches == [], f"{len(mismatches)} mismatch(es) on main"
912
913 _run(root, "checkout", "feat")
914 snp = _head_manifest(root, "feat")
915 mismatches = verify_workdir_integrity(root, snp)
916 assert mismatches == [], f"{len(mismatches)} mismatch(es) on feat"
917
918 def test_VIII2_ff_merge_500_files_all_correct(
919 self, tmp_path: pathlib.Path
920 ) -> None:
921 """VIII2: FF merge with 500 incoming files — all correct after merge."""
922 root, repo_id = _init_repo(tmp_path)
923 _run(root, "init", str(root))
924
925 _write_disk(root, "base.py", b"base\n")
926 _run(root, "commit", "--allow-empty", "-m", "base")
927 _run(root, "branch", "feat")
928 _run(root, "checkout", "feat")
929
930 expected: Manifest = {}
931 for i in range(500):
932 content = f"# file {i} unique content {uuid.uuid4()}\n".encode()
933 oid = _write_disk(root, f"feat_{i:04d}.py", content)
934 expected[f"feat_{i:04d}.py"] = oid
935 _run(root, "commit", "-m", "feat 500")
936
937 _run(root, "checkout", "main")
938 code, out = _run(root, "merge", "feat")
939 assert code == 0, out
940
941 merged = _head_manifest(root, "main")
942 mismatches = verify_workdir_integrity(root, merged)
943 assert mismatches == [], (
944 f"DATA LOSS: {len(mismatches)} file(s) wrong after 500-file FF merge"
945 )
946
947 def test_VIII3_alternating_checkout_never_drifts(
948 self, tmp_path: pathlib.Path
949 ) -> None:
950 """VIII3: 20 alternating checkout cycles — workdir never drifts."""
951 root, repo_id = _init_repo(tmp_path)
952 _run(root, "init", str(root))
953
954 oid_a = _write_disk(root, "shared.py", b"# version A\n")
955 _run(root, "commit", "--allow-empty", "-m", "main")
956 snap_main = _head_manifest(root, "main")
957
958 _run(root, "branch", "feat")
959 _run(root, "checkout", "feat")
960 oid_b = _write_disk(root, "shared.py", b"# version B\n")
961 _run(root, "commit", "-m", "feat")
962 snap_feat = _head_manifest(root, "feat")
963
964 for cycle in range(20):
965 branch = "main" if cycle % 2 == 0 else "feat"
966 code, out = _run(root, "checkout", branch)
967 assert code == 0, f"Cycle {cycle}: checkout {branch} failed"
968 expected = snap_main if branch == "main" else snap_feat
969 mismatches = verify_workdir_integrity(root, expected)
970 assert mismatches == [], (
971 f"Cycle {cycle}, branch {branch}: {len(mismatches)} mismatch(es)"
972 )
973
974 def test_VIII4_deep_chain_checkout_base_restores_correctly(
975 self, tmp_path: pathlib.Path
976 ) -> None:
977 """VIII4: deep commit chain — checkout of base commit restores correctly."""
978 root, repo_id = _init_repo(tmp_path)
979 _run(root, "init", str(root))
980
981 # Build a chain of 20 commits, each modifying the same file
982 for i in range(20):
983 _write_disk(root, "evolving.py", f"# iteration {i}\n".encode())
984 _run(root, "commit", "--allow-empty", "-m", f"iter {i}")
985
986 snap = _head_manifest(root, "main")
987 assert (root / "evolving.py").read_bytes() == b"# iteration 19\n"
988 mismatches = verify_workdir_integrity(root, snap)
989 assert mismatches == []
990
991 def test_VIII5_stress_apply_manifest_100_times_deterministic(
992 self, tmp_path: pathlib.Path
993 ) -> None:
994 """VIII5: apply_manifest called 100 times is deterministic and
995 always produces an identical workdir."""
996 root, _ = _init_repo(tmp_path)
997 manifest: Manifest = {}
998 for i in range(50):
999 oid = _write_disk(root, f"f{i}.py", f"content {i}\n".encode())
1000 manifest[f"f{i}.py"] = oid
1001
1002 for trial in range(100):
1003 apply_manifest(root, manifest, manifest)
1004 mismatches = verify_workdir_integrity(root, manifest)
1005 assert mismatches == [], (
1006 f"Trial {trial}: {len(mismatches)} mismatch(es) after apply_manifest"
1007 )
1008
1009 def test_VIII6_diamond_topology_workdir_clean_after_all_merges(
1010 self, tmp_path: pathlib.Path
1011 ) -> None:
1012 """VIII6: diamond merge topology — verify integrity at every step."""
1013 root, repo_id = _init_repo(tmp_path)
1014 _run(root, "init", str(root))
1015
1016 # Base
1017 _write_disk(root, "base.py", b"base\n")
1018 _run(root, "commit", "--allow-empty", "-m", "base")
1019
1020 # Left branch
1021 _run(root, "branch", "left")
1022 _run(root, "checkout", "left")
1023 _write_disk(root, "left.py", b"left\n")
1024 _run(root, "commit", "-m", "left")
1025
1026 # Right branch (from main)
1027 _run(root, "checkout", "main")
1028 _run(root, "branch", "right")
1029 _run(root, "checkout", "right")
1030 _write_disk(root, "right.py", b"right\n")
1031 _run(root, "commit", "-m", "right")
1032
1033 # Merge left → main
1034 _run(root, "checkout", "main")
1035 code, out = _run(root, "merge", "left")
1036 assert code == 0, out
1037 snp = _head_manifest(root, "main")
1038 assert verify_workdir_integrity(root, snp) == []
1039
1040 # Merge right → main
1041 code, out = _run(root, "merge", "right")
1042 assert code == 0, out
1043 snp = _head_manifest(root, "main")
1044 assert verify_workdir_integrity(root, snp) == []
1045
1046 def test_VIII7_binary_files_survive_checkout(
1047 self, tmp_path: pathlib.Path
1048 ) -> None:
1049 """VIII7: binary content (random bytes) survives checkout intact."""
1050 root, repo_id = _init_repo(tmp_path)
1051 _run(root, "init", str(root))
1052
1053 binary = os.urandom(1024 * 512) # 512 KiB of random bytes
1054 oid = _write_disk(root, "data.bin", binary)
1055 _run(root, "commit", "--allow-empty", "-m", "binary")
1056
1057 _run(root, "branch", "feat")
1058 _run(root, "checkout", "feat")
1059 _write_disk(root, "other.py", b"other\n")
1060 _run(root, "commit", "-m", "other")
1061
1062 _run(root, "checkout", "main")
1063 snp = _head_manifest(root, "main")
1064 mismatches = verify_workdir_integrity(root, snp)
1065 assert mismatches == []
1066 assert (root / "data.bin").read_bytes() == binary
1067
1068 def test_VIII8_unicode_filenames_survive_checkout(
1069 self, tmp_path: pathlib.Path
1070 ) -> None:
1071 """VIII8: files with unicode path components survive checkout."""
1072 root, repo_id = _init_repo(tmp_path)
1073 _run(root, "init", str(root))
1074
1075 paths = [
1076 "src/módulo.py",
1077 "src/données.txt",
1078 "src/файл.py",
1079 ]
1080 for p in paths:
1081 _write_disk(root, p, f"# {p}\n".encode())
1082 _run(root, "commit", "--allow-empty", "-m", "unicode paths")
1083
1084 _run(root, "branch", "feat")
1085 _run(root, "checkout", "feat")
1086 _write_disk(root, "extra.py", b"extra\n")
1087 _run(root, "commit", "-m", "extra")
1088
1089 _run(root, "checkout", "main")
1090 snp = _head_manifest(root, "main")
1091 mismatches = verify_workdir_integrity(root, snp)
1092 assert mismatches == []
1093
1094 def test_VIII9_no_data_loss_after_100_consecutive_commits(
1095 self, tmp_path: pathlib.Path
1096 ) -> None:
1097 """VIII9: 100 consecutive commits on main — verify final state is correct."""
1098 root, repo_id = _init_repo(tmp_path)
1099 _run(root, "init", str(root))
1100
1101 for i in range(100):
1102 _write_disk(root, f"file_{i:03d}.py", f"# commit {i}\n".encode())
1103 code, out = _run(root, "commit", "--allow-empty", "-m", f"commit {i}")
1104 assert code == 0, f"Commit {i} failed: {out}"
1105
1106 snp = _head_manifest(root, "main")
1107 assert len(snp) == 100, f"Expected 100 files, got {len(snp)}"
1108 mismatches = verify_workdir_integrity(root, snp)
1109 assert mismatches == [], f"{len(mismatches)} mismatch(es) after 100 commits"
1110
1111
1112 # ===========================================================================
1113 # IX apply_manifest must not delete untracked files
1114 # ===========================================================================
1115
1116
1117 class TestApplyManifestUntrackedFilesIX:
1118 """Untracked files must survive apply_manifest regardless of target manifest.
1119
1120 Root cause of the bug: apply_manifest used walk_workdir(root) to build
1121 current_files, which returns ALL files on disk — including files the user
1122 created that were never committed. The fix: use prev_manifest.keys() as
1123 the deletion candidate set so only previously-tracked files are candidates.
1124 """
1125
1126 def test_IX1_untracked_file_not_deleted_by_apply_manifest(
1127 self, tmp_path: pathlib.Path
1128 ) -> None:
1129 """IX1: a file never in any manifest must survive apply_manifest."""
1130 root, _ = _init_repo(tmp_path)
1131
1132 # prev state: file A is tracked
1133 oid_a = _write_disk(root, "a.py", b"a = 1\n")
1134 # target state: file B is tracked
1135 oid_b = _store_object(root, b"b = 2\n")
1136 (root / "b.py").write_bytes(b"b = 2\n")
1137
1138 # untracked file — never in any manifest
1139 untracked = root / "notes.txt"
1140 untracked.write_bytes(b"my personal notes\n")
1141
1142 apply_manifest(root, {"a.py": oid_a}, {"b.py": oid_b})
1143
1144 assert untracked.exists(), "untracked file was deleted by apply_manifest — data loss bug"
1145 assert (root / "b.py").read_bytes() == b"b = 2\n"
1146 assert not (root / "a.py").exists(), "a.py was tracked then removed — must be deleted"
1147
1148 def test_IX2_untracked_dotfile_not_deleted(
1149 self, tmp_path: pathlib.Path
1150 ) -> None:
1151 """IX2: untracked dotfiles (e.g. spec docs written by Write tool) must survive."""
1152 root, _ = _init_repo(tmp_path)
1153
1154 oid_a = _write_disk(root, "main.py", b"x = 1\n")
1155 oid_b = _store_object(root, b"x = 2\n")
1156 (root / "main.py").write_bytes(b"x = 2\n")
1157
1158 spec_doc = root / "docs" / "spec.md"
1159 spec_doc.parent.mkdir()
1160 spec_doc.write_bytes(b"# spec\n")
1161
1162 apply_manifest(root, {"main.py": oid_a}, {"main.py": oid_b})
1163
1164 assert spec_doc.exists(), "untracked doc file was deleted by apply_manifest"
1165
1166 def test_IX3_tracked_file_removed_from_target_gets_deleted(
1167 self, tmp_path: pathlib.Path
1168 ) -> None:
1169 """IX3: files in prev_manifest but not in target must still be deleted."""
1170 root, _ = _init_repo(tmp_path)
1171
1172 oid_a = _write_disk(root, "gone.py", b"gone\n")
1173 oid_b = _write_disk(root, "kept.py", b"kept\n")
1174
1175 apply_manifest(root, {"gone.py": oid_a, "kept.py": oid_b}, {"kept.py": oid_b})
1176
1177 assert not (root / "gone.py").exists(), "tracked file removed from target must be deleted"
1178 assert (root / "kept.py").exists()
1179
1180 def test_IX4_commit_does_not_delete_untracked_file(
1181 self, tmp_path: pathlib.Path
1182 ) -> None:
1183 """IX4: muse commit must not delete untracked files from the working tree."""
1184 root, repo_id = _init_repo(tmp_path)
1185 _run(root, "init", str(root))
1186
1187 # First commit with one tracked file
1188 _write_disk(root, "tracked.py", b"x = 1\n")
1189 code, out = _run(root, "commit", "-m", "initial")
1190 assert code == 0, out
1191
1192 # Write an untracked file (simulates Write tool creating a spec doc)
1193 untracked = root / "docs" / "spec.md"
1194 untracked.parent.mkdir()
1195 untracked.write_bytes(b"# my spec\n")
1196 assert untracked.exists()
1197
1198 # Modify tracked file and commit
1199 (root / "tracked.py").write_bytes(b"x = 2\n")
1200 code, out = _run(root, "commit", "-m", "update")
1201 assert code == 0, out
1202
1203 assert untracked.exists(), (
1204 "muse commit deleted an untracked file — data loss bug. "
1205 "apply_manifest must not delete files absent from prev_manifest."
1206 )
File History 3 commits
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
sha256:7f9e2ef5286aedad9c1e6011b4c46ca27f39dbdad6e3409357e36b26e46b3b7c docs: docstring sprint for-each-ref→hotspots — idiomatic ru… Sonnet 4.6 patch 137 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago