gabriel / muse public
test_core_store.py python
317 lines 12.9 KB
Raw
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 12 days ago
1 """Tests for muse.core.store — file-based commit and snapshot storage."""
2
3 import datetime
4 import json
5 import pathlib
6
7 import pytest
8
9 from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id
10
11 from muse.core.types import Manifest, fake_id, long_id
12 from muse.core.paths import muse_dir, heads_dir, remote_tracking_dir, remotes_dir
13 from muse.core.refs import get_head_commit_id
14 from muse.core.commits import (
15 CommitDict,
16 CommitRecord,
17 _resolve_branch_commit_id,
18 find_commits_by_prefix,
19 get_all_commits,
20 get_commits_for_branch,
21 get_head_snapshot_id,
22 read_commit,
23 update_commit_metadata,
24 write_commit,
25 )
26 from muse.core.snapshots import (
27 SnapshotRecord,
28 get_head_snapshot_manifest,
29 read_snapshot,
30 write_snapshot,
31 )
32 from muse.core.tags import (
33 TagRecord,
34 get_all_tags,
35 get_tags_for_commit,
36 write_tag,
37 )
38
39
40 @pytest.fixture
41 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
42 """Create a minimal .muse/ directory structure."""
43 dot_muse = muse_dir(tmp_path)
44 (dot_muse / "commits").mkdir(parents=True)
45 (dot_muse / "snapshots").mkdir(parents=True)
46 (dot_muse / "refs" / "heads").mkdir(parents=True)
47 (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
48 (dot_muse / "HEAD").write_text("ref: refs/heads/main\n")
49 (dot_muse / "refs" / "heads" / "main").write_text("")
50 return tmp_path
51
52
53 def _make_commit(
54 root: pathlib.Path,
55 commit_id: str,
56 snapshot_id: str,
57 message: str,
58 parent: str | None = None,
59 ) -> CommitRecord:
60 """Write a commit whose ID is computed from its content fields.
61
62 The ``commit_id`` parameter is kept for call-site readability but is
63 ignored — the real ID is derived by :func:`~muse.core.snapshot.compute_commit_id`
64 so every stored commit satisfies the I-10 content-hash verification.
65 """
66 now = datetime.datetime.now(datetime.timezone.utc)
67 parents: list[str] = [parent] if parent else []
68 real_id = compute_commit_id(
69 parent_ids=parents,
70 snapshot_id=snapshot_id,
71 message=message,
72 committed_at_iso=now.isoformat(),
73 )
74 c = CommitRecord(
75 commit_id=real_id,
76 branch="main",
77 snapshot_id=snapshot_id,
78 message=message,
79 committed_at=now,
80 parent_commit_id=parent,
81 )
82 write_commit(root, c)
83 return c
84
85
86 def _make_snapshot(root: pathlib.Path, snapshot_id: str, manifest: Manifest) -> SnapshotRecord:
87 """Write a snapshot whose ID is computed from its manifest.
88
89 The ``snapshot_id`` parameter is kept for call-site readability but is
90 ignored — the real ID is derived by
91 :func:`~muse.core.snapshot.compute_snapshot_id` so every stored snapshot
92 satisfies the I-10 content-hash verification.
93 """
94 real_id = compute_snapshot_id(manifest)
95 s = SnapshotRecord(snapshot_id=real_id, manifest=manifest)
96 write_snapshot(root, s)
97 return s
98
99
100 class TestWriteReadCommit:
101 def test_roundtrip(self, repo: pathlib.Path) -> None:
102 c = _make_commit(repo, "ignored", fake_id("snap"), "Initial commit")
103 loaded = read_commit(repo, c.commit_id)
104 assert loaded is not None
105 assert loaded.commit_id == c.commit_id
106 assert loaded.message == "Initial commit"
107
108 def test_read_missing_returns_none(self, repo: pathlib.Path) -> None:
109 assert read_commit(repo, fake_id("nonexistent-commit")) is None
110
111 def test_idempotent_write(self, repo: pathlib.Path) -> None:
112 c = _make_commit(repo, "ignored", fake_id("snap"), "First")
113 _make_commit(repo, "ignored", fake_id("snap"), "Second") # different timestamp → different ID; should write
114 loaded = read_commit(repo, c.commit_id)
115 assert loaded is not None
116 assert loaded.message == "First"
117
118 def test_metadata_preserved(self, repo: pathlib.Path) -> None:
119 now = datetime.datetime.now(datetime.timezone.utc)
120 snap_id = fake_id("snap")
121 cid = compute_commit_id(parent_ids=[], snapshot_id=snap_id, message="With metadata", committed_at_iso=now.isoformat())
122 c = CommitRecord(
123 commit_id=cid,
124 branch="main",
125 snapshot_id=snap_id,
126 message="With metadata",
127 committed_at=now,
128 metadata={"section": "chorus", "emotion": "joyful"},
129 )
130 write_commit(repo, c)
131 loaded = read_commit(repo, cid)
132 assert loaded is not None
133 assert loaded.metadata["section"] == "chorus"
134 assert loaded.metadata["emotion"] == "joyful"
135
136
137 class TestUpdateCommitMetadata:
138 def test_set_key(self, repo: pathlib.Path) -> None:
139 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
140 result = update_commit_metadata(repo, c.commit_id, "tempo_bpm", "120.0")
141 assert result is True
142 loaded = read_commit(repo, c.commit_id)
143 assert loaded is not None
144 assert loaded.metadata["tempo_bpm"] == "120.0"
145
146 def test_missing_commit_returns_false(self, repo: pathlib.Path) -> None:
147 assert update_commit_metadata(repo, fake_id("nonexistent-commit"), "k", "v") is False
148
149
150 class TestWriteReadSnapshot:
151 def test_roundtrip(self, repo: pathlib.Path) -> None:
152 _drum_id = fake_id("deadbeef")
153 s = _make_snapshot(repo, "ignored", {"tracks/drums.mid": _drum_id})
154 loaded = read_snapshot(repo, s.snapshot_id)
155 assert loaded is not None
156 assert loaded.manifest == {"tracks/drums.mid": _drum_id}
157
158 def test_read_missing_returns_none(self, repo: pathlib.Path) -> None:
159 assert read_snapshot(repo, fake_id("nonexistent-snapshot")) is None
160
161
162 class TestHeadQueries:
163 def test_get_head_commit_id_empty_branch(self, repo: pathlib.Path) -> None:
164 assert get_head_commit_id(repo, "main") is None
165
166 def test_get_head_commit_id(self, repo: pathlib.Path) -> None:
167 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
168 (heads_dir(repo) / "main").write_text(c.commit_id)
169 assert get_head_commit_id(repo, "main") == c.commit_id
170
171 def test_get_head_snapshot_id(self, repo: pathlib.Path) -> None:
172 _f_id = fake_id("f.mid-content")
173 snap = _make_snapshot(repo, "ignored", {"f.mid": _f_id})
174 c = _make_commit(repo, "ignored", snap.snapshot_id, "msg")
175 (heads_dir(repo) / "main").write_text(c.commit_id)
176 assert get_head_snapshot_id(repo, "main") == snap.snapshot_id
177
178 def test_get_head_snapshot_manifest(self, repo: pathlib.Path) -> None:
179 _f_id = fake_id("f.mid-content")
180 snap = _make_snapshot(repo, "ignored", {"f.mid": _f_id})
181 c = _make_commit(repo, "ignored", snap.snapshot_id, "msg")
182 (heads_dir(repo) / "main").write_text(c.commit_id)
183 manifest = get_head_snapshot_manifest(repo, "main")
184 assert manifest == {"f.mid": _f_id}
185
186
187 class TestResolveRemoteBranchCommitId:
188 """_resolve_branch_commit_id handles local branches and remote tracking refs."""
189
190 def test_local_branch_resolved(self, repo: pathlib.Path) -> None:
191 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
192 (heads_dir(repo) / "main").write_text(c.commit_id)
193 assert _resolve_branch_commit_id(repo, "main") == c.commit_id
194
195 def test_empty_local_branch_returns_none(self, repo: pathlib.Path) -> None:
196 assert _resolve_branch_commit_id(repo, "main") is None
197
198 def test_remote_tracking_ref_resolved(self, repo: pathlib.Path) -> None:
199 remote_dir = remote_tracking_dir(repo, "origin")
200 remote_dir.mkdir(parents=True)
201 cid = long_id("a" * 64)
202 (remote_dir / "dev").write_text(cid)
203 assert _resolve_branch_commit_id(repo, "origin/dev") == cid
204
205 def test_remote_tracking_ref_missing_returns_none(self, repo: pathlib.Path) -> None:
206 remote_tracking_dir(repo, "origin").mkdir(parents=True)
207 assert _resolve_branch_commit_id(repo, "origin/nonexistent") is None
208
209 def test_remote_tracking_ref_no_remotes_dir_returns_none(self, repo: pathlib.Path) -> None:
210 assert _resolve_branch_commit_id(repo, "origin/dev") is None
211
212 def test_ref_without_slash_only_checks_local(self, repo: pathlib.Path) -> None:
213 # "dev" has no slash — never checks .muse/remotes/
214 assert _resolve_branch_commit_id(repo, "dev") is None
215
216 def test_local_branch_takes_priority_over_remote(self, repo: pathlib.Path) -> None:
217 local_id = long_id("b" * 64)
218 remote_id = long_id("c" * 64)
219 # Both a local branch named "origin/dev" (pathological) and a remote ref exist.
220 local_branch_dir = heads_dir(repo) / "origin"
221 local_branch_dir.mkdir(parents=True)
222 (local_branch_dir / "dev").write_text(local_id)
223 remote_dir = remote_tracking_dir(repo, "origin")
224 remote_dir.mkdir(parents=True)
225 (remote_dir / "dev").write_text(remote_id)
226 # Local branch wins.
227 assert _resolve_branch_commit_id(repo, "origin/dev") == local_id
228
229
230 class TestGetCommitsForBranch:
231 def test_chain(self, repo: pathlib.Path) -> None:
232 root = _make_commit(repo, "ignored", fake_id("snap-root"), "Root")
233 child = _make_commit(repo, "ignored", fake_id("snap-child"), "Child", parent=root.commit_id)
234 grandchild = _make_commit(repo, "ignored", fake_id("snap-grandchild"), "Grandchild", parent=child.commit_id)
235 (heads_dir(repo) / "main").write_text(grandchild.commit_id)
236
237 commits = get_commits_for_branch(repo, "main")
238 assert [c.commit_id for c in commits] == [
239 grandchild.commit_id, child.commit_id, root.commit_id
240 ]
241
242 def test_empty_branch(self, repo: pathlib.Path) -> None:
243 assert get_commits_for_branch(repo, "main") == []
244
245 def test_remote_tracking_ref(self, repo: pathlib.Path) -> None:
246 """get_commits_for_branch resolves 'origin/dev' via .muse/remotes/."""
247 c = _make_commit(repo, "ignored", fake_id("snap"), "Remote commit")
248 remote_dir = remotes_dir(repo) / "origin"
249 remote_dir.mkdir(parents=True)
250 (remote_dir / "dev").write_text(c.commit_id)
251
252 commits = get_commits_for_branch(repo, "origin/dev")
253 assert len(commits) == 1
254 assert commits[0].commit_id == c.commit_id
255
256 def test_remote_tracking_ref_chain(self, repo: pathlib.Path) -> None:
257 root = _make_commit(repo, "ignored", fake_id("snap-root"), "Root")
258 tip = _make_commit(repo, "ignored", fake_id("snap-tip"), "Tip", parent=root.commit_id)
259 remote_dir = remotes_dir(repo) / "upstream"
260 remote_dir.mkdir(parents=True)
261 (remote_dir / "main").write_text(tip.commit_id)
262
263 commits = get_commits_for_branch(repo, "upstream/main")
264 assert [c.commit_id for c in commits] == [tip.commit_id, root.commit_id]
265
266 def test_remote_tracking_ref_missing_returns_empty(self, repo: pathlib.Path) -> None:
267 assert get_commits_for_branch(repo, "origin/dev") == []
268
269 def test_max_count_with_remote_ref(self, repo: pathlib.Path) -> None:
270 commits_written = [_make_commit(repo, "ignored", fake_id(f"snap-s{i}"), f"Commit {i}") for i in range(5)]
271 for i in range(1, 5):
272 commits_written[i] = _make_commit(
273 repo, "ignored", fake_id(f"snap-t{i}"), f"C{i}", parent=commits_written[i - 1].commit_id
274 )
275 tip = _make_commit(repo, "ignored", fake_id("snap-tip"), "Tip", parent=commits_written[-1].commit_id)
276 remote_dir = remotes_dir(repo) / "origin"
277 remote_dir.mkdir(parents=True)
278 (remote_dir / "dev").write_text(tip.commit_id)
279
280 commits = get_commits_for_branch(repo, "origin/dev", max_count=2)
281 assert len(commits) == 2
282 assert commits[0].commit_id == tip.commit_id
283
284
285 class TestFindByPrefix:
286 def test_finds_match(self, repo: pathlib.Path) -> None:
287 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
288 hex_prefix = c.commit_id[len("sha256:"):len("sha256:") + 6]
289 results = find_commits_by_prefix(repo, hex_prefix)
290 assert len(results) == 1
291 assert results[0].commit_id == c.commit_id
292
293 def test_no_match(self, repo: pathlib.Path) -> None:
294 assert find_commits_by_prefix(repo, "zzz") == []
295
296
297 class TestTags:
298 def test_write_and_read(self, repo: pathlib.Path) -> None:
299 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
300 repo_id = fake_id("test-repo")
301 write_tag(repo, TagRecord(
302 repo_id=repo_id,
303 tag_id=fake_id("tag1"),
304 commit_id=c.commit_id,
305 tag="emotion:joyful",
306 ))
307 tags = get_tags_for_commit(repo, repo_id, c.commit_id)
308 assert len(tags) == 1
309 assert tags[0].tag == "emotion:joyful"
310
311 def test_get_all_tags(self, repo: pathlib.Path) -> None:
312 c = _make_commit(repo, "ignored", fake_id("snap"), "msg")
313 repo_id = fake_id("test-repo")
314 write_tag(repo, TagRecord(tag_id=fake_id("t1"), repo_id=repo_id, commit_id=c.commit_id, tag="stage:rough-mix"))
315 write_tag(repo, TagRecord(tag_id=fake_id("t2"), repo_id=repo_id, commit_id=c.commit_id, tag="key:Am"))
316 all_tags = get_all_tags(repo, repo_id)
317 assert len(all_tags) == 2
File History 5 commits
sha256:c5131d76c6eada02939111fda4aa8e51b0c1456b9983727cfd6be101916de14e merge: pull local/dev — resolve trivial _EXT_MAP symbol con… Sonnet 4.6 patch 12 days ago
sha256:9c33d61749fff814c5226d5386aa2af7064c2c02788594a25fdd709358132eea fix: _PROPOSAL_PREFIX_RESOLVE_LIMIT 200 → 100 to match hub … Sonnet 4.6 19 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 28 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 28 days ago