gabriel / muse public
test_core_snapshot.py python
474 lines 19.2 KB
Raw
sha256:b636f72dcba9e190afb980bece906fa5b717fbde014b76ef023df8cb96e01eb9 docs: expand cache plan with all seven testing tiers and do… Sonnet 4.6 131 days ago
1 """Tests for muse.core.snapshot — content-addressed snapshot computation.
2
3 Test categories
4 ---------------
5 - TestHashFile — unit: SHA-256 hash_file
6 - TestBuildSnapshotManifest — unit: full manifest walks
7 - TestNestedRepoWalk — unit/integration: nested .muse repos are excluded
8 - TestComputeSnapshotId — unit: snapshot id derivation
9 - TestComputeCommitId — unit: commit id derivation
10 - TestDiffWorkdirVsSnapshot — unit: diff logic
11 """
12
13 import os
14 import pathlib
15 import threading
16 import time
17
18 import pytest
19
20 from muse.core._types import fake_id
21 from muse.core.snapshot import (
22 build_snapshot_manifest,
23 compute_commit_id,
24 compute_snapshot_id,
25 diff_workdir_vs_snapshot,
26 hash_file,
27 walk_workdir,
28 walk_workdir_with_dirs,
29 )
30
31
32 @pytest.fixture
33 def workdir(tmp_path: pathlib.Path) -> pathlib.Path:
34 return tmp_path
35
36
37 class TestHashFile:
38 def test_consistent(self, tmp_path: pathlib.Path) -> None:
39 f = tmp_path / "file.mid"
40 f.write_bytes(b"hello world")
41 assert hash_file(f) == hash_file(f)
42
43 def test_different_content_different_hash(self, tmp_path: pathlib.Path) -> None:
44 a = tmp_path / "a.mid"
45 b = tmp_path / "b.mid"
46 a.write_bytes(b"aaa")
47 b.write_bytes(b"bbb")
48 assert hash_file(a) != hash_file(b)
49
50 def test_known_hash(self, tmp_path: pathlib.Path) -> None:
51 from muse.core._types import blob_id
52 content = b"muse"
53 f = tmp_path / "f.mid"
54 f.write_bytes(content)
55 expected = blob_id(content)
56 assert hash_file(f) == expected
57
58
59 class TestBuildSnapshotManifest:
60 def test_empty_workdir(self, workdir: pathlib.Path) -> None:
61 assert build_snapshot_manifest(workdir) == {}
62
63 def test_single_file(self, workdir: pathlib.Path) -> None:
64 (workdir / "beat.mid").write_bytes(b"drums")
65 manifest = build_snapshot_manifest(workdir)
66 assert "beat.mid" in manifest
67 assert len(manifest["beat.mid"]) == 71 # sha256:<64 hex>
68
69 def test_nested_file(self, workdir: pathlib.Path) -> None:
70 (workdir / "tracks").mkdir()
71 (workdir / "tracks" / "bass.mid").write_bytes(b"bass")
72 manifest = build_snapshot_manifest(workdir)
73 assert "tracks/bass.mid" in manifest
74
75 def test_secrets_excluded_by_builtin_blocklist(self, workdir: pathlib.Path) -> None:
76 """Built-in secrets blocklist protects even without a .museignore file."""
77 (workdir / ".env").write_bytes(b"SECRET=abc")
78 (workdir / ".DS_Store").write_bytes(b"junk")
79 (workdir / "beat.mid").write_bytes(b"drums")
80 manifest = build_snapshot_manifest(workdir)
81 assert ".env" not in manifest
82 assert ".DS_Store" not in manifest
83 assert "beat.mid" in manifest
84
85 def test_dotfiles_tracked_when_not_ignored(self, workdir: pathlib.Path) -> None:
86 """Non-secret dotfiles like .cursorrules are tracked by default."""
87 (workdir / ".cursorrules").write_bytes(b"# rules")
88 (workdir / ".editorconfig").write_bytes(b"[*]\nindent_size=4")
89 manifest = build_snapshot_manifest(workdir)
90 assert ".cursorrules" in manifest
91 assert ".editorconfig" in manifest
92
93 def test_museignore_excludes_custom_pattern(self, workdir: pathlib.Path) -> None:
94 """A pattern in .museignore excludes the matched file."""
95 (workdir / ".museignore").write_bytes(b'[global]\npatterns = ["*.secret"]\n')
96 (workdir / "api.secret").write_bytes(b"token")
97 (workdir / "beat.mid").write_bytes(b"drums")
98 manifest = build_snapshot_manifest(workdir)
99 assert "api.secret" not in manifest
100 assert "beat.mid" in manifest
101
102 def test_deterministic_order(self, workdir: pathlib.Path) -> None:
103 for name in ["c.mid", "a.mid", "b.mid"]:
104 (workdir / name).write_bytes(name.encode())
105 m1 = build_snapshot_manifest(workdir)
106 m2 = build_snapshot_manifest(workdir)
107 assert m1 == m2
108
109
110 class TestComputeSnapshotId:
111 def test_empty_manifest(self) -> None:
112 sid = compute_snapshot_id({})
113 assert len(sid) == 71
114
115 def test_deterministic(self) -> None:
116 manifest = {"a.mid": fake_id("hash1"), "b.mid": fake_id("hash2")}
117 assert compute_snapshot_id(manifest) == compute_snapshot_id(manifest)
118
119 def test_order_independent(self) -> None:
120 m1 = {"a.mid": fake_id("h1"), "b.mid": fake_id("h2")}
121 m2 = {"b.mid": fake_id("h2"), "a.mid": fake_id("h1")}
122 assert compute_snapshot_id(m1) == compute_snapshot_id(m2)
123
124 def test_different_content_different_id(self) -> None:
125 m1 = {"a.mid": fake_id("h1")}
126 m2 = {"a.mid": fake_id("h2")}
127 assert compute_snapshot_id(m1) != compute_snapshot_id(m2)
128
129
130 class TestComputeCommitId:
131 _BASE = dict(
132 repo_id="repo-abc",
133 parent_ids=[fake_id("p1")],
134 snapshot_id=fake_id("snap"),
135 message="msg",
136 committed_at_iso="2026-01-01T00:00:00+00:00",
137 author="gabriel",
138 signer_public_key="ed25519:AAAA",
139 )
140
141 def test_deterministic(self) -> None:
142 assert compute_commit_id(**self._BASE) == compute_commit_id(**self._BASE)
143
144 def test_parent_order_independent(self) -> None:
145 a = compute_commit_id(**{**self._BASE, "parent_ids": [fake_id("p1"), fake_id("p2")]})
146 b = compute_commit_id(**{**self._BASE, "parent_ids": [fake_id("p2"), fake_id("p1")]})
147 assert a == b
148
149 def test_different_messages_different_ids(self) -> None:
150 a = compute_commit_id(**{**self._BASE, "message": "msg1"})
151 b = compute_commit_id(**{**self._BASE, "message": "msg2"})
152 assert a != b
153
154 def test_different_repo_ids_different_commit_ids(self) -> None:
155 a = compute_commit_id(**{**self._BASE, "repo_id": "repo-A"})
156 b = compute_commit_id(**{**self._BASE, "repo_id": "repo-B"})
157 assert a != b
158
159 def test_different_authors_different_commit_ids(self) -> None:
160 a = compute_commit_id(**{**self._BASE, "author": "alice"})
161 b = compute_commit_id(**{**self._BASE, "author": "bob"})
162 assert a != b
163
164 def test_different_signer_keys_different_commit_ids(self) -> None:
165 a = compute_commit_id(**{**self._BASE, "signer_public_key": "ed25519:AAAA"})
166 b = compute_commit_id(**{**self._BASE, "signer_public_key": "ed25519:BBBB"})
167 assert a != b
168
169 def test_empty_author_and_key_still_deterministic(self) -> None:
170 kwargs = {**self._BASE, "author": "", "signer_public_key": ""}
171 assert compute_commit_id(**kwargs) == compute_commit_id(**kwargs)
172
173 def test_result_has_sha256_prefix(self) -> None:
174 result = compute_commit_id(**self._BASE)
175 assert result.startswith("sha256:")
176
177 def test_repo_id_bound_prevents_cross_repo_replay(self) -> None:
178 src = compute_commit_id(**{**self._BASE, "repo_id": "repo-src"})
179 dst = compute_commit_id(**{**self._BASE, "repo_id": "repo-dst"})
180 assert src != dst
181
182
183 class TestDiffWorkdirVsSnapshot:
184 def test_new_repo_all_untracked(self, workdir: pathlib.Path) -> None:
185 (workdir / "beat.mid").write_bytes(b"x")
186 added, modified, deleted, untracked, added_dirs, deleted_dirs = diff_workdir_vs_snapshot(workdir, {})
187 assert added == set()
188 assert untracked == {"beat.mid"}
189
190 def test_added_file(self, workdir: pathlib.Path) -> None:
191 (workdir / "beat.mid").write_bytes(b"x")
192 last = {"other.mid": "abc"}
193 added, modified, deleted, untracked, added_dirs, deleted_dirs = diff_workdir_vs_snapshot(workdir, last)
194 assert "beat.mid" in added
195 assert "other.mid" in deleted
196
197 def test_modified_file(self, workdir: pathlib.Path) -> None:
198 f = workdir / "beat.mid"
199 f.write_bytes(b"new content")
200 last = {"beat.mid": "oldhash"}
201 added, modified, deleted, untracked, added_dirs, deleted_dirs = diff_workdir_vs_snapshot(workdir, last)
202 assert "beat.mid" in modified
203
204 def test_clean_workdir(self, workdir: pathlib.Path) -> None:
205 f = workdir / "beat.mid"
206 f.write_bytes(b"content")
207 from muse.core.snapshot import hash_file
208 h = hash_file(f)
209 added, modified, deleted, untracked, added_dirs, deleted_dirs = diff_workdir_vs_snapshot(workdir, {"beat.mid": h})
210 assert not added and not modified and not deleted and not untracked
211
212 def test_ignored_extant_file_not_reported_as_deleted(
213 self, workdir: pathlib.Path
214 ) -> None:
215 """A file that was tracked, is now in .museignore, and still exists on
216 disk must NOT appear in ``deleted``. It was intentionally moved out of
217 tracking — reporting it as deleted would block checkout and cause shelf
218 pop to unlink it."""
219 (workdir / ".museignore").write_bytes(
220 b'[global]\npatterns = ["app.js"]\n'
221 )
222 (workdir / "app.js").write_bytes(b"// build artifact")
223 (workdir / "src.py").write_bytes(b"# source")
224 from muse.core.snapshot import hash_file
225 # Pretend HEAD tracked both files.
226 last = {
227 "app.js": hash_file(workdir / "app.js"),
228 "src.py": hash_file(workdir / "src.py"),
229 }
230 added, modified, deleted, _, _, _ = diff_workdir_vs_snapshot(workdir, last)
231 assert "app.js" not in deleted, (
232 "ignored-and-extant file must not appear in deleted"
233 )
234 assert "src.py" not in deleted
235
236 def test_ignored_absent_file_is_reported_as_deleted(
237 self, workdir: pathlib.Path
238 ) -> None:
239 """A file that is in .museignore but is genuinely absent from disk IS
240 deleted and must appear in ``deleted``."""
241 (workdir / ".museignore").write_bytes(
242 b'[global]\npatterns = ["app.js"]\n'
243 )
244 # app.js is in .museignore but does NOT exist on disk.
245 (workdir / "src.py").write_bytes(b"# source")
246 from muse.core.snapshot import hash_file
247 last = {
248 "app.js": "a" * 64, # was in HEAD but is gone from disk
249 "src.py": hash_file(workdir / "src.py"),
250 }
251 added, modified, deleted, _, _, _ = diff_workdir_vs_snapshot(workdir, last)
252 assert "app.js" in deleted, (
253 "ignored file that is genuinely absent from disk must still be deleted"
254 )
255
256
257 # ---------------------------------------------------------------------------
258 # Nested repo boundary — unit / integration
259 # ---------------------------------------------------------------------------
260
261 def _make_nested_repo(parent: pathlib.Path, name: str) -> pathlib.Path:
262 """Create a child directory that looks like a muse repo (.muse/ present)."""
263 child = parent / name
264 child.mkdir(parents=True, exist_ok=True)
265 (child / ".muse").mkdir()
266 (child / ".muse" / "repo.json").write_text('{"repo_id": "child"}')
267 return child
268
269
270 class TestNestedRepoWalk:
271 """Nested muse repos must be excluded from the parent's walk.
272
273 The parent repo's ``os.walk`` must prune any subdirectory that contains
274 its own ``.muse/`` directory. This mirrors git submodule behaviour —
275 child repo files belong to the child snapshot, not the parent.
276 """
277
278 # --- walk_workdir -------------------------------------------------------
279
280 def test_nested_repo_files_excluded(self, tmp_path: pathlib.Path) -> None:
281 """Files inside a nested repo do not appear in the parent manifest."""
282 (tmp_path / "parent.py").write_bytes(b"# parent")
283 child = _make_nested_repo(tmp_path, "child_repo")
284 (child / "child.py").write_bytes(b"# child")
285
286 manifest = walk_workdir(tmp_path)
287 assert "parent.py" in manifest
288 assert "child_repo/child.py" not in manifest
289
290 def test_nested_repo_root_dir_excluded(self, tmp_path: pathlib.Path) -> None:
291 """The child root directory itself is not descended into."""
292 _make_nested_repo(tmp_path, "child_repo")
293 manifest = walk_workdir(tmp_path)
294 # No key should start with child_repo/
295 assert not any(k.startswith("child_repo/") for k in manifest)
296
297 def test_sibling_dirs_still_walked(self, tmp_path: pathlib.Path) -> None:
298 """Normal subdirs next to a nested repo are still walked."""
299 _make_nested_repo(tmp_path, "child_repo")
300 sibling = tmp_path / "src"
301 sibling.mkdir()
302 (sibling / "main.py").write_bytes(b"# main")
303
304 manifest = walk_workdir(tmp_path)
305 assert "src/main.py" in manifest
306
307 def test_deeply_nested_repo_excluded(self, tmp_path: pathlib.Path) -> None:
308 """Nested repos two levels deep are also excluded."""
309 mid = tmp_path / "packages"
310 mid.mkdir()
311 (mid / "shared.py").write_bytes(b"# shared")
312 child = _make_nested_repo(mid, "plugin")
313 (child / "plugin.py").write_bytes(b"# plugin")
314
315 manifest = walk_workdir(tmp_path)
316 assert "packages/shared.py" in manifest
317 assert "packages/plugin/plugin.py" not in manifest
318
319 def test_multiple_nested_repos_all_excluded(self, tmp_path: pathlib.Path) -> None:
320 """Multiple sibling nested repos are all pruned."""
321 _make_nested_repo(tmp_path, "repo_a")
322 _make_nested_repo(tmp_path, "repo_b")
323 _make_nested_repo(tmp_path, "repo_c")
324 (tmp_path / "root.py").write_bytes(b"# root")
325 for repo in ("repo_a", "repo_b", "repo_c"):
326 ((tmp_path / repo) / "file.py").write_bytes(b"# file")
327
328 manifest = walk_workdir(tmp_path)
329 assert "root.py" in manifest
330 for repo in ("repo_a", "repo_b", "repo_c"):
331 assert f"{repo}/file.py" not in manifest
332
333 # --- walk_workdir_with_dirs ---------------------------------------------
334
335 def test_dirs_output_excludes_nested_repo(self, tmp_path: pathlib.Path) -> None:
336 """walk_workdir_with_dirs must not list the nested repo as a directory."""
337 _make_nested_repo(tmp_path, "child_repo")
338 src = tmp_path / "src"
339 src.mkdir()
340 (src / "a.py").write_bytes(b"a")
341
342 _, dirs = walk_workdir_with_dirs(tmp_path)
343 assert "src" in dirs
344 assert "child_repo" not in dirs
345
346 # --- build_snapshot_manifest (public API) --------------------------------
347
348 def test_build_snapshot_manifest_excludes_nested(self, tmp_path: pathlib.Path) -> None:
349 """build_snapshot_manifest is the public wrapper — same boundary."""
350 (tmp_path / "root.py").write_bytes(b"# root")
351 child = _make_nested_repo(tmp_path, "nested")
352 (child / "nested.py").write_bytes(b"# nested")
353
354 manifest = build_snapshot_manifest(tmp_path)
355 assert "root.py" in manifest
356 assert "nested/nested.py" not in manifest
357
358 # --- diff_workdir_vs_snapshot integration --------------------------------
359
360 def test_diff_does_not_report_nested_files_as_added(
361 self, tmp_path: pathlib.Path
362 ) -> None:
363 """diff sees an empty last-snapshot: nested files must not appear as untracked."""
364 (tmp_path / "root.py").write_bytes(b"# root")
365 child = _make_nested_repo(tmp_path, "sub")
366 (child / "sub.py").write_bytes(b"# sub")
367
368 added, modified, deleted, untracked, _, _ = diff_workdir_vs_snapshot(
369 tmp_path, {}
370 )
371 assert "root.py" in untracked
372 assert not any(k.startswith("sub/") for k in untracked)
373 assert not any(k.startswith("sub/") for k in added)
374
375 # --- data integrity -----------------------------------------------------
376
377 def test_manifest_keys_posix_separators(self, tmp_path: pathlib.Path) -> None:
378 """Manifest keys always use '/' regardless of OS."""
379 sub = tmp_path / "a" / "b"
380 sub.mkdir(parents=True)
381 (sub / "file.py").write_bytes(b"x")
382 manifest = walk_workdir(tmp_path)
383 assert "a/b/file.py" in manifest
384 assert all("/" in k or "/" not in k for k in manifest) # no backslash keys
385 assert not any("\\" in k for k in manifest)
386
387 def test_nested_muse_dir_itself_not_tracked(self, tmp_path: pathlib.Path) -> None:
388 """The .muse/ directory of a nested repo is not tracked as a file."""
389 child = _make_nested_repo(tmp_path, "child")
390 (child / "real.py").write_bytes(b"x")
391 manifest = walk_workdir(tmp_path)
392 assert not any(".muse" in k for k in manifest)
393
394 # --- security -----------------------------------------------------------
395
396 def test_symlink_to_nested_repo_not_followed(self, tmp_path: pathlib.Path) -> None:
397 """A symlink pointing at a directory that has .muse/ is not followed.
398 walk_workdir uses followlinks=False so symlinks are excluded by design."""
399 real = _make_nested_repo(tmp_path, "real_repo")
400 (real / "secret.py").write_bytes(b"# secret")
401 link = tmp_path / "link_to_repo"
402 link.symlink_to(real)
403
404 manifest = walk_workdir(tmp_path)
405 assert "link_to_repo/secret.py" not in manifest
406
407 def test_symlink_to_regular_dir_not_followed(self, tmp_path: pathlib.Path) -> None:
408 """Symlinks to any directory are never followed — followlinks=False."""
409 real = tmp_path / "outside"
410 real.mkdir()
411 (real / "file.py").write_bytes(b"x")
412 link = tmp_path / "link_to_dir"
413 link.symlink_to(real)
414
415 manifest = walk_workdir(tmp_path)
416 assert "link_to_dir/file.py" not in manifest
417
418 def test_nested_repo_with_unusual_name(self, tmp_path: pathlib.Path) -> None:
419 """Nested repos with names containing spaces or dots are excluded."""
420 for name in ("my.repo", "repo name", ".hidden_repo"):
421 child = tmp_path / name
422 child.mkdir()
423 (child / ".muse").mkdir()
424 (child / "file.py").write_bytes(b"x")
425
426 (tmp_path / "root.py").write_bytes(b"r")
427 manifest = walk_workdir(tmp_path)
428 assert "root.py" in manifest
429 assert not any("file.py" in k for k in manifest)
430
431 # --- performance --------------------------------------------------------
432
433 def test_large_parent_with_nested_repo_fast(self, tmp_path: pathlib.Path) -> None:
434 """Walking 500-file parent with a nested repo completes in < 2 s."""
435 for i in range(500):
436 (tmp_path / f"file_{i:04d}.py").write_bytes(b"x" * 100)
437 child = _make_nested_repo(tmp_path, "child")
438 for i in range(200):
439 (child / f"child_{i:04d}.py").write_bytes(b"x" * 100)
440
441 start = time.monotonic()
442 manifest = walk_workdir(tmp_path)
443 elapsed = time.monotonic() - start
444
445 assert elapsed < 2.0, f"walk took {elapsed:.2f}s — too slow"
446 # Parent files included, child files excluded.
447 assert len(manifest) == 500
448 assert not any(k.startswith("child/") for k in manifest)
449
450 def test_concurrent_walks_consistent(self, tmp_path: pathlib.Path) -> None:
451 """Concurrent walks of the same tree return identical manifests."""
452 (tmp_path / "a.py").write_bytes(b"a")
453 (tmp_path / "b.py").write_bytes(b"b")
454 _make_nested_repo(tmp_path, "child")
455 (tmp_path / "child" / "c.py").write_bytes(b"c")
456
457 results: list[dict] = []
458 errors: list[Exception] = []
459
460 def _walk() -> None:
461 try:
462 results.append(walk_workdir(tmp_path))
463 except Exception as exc:
464 errors.append(exc)
465
466 threads = [threading.Thread(target=_walk) for _ in range(8)]
467 for t in threads:
468 t.start()
469 for t in threads:
470 t.join()
471
472 assert not errors
473 assert len(results) == 8
474 assert all(r == results[0] for r in results), "concurrent walks diverged"
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 138 days ago
sha256:a09b1b4f6838754495547f200aa0ce88e2f56ffc5b20b900f6f0cff2c3cdede9 fix(cursorignore): remove git-ism (.git/worktrees) Human minor 140 days ago