"""Tests for muse.core.store — file-based commit and snapshot storage.""" import datetime import json import pathlib import pytest from muse.core.ids import hash_commit as compute_commit_id, hash_snapshot as compute_snapshot_id from muse.core.types import Manifest, fake_id, long_id from muse.core.paths import muse_dir, heads_dir, remote_tracking_dir, remotes_dir from muse.core.refs import get_head_commit_id from muse.core.commits import ( CommitDict, CommitRecord, _resolve_branch_commit_id, find_commits_by_prefix, get_all_commits, get_commits_for_branch, get_head_snapshot_id, read_commit, update_commit_metadata, write_commit, ) from muse.core.snapshots import ( SnapshotRecord, get_head_snapshot_manifest, read_snapshot, write_snapshot, ) from muse.core.tags import ( TagRecord, get_all_tags, get_tags_for_commit, write_tag, ) @pytest.fixture def repo(tmp_path: pathlib.Path) -> pathlib.Path: """Create a minimal .muse/ directory structure.""" dot_muse = muse_dir(tmp_path) (dot_muse / "commits").mkdir(parents=True) (dot_muse / "snapshots").mkdir(parents=True) (dot_muse / "refs" / "heads").mkdir(parents=True) (dot_muse / "repo.json").write_text(json.dumps({"repo_id": "test-repo"})) (dot_muse / "HEAD").write_text("ref: refs/heads/main\n") (dot_muse / "refs" / "heads" / "main").write_text("") return tmp_path def _make_commit( root: pathlib.Path, commit_id: str, snapshot_id: str, message: str, parent: str | None = None, ) -> CommitRecord: """Write a commit whose ID is computed from its content fields. The ``commit_id`` parameter is kept for call-site readability but is ignored — the real ID is derived by :func:`~muse.core.snapshot.compute_commit_id` so every stored commit satisfies the I-10 content-hash verification. """ now = datetime.datetime.now(datetime.timezone.utc) parents: list[str] = [parent] if parent else [] real_id = compute_commit_id( parent_ids=parents, snapshot_id=snapshot_id, message=message, committed_at_iso=now.isoformat(), ) c = CommitRecord( commit_id=real_id, branch="main", snapshot_id=snapshot_id, message=message, committed_at=now, parent_commit_id=parent, ) write_commit(root, c) return c def _make_snapshot(root: pathlib.Path, snapshot_id: str, manifest: Manifest) -> SnapshotRecord: """Write a snapshot whose ID is computed from its manifest. The ``snapshot_id`` parameter is kept for call-site readability but is ignored — the real ID is derived by :func:`~muse.core.snapshot.compute_snapshot_id` so every stored snapshot satisfies the I-10 content-hash verification. """ real_id = compute_snapshot_id(manifest) s = SnapshotRecord(snapshot_id=real_id, manifest=manifest) write_snapshot(root, s) return s class TestWriteReadCommit: def test_roundtrip(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "Initial commit") loaded = read_commit(repo, c.commit_id) assert loaded is not None assert loaded.commit_id == c.commit_id assert loaded.message == "Initial commit" def test_read_missing_returns_none(self, repo: pathlib.Path) -> None: assert read_commit(repo, fake_id("nonexistent-commit")) is None def test_idempotent_write(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "First") _make_commit(repo, "ignored", fake_id("snap"), "Second") # different timestamp → different ID; should write loaded = read_commit(repo, c.commit_id) assert loaded is not None assert loaded.message == "First" def test_metadata_preserved(self, repo: pathlib.Path) -> None: now = datetime.datetime.now(datetime.timezone.utc) snap_id = fake_id("snap") cid = compute_commit_id(parent_ids=[], snapshot_id=snap_id, message="With metadata", committed_at_iso=now.isoformat()) c = CommitRecord( commit_id=cid, branch="main", snapshot_id=snap_id, message="With metadata", committed_at=now, metadata={"section": "chorus", "emotion": "joyful"}, ) write_commit(repo, c) loaded = read_commit(repo, cid) assert loaded is not None assert loaded.metadata["section"] == "chorus" assert loaded.metadata["emotion"] == "joyful" class TestUpdateCommitMetadata: def test_set_key(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") result = update_commit_metadata(repo, c.commit_id, "tempo_bpm", "120.0") assert result is True loaded = read_commit(repo, c.commit_id) assert loaded is not None assert loaded.metadata["tempo_bpm"] == "120.0" def test_missing_commit_returns_false(self, repo: pathlib.Path) -> None: assert update_commit_metadata(repo, fake_id("nonexistent-commit"), "k", "v") is False class TestWriteReadSnapshot: def test_roundtrip(self, repo: pathlib.Path) -> None: _drum_id = fake_id("deadbeef") s = _make_snapshot(repo, "ignored", {"tracks/drums.mid": _drum_id}) loaded = read_snapshot(repo, s.snapshot_id) assert loaded is not None assert loaded.manifest == {"tracks/drums.mid": _drum_id} def test_read_missing_returns_none(self, repo: pathlib.Path) -> None: assert read_snapshot(repo, fake_id("nonexistent-snapshot")) is None class TestHeadQueries: def test_get_head_commit_id_empty_branch(self, repo: pathlib.Path) -> None: assert get_head_commit_id(repo, "main") is None def test_get_head_commit_id(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") (heads_dir(repo) / "main").write_text(c.commit_id) assert get_head_commit_id(repo, "main") == c.commit_id def test_get_head_snapshot_id(self, repo: pathlib.Path) -> None: _f_id = fake_id("f.mid-content") snap = _make_snapshot(repo, "ignored", {"f.mid": _f_id}) c = _make_commit(repo, "ignored", snap.snapshot_id, "msg") (heads_dir(repo) / "main").write_text(c.commit_id) assert get_head_snapshot_id(repo, "main") == snap.snapshot_id def test_get_head_snapshot_manifest(self, repo: pathlib.Path) -> None: _f_id = fake_id("f.mid-content") snap = _make_snapshot(repo, "ignored", {"f.mid": _f_id}) c = _make_commit(repo, "ignored", snap.snapshot_id, "msg") (heads_dir(repo) / "main").write_text(c.commit_id) manifest = get_head_snapshot_manifest(repo, "main") assert manifest == {"f.mid": _f_id} class TestResolveRemoteBranchCommitId: """_resolve_branch_commit_id handles local branches and remote tracking refs.""" def test_local_branch_resolved(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") (heads_dir(repo) / "main").write_text(c.commit_id) assert _resolve_branch_commit_id(repo, "main") == c.commit_id def test_empty_local_branch_returns_none(self, repo: pathlib.Path) -> None: assert _resolve_branch_commit_id(repo, "main") is None def test_remote_tracking_ref_resolved(self, repo: pathlib.Path) -> None: remote_dir = remote_tracking_dir(repo, "origin") remote_dir.mkdir(parents=True) cid = long_id("a" * 64) (remote_dir / "dev").write_text(cid) assert _resolve_branch_commit_id(repo, "origin/dev") == cid def test_remote_tracking_ref_missing_returns_none(self, repo: pathlib.Path) -> None: remote_tracking_dir(repo, "origin").mkdir(parents=True) assert _resolve_branch_commit_id(repo, "origin/nonexistent") is None def test_remote_tracking_ref_no_remotes_dir_returns_none(self, repo: pathlib.Path) -> None: assert _resolve_branch_commit_id(repo, "origin/dev") is None def test_ref_without_slash_only_checks_local(self, repo: pathlib.Path) -> None: # "dev" has no slash — never checks .muse/remotes/ assert _resolve_branch_commit_id(repo, "dev") is None def test_local_branch_takes_priority_over_remote(self, repo: pathlib.Path) -> None: local_id = long_id("b" * 64) remote_id = long_id("c" * 64) # Both a local branch named "origin/dev" (pathological) and a remote ref exist. local_branch_dir = heads_dir(repo) / "origin" local_branch_dir.mkdir(parents=True) (local_branch_dir / "dev").write_text(local_id) remote_dir = remote_tracking_dir(repo, "origin") remote_dir.mkdir(parents=True) (remote_dir / "dev").write_text(remote_id) # Local branch wins. assert _resolve_branch_commit_id(repo, "origin/dev") == local_id class TestGetCommitsForBranch: def test_chain(self, repo: pathlib.Path) -> None: root = _make_commit(repo, "ignored", fake_id("snap-root"), "Root") child = _make_commit(repo, "ignored", fake_id("snap-child"), "Child", parent=root.commit_id) grandchild = _make_commit(repo, "ignored", fake_id("snap-grandchild"), "Grandchild", parent=child.commit_id) (heads_dir(repo) / "main").write_text(grandchild.commit_id) commits = get_commits_for_branch(repo, "main") assert [c.commit_id for c in commits] == [ grandchild.commit_id, child.commit_id, root.commit_id ] def test_empty_branch(self, repo: pathlib.Path) -> None: assert get_commits_for_branch(repo, "main") == [] def test_remote_tracking_ref(self, repo: pathlib.Path) -> None: """get_commits_for_branch resolves 'origin/dev' via .muse/remotes/.""" c = _make_commit(repo, "ignored", fake_id("snap"), "Remote commit") remote_dir = remotes_dir(repo) / "origin" remote_dir.mkdir(parents=True) (remote_dir / "dev").write_text(c.commit_id) commits = get_commits_for_branch(repo, "origin/dev") assert len(commits) == 1 assert commits[0].commit_id == c.commit_id def test_remote_tracking_ref_chain(self, repo: pathlib.Path) -> None: root = _make_commit(repo, "ignored", fake_id("snap-root"), "Root") tip = _make_commit(repo, "ignored", fake_id("snap-tip"), "Tip", parent=root.commit_id) remote_dir = remotes_dir(repo) / "upstream" remote_dir.mkdir(parents=True) (remote_dir / "main").write_text(tip.commit_id) commits = get_commits_for_branch(repo, "upstream/main") assert [c.commit_id for c in commits] == [tip.commit_id, root.commit_id] def test_remote_tracking_ref_missing_returns_empty(self, repo: pathlib.Path) -> None: assert get_commits_for_branch(repo, "origin/dev") == [] def test_max_count_with_remote_ref(self, repo: pathlib.Path) -> None: commits_written = [_make_commit(repo, "ignored", fake_id(f"snap-s{i}"), f"Commit {i}") for i in range(5)] for i in range(1, 5): commits_written[i] = _make_commit( repo, "ignored", fake_id(f"snap-t{i}"), f"C{i}", parent=commits_written[i - 1].commit_id ) tip = _make_commit(repo, "ignored", fake_id("snap-tip"), "Tip", parent=commits_written[-1].commit_id) remote_dir = remotes_dir(repo) / "origin" remote_dir.mkdir(parents=True) (remote_dir / "dev").write_text(tip.commit_id) commits = get_commits_for_branch(repo, "origin/dev", max_count=2) assert len(commits) == 2 assert commits[0].commit_id == tip.commit_id class TestFindByPrefix: def test_finds_match(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") hex_prefix = c.commit_id[len("sha256:"):len("sha256:") + 6] results = find_commits_by_prefix(repo, hex_prefix) assert len(results) == 1 assert results[0].commit_id == c.commit_id def test_no_match(self, repo: pathlib.Path) -> None: assert find_commits_by_prefix(repo, "zzz") == [] class TestTags: def test_write_and_read(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") repo_id = fake_id("test-repo") write_tag(repo, TagRecord( repo_id=repo_id, tag_id=fake_id("tag1"), commit_id=c.commit_id, tag="emotion:joyful", )) tags = get_tags_for_commit(repo, repo_id, c.commit_id) assert len(tags) == 1 assert tags[0].tag == "emotion:joyful" def test_get_all_tags(self, repo: pathlib.Path) -> None: c = _make_commit(repo, "ignored", fake_id("snap"), "msg") repo_id = fake_id("test-repo") write_tag(repo, TagRecord(tag_id=fake_id("t1"), repo_id=repo_id, commit_id=c.commit_id, tag="stage:rough-mix")) write_tag(repo, TagRecord(tag_id=fake_id("t2"), repo_id=repo_id, commit_id=c.commit_id, tag="key:Am")) all_tags = get_all_tags(repo, repo_id) assert len(all_tags) == 2