gabriel / muse public
test_core_store.py python
364 lines 14.6 KB
Raw
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 73 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.snapshot import compute_commit_id, compute_snapshot_id
10
11 from muse.core._types import Manifest
12 from muse.core.store import (
13 CommitDict,
14 CommitRecord,
15 SnapshotRecord,
16 TagRecord,
17 _resolve_branch_commit_id,
18 find_commits_by_prefix,
19 get_all_commits,
20 get_all_tags,
21 get_commits_for_branch,
22 get_head_commit_id,
23 get_head_snapshot_id,
24 get_head_snapshot_manifest,
25 get_tags_for_commit,
26 read_commit,
27 read_snapshot,
28 update_commit_metadata,
29 write_commit,
30 write_snapshot,
31 write_tag,
32 )
33
34
35 @pytest.fixture
36 def repo(tmp_path: pathlib.Path) -> pathlib.Path:
37 """Create a minimal .muse/ directory structure."""
38 muse_dir = tmp_path / ".muse"
39 (muse_dir / "commits").mkdir(parents=True)
40 (muse_dir / "snapshots").mkdir(parents=True)
41 (muse_dir / "refs" / "heads").mkdir(parents=True)
42 (muse_dir / "repo.json").write_text(json.dumps({"repo_id": "test-repo"}))
43 (muse_dir / "HEAD").write_text("ref: refs/heads/main\n")
44 (muse_dir / "refs" / "heads" / "main").write_text("")
45 return tmp_path
46
47
48 def _make_commit(
49 root: pathlib.Path,
50 commit_id: str,
51 snapshot_id: str,
52 message: str,
53 parent: str | None = None,
54 ) -> CommitRecord:
55 """Write a commit whose ID is computed from its content fields.
56
57 The ``commit_id`` parameter is kept for call-site readability but is
58 ignored — the real ID is derived by :func:`~muse.core.snapshot.compute_commit_id`
59 so every stored commit satisfies the I-10 content-hash verification.
60 """
61 now = datetime.datetime.now(datetime.timezone.utc)
62 parents: list[str] = [parent] if parent else []
63 real_id = compute_commit_id(parents, snapshot_id, message, now.isoformat())
64 c = CommitRecord(
65 commit_id=real_id,
66 repo_id="test-repo",
67 branch="main",
68 snapshot_id=snapshot_id,
69 message=message,
70 committed_at=now,
71 parent_commit_id=parent,
72 )
73 write_commit(root, c)
74 return c
75
76
77 def _make_snapshot(root: pathlib.Path, snapshot_id: str, manifest: Manifest) -> SnapshotRecord:
78 """Write a snapshot whose ID is computed from its manifest.
79
80 The ``snapshot_id`` parameter is kept for call-site readability but is
81 ignored — the real ID is derived by
82 :func:`~muse.core.snapshot.compute_snapshot_id` so every stored snapshot
83 satisfies the I-10 content-hash verification.
84 """
85 real_id = compute_snapshot_id(manifest)
86 s = SnapshotRecord(snapshot_id=real_id, manifest=manifest)
87 write_snapshot(root, s)
88 return s
89
90
91 class TestFormatVersion:
92 """CommitRecord.format_version tracks schema evolution."""
93
94 def test_new_commit_has_format_version_7(self, repo: pathlib.Path) -> None:
95 """format_version 7 is the current default — Ed25519 provenance signing."""
96 c = _make_commit(repo, "ignored", "s" * 64, "msg")
97 assert c.format_version == 7
98
99 def test_format_version_round_trips_through_store_v7(self, repo: pathlib.Path) -> None:
100 """A v7 record serialises and deserialises with format_version preserved."""
101 c = _make_commit(repo, "ignored", "s" * 64, "msg")
102 loaded = read_commit(repo, c.commit_id)
103 assert loaded is not None
104 assert loaded.format_version == 7
105
106 def test_format_version_in_serialised_dict(self) -> None:
107 c = CommitRecord(
108 commit_id="x",
109 repo_id="r",
110 branch="main",
111 snapshot_id="s",
112 message="m",
113 committed_at=datetime.datetime.now(datetime.timezone.utc),
114 )
115 d = c.to_dict()
116 assert "format_version" in d
117 assert d["format_version"] == 7
118
119 def test_missing_format_version_defaults_to_1(self) -> None:
120 """Existing JSON without format_version field deserialises as version 1."""
121 raw = CommitDict(
122 commit_id="abc",
123 repo_id="r",
124 branch="main",
125 snapshot_id="s",
126 message="old record",
127 committed_at="2025-01-01T00:00:00+00:00",
128 )
129 c = CommitRecord.from_dict(raw)
130 assert c.format_version == 1
131
132 def test_explicit_format_version_preserved(self) -> None:
133 raw = CommitDict(
134 commit_id="abc",
135 repo_id="r",
136 branch="main",
137 snapshot_id="s",
138 message="versioned record",
139 committed_at="2025-01-01T00:00:00+00:00",
140 format_version=2,
141 )
142 c = CommitRecord.from_dict(raw)
143 assert c.format_version == 2
144
145 def test_format_version_field_is_integer(self, repo: pathlib.Path) -> None:
146 c = _make_commit(repo, "ignored", "s" * 64, "msg")
147 loaded = read_commit(repo, c.commit_id)
148 assert loaded is not None
149 assert isinstance(loaded.format_version, int)
150
151
152 class TestWriteReadCommit:
153 def test_roundtrip(self, repo: pathlib.Path) -> None:
154 c = _make_commit(repo, "ignored", "s" * 64, "Initial commit")
155 loaded = read_commit(repo, c.commit_id)
156 assert loaded is not None
157 assert loaded.commit_id == c.commit_id
158 assert loaded.message == "Initial commit"
159 assert loaded.repo_id == "test-repo"
160
161 def test_read_missing_returns_none(self, repo: pathlib.Path) -> None:
162 assert read_commit(repo, "nonexistent") is None
163
164 def test_idempotent_write(self, repo: pathlib.Path) -> None:
165 c = _make_commit(repo, "ignored", "s" * 64, "First")
166 _make_commit(repo, "ignored", "s" * 64, "Second") # different timestamp → different ID; should write
167 loaded = read_commit(repo, c.commit_id)
168 assert loaded is not None
169 assert loaded.message == "First"
170
171 def test_metadata_preserved(self, repo: pathlib.Path) -> None:
172 now = datetime.datetime.now(datetime.timezone.utc)
173 snap_id = "s" * 64
174 cid = compute_commit_id([], snap_id, "With metadata", now.isoformat())
175 c = CommitRecord(
176 commit_id=cid,
177 repo_id="test-repo",
178 branch="main",
179 snapshot_id=snap_id,
180 message="With metadata",
181 committed_at=now,
182 metadata={"section": "chorus", "emotion": "joyful"},
183 )
184 write_commit(repo, c)
185 loaded = read_commit(repo, cid)
186 assert loaded is not None
187 assert loaded.metadata["section"] == "chorus"
188 assert loaded.metadata["emotion"] == "joyful"
189
190
191 class TestUpdateCommitMetadata:
192 def test_set_key(self, repo: pathlib.Path) -> None:
193 c = _make_commit(repo, "ignored", "s" * 64, "msg")
194 result = update_commit_metadata(repo, c.commit_id, "tempo_bpm", "120.0")
195 assert result is True
196 loaded = read_commit(repo, c.commit_id)
197 assert loaded is not None
198 assert loaded.metadata["tempo_bpm"] == "120.0"
199
200 def test_missing_commit_returns_false(self, repo: pathlib.Path) -> None:
201 assert update_commit_metadata(repo, "missing", "k", "v") is False
202
203
204 class TestWriteReadSnapshot:
205 def test_roundtrip(self, repo: pathlib.Path) -> None:
206 s = _make_snapshot(repo, "ignored", {"tracks/drums.mid": "deadbeef"})
207 loaded = read_snapshot(repo, s.snapshot_id)
208 assert loaded is not None
209 assert loaded.manifest == {"tracks/drums.mid": "deadbeef"}
210
211 def test_read_missing_returns_none(self, repo: pathlib.Path) -> None:
212 assert read_snapshot(repo, "nonexistent") is None
213
214
215 class TestHeadQueries:
216 def test_get_head_commit_id_empty_branch(self, repo: pathlib.Path) -> None:
217 assert get_head_commit_id(repo, "main") is None
218
219 def test_get_head_commit_id(self, repo: pathlib.Path) -> None:
220 c = _make_commit(repo, "ignored", "s" * 64, "msg")
221 (repo / ".muse" / "refs" / "heads" / "main").write_text(c.commit_id)
222 assert get_head_commit_id(repo, "main") == c.commit_id
223
224 def test_get_head_snapshot_id(self, repo: pathlib.Path) -> None:
225 snap = _make_snapshot(repo, "ignored", {"f.mid": "hash1"})
226 c = _make_commit(repo, "ignored", snap.snapshot_id, "msg")
227 (repo / ".muse" / "refs" / "heads" / "main").write_text(c.commit_id)
228 assert get_head_snapshot_id(repo, "test-repo", "main") == snap.snapshot_id
229
230 def test_get_head_snapshot_manifest(self, repo: pathlib.Path) -> None:
231 snap = _make_snapshot(repo, "ignored", {"f.mid": "hash1"})
232 c = _make_commit(repo, "ignored", snap.snapshot_id, "msg")
233 (repo / ".muse" / "refs" / "heads" / "main").write_text(c.commit_id)
234 manifest = get_head_snapshot_manifest(repo, "test-repo", "main")
235 assert manifest == {"f.mid": "hash1"}
236
237
238 class TestResolveRemoteBranchCommitId:
239 """_resolve_branch_commit_id handles local branches and remote tracking refs."""
240
241 def test_local_branch_resolved(self, repo: pathlib.Path) -> None:
242 c = _make_commit(repo, "ignored", "s" * 64, "msg")
243 (repo / ".muse" / "refs" / "heads" / "main").write_text(c.commit_id)
244 assert _resolve_branch_commit_id(repo, "main") == c.commit_id
245
246 def test_empty_local_branch_returns_none(self, repo: pathlib.Path) -> None:
247 assert _resolve_branch_commit_id(repo, "main") is None
248
249 def test_remote_tracking_ref_resolved(self, repo: pathlib.Path) -> None:
250 remote_dir = repo / ".muse" / "remotes" / "origin"
251 remote_dir.mkdir(parents=True)
252 (remote_dir / "dev").write_text("a" * 64)
253 assert _resolve_branch_commit_id(repo, "origin/dev") == "a" * 64
254
255 def test_remote_tracking_ref_missing_returns_none(self, repo: pathlib.Path) -> None:
256 (repo / ".muse" / "remotes" / "origin").mkdir(parents=True)
257 assert _resolve_branch_commit_id(repo, "origin/nonexistent") is None
258
259 def test_remote_tracking_ref_no_remotes_dir_returns_none(self, repo: pathlib.Path) -> None:
260 assert _resolve_branch_commit_id(repo, "origin/dev") is None
261
262 def test_ref_without_slash_only_checks_local(self, repo: pathlib.Path) -> None:
263 # "dev" has no slash — never checks .muse/remotes/
264 assert _resolve_branch_commit_id(repo, "dev") is None
265
266 def test_local_branch_takes_priority_over_remote(self, repo: pathlib.Path) -> None:
267 local_id = "b" * 64
268 remote_id = "c" * 64
269 # Both a local branch named "origin/dev" (pathological) and a remote ref exist.
270 feat_dir = repo / ".muse" / "refs" / "heads" / "origin"
271 feat_dir.mkdir(parents=True)
272 (feat_dir / "dev").write_text(local_id)
273 remote_dir = repo / ".muse" / "remotes" / "origin"
274 remote_dir.mkdir(parents=True)
275 (remote_dir / "dev").write_text(remote_id)
276 # Local branch wins.
277 assert _resolve_branch_commit_id(repo, "origin/dev") == local_id
278
279
280 class TestGetCommitsForBranch:
281 def test_chain(self, repo: pathlib.Path) -> None:
282 root = _make_commit(repo, "ignored", "s" * 64, "Root")
283 child = _make_commit(repo, "ignored", "t" * 64, "Child", parent=root.commit_id)
284 grandchild = _make_commit(repo, "ignored", "u" * 64, "Grandchild", parent=child.commit_id)
285 (repo / ".muse" / "refs" / "heads" / "main").write_text(grandchild.commit_id)
286
287 commits = get_commits_for_branch(repo, "test-repo", "main")
288 assert [c.commit_id for c in commits] == [
289 grandchild.commit_id, child.commit_id, root.commit_id
290 ]
291
292 def test_empty_branch(self, repo: pathlib.Path) -> None:
293 assert get_commits_for_branch(repo, "test-repo", "main") == []
294
295 def test_remote_tracking_ref(self, repo: pathlib.Path) -> None:
296 """get_commits_for_branch resolves 'origin/dev' via .muse/remotes/."""
297 c = _make_commit(repo, "ignored", "s" * 64, "Remote commit")
298 remote_dir = repo / ".muse" / "remotes" / "origin"
299 remote_dir.mkdir(parents=True)
300 (remote_dir / "dev").write_text(c.commit_id)
301
302 commits = get_commits_for_branch(repo, "test-repo", "origin/dev")
303 assert len(commits) == 1
304 assert commits[0].commit_id == c.commit_id
305
306 def test_remote_tracking_ref_chain(self, repo: pathlib.Path) -> None:
307 root = _make_commit(repo, "ignored", "s" * 64, "Root")
308 tip = _make_commit(repo, "ignored", "t" * 64, "Tip", parent=root.commit_id)
309 remote_dir = repo / ".muse" / "remotes" / "upstream"
310 remote_dir.mkdir(parents=True)
311 (remote_dir / "main").write_text(tip.commit_id)
312
313 commits = get_commits_for_branch(repo, "test-repo", "upstream/main")
314 assert [c.commit_id for c in commits] == [tip.commit_id, root.commit_id]
315
316 def test_remote_tracking_ref_missing_returns_empty(self, repo: pathlib.Path) -> None:
317 assert get_commits_for_branch(repo, "test-repo", "origin/dev") == []
318
319 def test_max_count_with_remote_ref(self, repo: pathlib.Path) -> None:
320 commits_written = [_make_commit(repo, "ignored", f"{'s' * 63}{i}", f"Commit {i}") for i in range(5)]
321 for i in range(1, 5):
322 commits_written[i] = _make_commit(
323 repo, "ignored", f"{'t' * 63}{i}", f"C{i}", parent=commits_written[i - 1].commit_id
324 )
325 tip = _make_commit(repo, "ignored", "u" * 64, "Tip", parent=commits_written[-1].commit_id)
326 remote_dir = repo / ".muse" / "remotes" / "origin"
327 remote_dir.mkdir(parents=True)
328 (remote_dir / "dev").write_text(tip.commit_id)
329
330 commits = get_commits_for_branch(repo, "test-repo", "origin/dev", max_count=2)
331 assert len(commits) == 2
332 assert commits[0].commit_id == tip.commit_id
333
334
335 class TestFindByPrefix:
336 def test_finds_match(self, repo: pathlib.Path) -> None:
337 c = _make_commit(repo, "ignored", "s" * 64, "msg")
338 results = find_commits_by_prefix(repo, c.commit_id[:6])
339 assert len(results) == 1
340 assert results[0].commit_id == c.commit_id
341
342 def test_no_match(self, repo: pathlib.Path) -> None:
343 assert find_commits_by_prefix(repo, "zzz") == []
344
345
346 class TestTags:
347 def test_write_and_read(self, repo: pathlib.Path) -> None:
348 c = _make_commit(repo, "ignored", "s" * 64, "msg")
349 write_tag(repo, TagRecord(
350 tag_id="tag1",
351 repo_id="test-repo",
352 commit_id=c.commit_id,
353 tag="emotion:joyful",
354 ))
355 tags = get_tags_for_commit(repo, "test-repo", c.commit_id)
356 assert len(tags) == 1
357 assert tags[0].tag == "emotion:joyful"
358
359 def test_get_all_tags(self, repo: pathlib.Path) -> None:
360 c = _make_commit(repo, "ignored", "s" * 64, "msg")
361 write_tag(repo, TagRecord(tag_id="t1", repo_id="test-repo", commit_id=c.commit_id, tag="stage:rough-mix"))
362 write_tag(repo, TagRecord(tag_id="t2", repo_id="test-repo", commit_id=c.commit_id, tag="key:Am"))
363 all_tags = get_all_tags(repo, "test-repo")
364 assert len(all_tags) == 2
File History 1 commit
sha256:1c4b3e3a9a1f300774c3ee662b572a698d5fd405bf765a71e6011a2e9c3eaaaa feat: Muse — version control for the agent era Human 73 days ago