gabriel / muse public
test_core_snapshot.py python
157 lines 6.2 KB
e74bbfd6 chore: ignore .hypothesis, .pytest_cache, .mypy_cache, .ruff_cache; add… gabriel 8h ago
1 """Tests for muse.core.snapshot — content-addressed snapshot computation."""
2
3 import pathlib
4
5 import pytest
6
7 from muse.core.snapshot import (
8 build_snapshot_manifest,
9 compute_commit_id,
10 compute_snapshot_id,
11 diff_workdir_vs_snapshot,
12 hash_file,
13 )
14
15
16 @pytest.fixture
17 def workdir(tmp_path: pathlib.Path) -> pathlib.Path:
18 return tmp_path
19
20
21 class TestHashFile:
22 def test_consistent(self, tmp_path: pathlib.Path) -> None:
23 f = tmp_path / "file.mid"
24 f.write_bytes(b"hello world")
25 assert hash_file(f) == hash_file(f)
26
27 def test_different_content_different_hash(self, tmp_path: pathlib.Path) -> None:
28 a = tmp_path / "a.mid"
29 b = tmp_path / "b.mid"
30 a.write_bytes(b"aaa")
31 b.write_bytes(b"bbb")
32 assert hash_file(a) != hash_file(b)
33
34 def test_known_hash(self, tmp_path: pathlib.Path) -> None:
35 import hashlib
36 content = b"muse"
37 f = tmp_path / "f.mid"
38 f.write_bytes(content)
39 expected = hashlib.sha256(content).hexdigest()
40 assert hash_file(f) == expected
41
42
43 class TestBuildSnapshotManifest:
44 def test_empty_workdir(self, workdir: pathlib.Path) -> None:
45 assert build_snapshot_manifest(workdir) == {}
46
47 def test_single_file(self, workdir: pathlib.Path) -> None:
48 (workdir / "beat.mid").write_bytes(b"drums")
49 manifest = build_snapshot_manifest(workdir)
50 assert "beat.mid" in manifest
51 assert len(manifest["beat.mid"]) == 64 # sha256 hex
52
53 def test_nested_file(self, workdir: pathlib.Path) -> None:
54 (workdir / "tracks").mkdir()
55 (workdir / "tracks" / "bass.mid").write_bytes(b"bass")
56 manifest = build_snapshot_manifest(workdir)
57 assert "tracks/bass.mid" in manifest
58
59 def test_secrets_excluded_by_builtin_blocklist(self, workdir: pathlib.Path) -> None:
60 """Built-in secrets blocklist protects even without a .museignore file."""
61 (workdir / ".env").write_bytes(b"SECRET=abc")
62 (workdir / ".DS_Store").write_bytes(b"junk")
63 (workdir / "beat.mid").write_bytes(b"drums")
64 manifest = build_snapshot_manifest(workdir)
65 assert ".env" not in manifest
66 assert ".DS_Store" not in manifest
67 assert "beat.mid" in manifest
68
69 def test_dotfiles_tracked_when_not_ignored(self, workdir: pathlib.Path) -> None:
70 """Non-secret dotfiles like .cursorrules are tracked by default."""
71 (workdir / ".cursorrules").write_bytes(b"# rules")
72 (workdir / ".editorconfig").write_bytes(b"[*]\nindent_size=4")
73 manifest = build_snapshot_manifest(workdir)
74 assert ".cursorrules" in manifest
75 assert ".editorconfig" in manifest
76
77 def test_museignore_excludes_custom_pattern(self, workdir: pathlib.Path) -> None:
78 """A pattern in .museignore excludes the matched file."""
79 (workdir / ".museignore").write_bytes(b'[global]\npatterns = ["*.secret"]\n')
80 (workdir / "api.secret").write_bytes(b"token")
81 (workdir / "beat.mid").write_bytes(b"drums")
82 manifest = build_snapshot_manifest(workdir)
83 assert "api.secret" not in manifest
84 assert "beat.mid" in manifest
85
86 def test_deterministic_order(self, workdir: pathlib.Path) -> None:
87 for name in ["c.mid", "a.mid", "b.mid"]:
88 (workdir / name).write_bytes(name.encode())
89 m1 = build_snapshot_manifest(workdir)
90 m2 = build_snapshot_manifest(workdir)
91 assert m1 == m2
92
93
94 class TestComputeSnapshotId:
95 def test_empty_manifest(self) -> None:
96 sid = compute_snapshot_id({})
97 assert len(sid) == 64
98
99 def test_deterministic(self) -> None:
100 manifest = {"a.mid": "hash1", "b.mid": "hash2"}
101 assert compute_snapshot_id(manifest) == compute_snapshot_id(manifest)
102
103 def test_order_independent(self) -> None:
104 m1 = {"a.mid": "h1", "b.mid": "h2"}
105 m2 = {"b.mid": "h2", "a.mid": "h1"}
106 assert compute_snapshot_id(m1) == compute_snapshot_id(m2)
107
108 def test_different_content_different_id(self) -> None:
109 m1 = {"a.mid": "h1"}
110 m2 = {"a.mid": "h2"}
111 assert compute_snapshot_id(m1) != compute_snapshot_id(m2)
112
113
114 class TestComputeCommitId:
115 def test_deterministic(self) -> None:
116 kwargs = dict(parent_ids=["p1"], snapshot_id="1" * 64, message="msg", committed_at_iso="2026-01-01T00:00:00+00:00")
117 assert compute_commit_id(**kwargs) == compute_commit_id(**kwargs)
118
119 def test_parent_order_independent(self) -> None:
120 a = compute_commit_id(parent_ids=["p1", "p2"], snapshot_id="1" * 64, message="m", committed_at_iso="t")
121 b = compute_commit_id(parent_ids=["p2", "p1"], snapshot_id="1" * 64, message="m", committed_at_iso="t")
122 assert a == b
123
124 def test_different_messages_different_ids(self) -> None:
125 a = compute_commit_id(parent_ids=[], snapshot_id="1" * 64, message="msg1", committed_at_iso="t")
126 b = compute_commit_id(parent_ids=[], snapshot_id="1" * 64, message="msg2", committed_at_iso="t")
127 assert a != b
128
129
130 class TestDiffWorkdirVsSnapshot:
131 def test_new_repo_all_untracked(self, workdir: pathlib.Path) -> None:
132 (workdir / "beat.mid").write_bytes(b"x")
133 added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, {})
134 assert added == set()
135 assert untracked == {"beat.mid"}
136
137 def test_added_file(self, workdir: pathlib.Path) -> None:
138 (workdir / "beat.mid").write_bytes(b"x")
139 last = {"other.mid": "abc"}
140 added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, last)
141 assert "beat.mid" in added
142 assert "other.mid" in deleted
143
144 def test_modified_file(self, workdir: pathlib.Path) -> None:
145 f = workdir / "beat.mid"
146 f.write_bytes(b"new content")
147 last = {"beat.mid": "oldhash"}
148 added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, last)
149 assert "beat.mid" in modified
150
151 def test_clean_workdir(self, workdir: pathlib.Path) -> None:
152 f = workdir / "beat.mid"
153 f.write_bytes(b"content")
154 from muse.core.snapshot import hash_file
155 h = hash_file(f)
156 added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, {"beat.mid": h})
157 assert not added and not modified and not deleted and not untracked