test_core_snapshot.py
python
| 1 | """Tests for muse.core.snapshot — content-addressed snapshot computation.""" |
| 2 | from __future__ import annotations |
| 3 | |
| 4 | import pathlib |
| 5 | |
| 6 | import pytest |
| 7 | |
| 8 | from muse.core.snapshot import ( |
| 9 | build_snapshot_manifest, |
| 10 | compute_commit_id, |
| 11 | compute_snapshot_id, |
| 12 | diff_workdir_vs_snapshot, |
| 13 | hash_file, |
| 14 | ) |
| 15 | |
| 16 | |
| 17 | @pytest.fixture |
| 18 | def workdir(tmp_path: pathlib.Path) -> pathlib.Path: |
| 19 | d = tmp_path / "muse-work" |
| 20 | d.mkdir() |
| 21 | return d |
| 22 | |
| 23 | |
| 24 | class TestHashFile: |
| 25 | def test_consistent(self, tmp_path: pathlib.Path) -> None: |
| 26 | f = tmp_path / "file.mid" |
| 27 | f.write_bytes(b"hello world") |
| 28 | assert hash_file(f) == hash_file(f) |
| 29 | |
| 30 | def test_different_content_different_hash(self, tmp_path: pathlib.Path) -> None: |
| 31 | a = tmp_path / "a.mid" |
| 32 | b = tmp_path / "b.mid" |
| 33 | a.write_bytes(b"aaa") |
| 34 | b.write_bytes(b"bbb") |
| 35 | assert hash_file(a) != hash_file(b) |
| 36 | |
| 37 | def test_known_hash(self, tmp_path: pathlib.Path) -> None: |
| 38 | import hashlib |
| 39 | content = b"muse" |
| 40 | f = tmp_path / "f.mid" |
| 41 | f.write_bytes(content) |
| 42 | expected = hashlib.sha256(content).hexdigest() |
| 43 | assert hash_file(f) == expected |
| 44 | |
| 45 | |
| 46 | class TestBuildSnapshotManifest: |
| 47 | def test_empty_workdir(self, workdir: pathlib.Path) -> None: |
| 48 | assert build_snapshot_manifest(workdir) == {} |
| 49 | |
| 50 | def test_single_file(self, workdir: pathlib.Path) -> None: |
| 51 | (workdir / "beat.mid").write_bytes(b"drums") |
| 52 | manifest = build_snapshot_manifest(workdir) |
| 53 | assert "beat.mid" in manifest |
| 54 | assert len(manifest["beat.mid"]) == 64 # sha256 hex |
| 55 | |
| 56 | def test_nested_file(self, workdir: pathlib.Path) -> None: |
| 57 | (workdir / "tracks").mkdir() |
| 58 | (workdir / "tracks" / "bass.mid").write_bytes(b"bass") |
| 59 | manifest = build_snapshot_manifest(workdir) |
| 60 | assert "tracks/bass.mid" in manifest |
| 61 | |
| 62 | def test_hidden_files_excluded(self, workdir: pathlib.Path) -> None: |
| 63 | (workdir / ".DS_Store").write_bytes(b"junk") |
| 64 | (workdir / "beat.mid").write_bytes(b"drums") |
| 65 | manifest = build_snapshot_manifest(workdir) |
| 66 | assert ".DS_Store" not in manifest |
| 67 | assert "beat.mid" in manifest |
| 68 | |
| 69 | def test_deterministic_order(self, workdir: pathlib.Path) -> None: |
| 70 | for name in ["c.mid", "a.mid", "b.mid"]: |
| 71 | (workdir / name).write_bytes(name.encode()) |
| 72 | m1 = build_snapshot_manifest(workdir) |
| 73 | m2 = build_snapshot_manifest(workdir) |
| 74 | assert m1 == m2 |
| 75 | |
| 76 | |
| 77 | class TestComputeSnapshotId: |
| 78 | def test_empty_manifest(self) -> None: |
| 79 | sid = compute_snapshot_id({}) |
| 80 | assert len(sid) == 64 |
| 81 | |
| 82 | def test_deterministic(self) -> None: |
| 83 | manifest = {"a.mid": "hash1", "b.mid": "hash2"} |
| 84 | assert compute_snapshot_id(manifest) == compute_snapshot_id(manifest) |
| 85 | |
| 86 | def test_order_independent(self) -> None: |
| 87 | m1 = {"a.mid": "h1", "b.mid": "h2"} |
| 88 | m2 = {"b.mid": "h2", "a.mid": "h1"} |
| 89 | assert compute_snapshot_id(m1) == compute_snapshot_id(m2) |
| 90 | |
| 91 | def test_different_content_different_id(self) -> None: |
| 92 | m1 = {"a.mid": "h1"} |
| 93 | m2 = {"a.mid": "h2"} |
| 94 | assert compute_snapshot_id(m1) != compute_snapshot_id(m2) |
| 95 | |
| 96 | |
| 97 | class TestComputeCommitId: |
| 98 | def test_deterministic(self) -> None: |
| 99 | kwargs = dict(parent_ids=["p1"], snapshot_id="s1", message="msg", committed_at_iso="2026-01-01T00:00:00+00:00") |
| 100 | assert compute_commit_id(**kwargs) == compute_commit_id(**kwargs) |
| 101 | |
| 102 | def test_parent_order_independent(self) -> None: |
| 103 | a = compute_commit_id(parent_ids=["p1", "p2"], snapshot_id="s1", message="m", committed_at_iso="t") |
| 104 | b = compute_commit_id(parent_ids=["p2", "p1"], snapshot_id="s1", message="m", committed_at_iso="t") |
| 105 | assert a == b |
| 106 | |
| 107 | def test_different_messages_different_ids(self) -> None: |
| 108 | a = compute_commit_id(parent_ids=[], snapshot_id="s1", message="msg1", committed_at_iso="t") |
| 109 | b = compute_commit_id(parent_ids=[], snapshot_id="s1", message="msg2", committed_at_iso="t") |
| 110 | assert a != b |
| 111 | |
| 112 | |
| 113 | class TestDiffWorkdirVsSnapshot: |
| 114 | def test_new_repo_all_untracked(self, workdir: pathlib.Path) -> None: |
| 115 | (workdir / "beat.mid").write_bytes(b"x") |
| 116 | added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, {}) |
| 117 | assert added == set() |
| 118 | assert untracked == {"beat.mid"} |
| 119 | |
| 120 | def test_added_file(self, workdir: pathlib.Path) -> None: |
| 121 | (workdir / "beat.mid").write_bytes(b"x") |
| 122 | last = {"other.mid": "abc"} |
| 123 | added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, last) |
| 124 | assert "beat.mid" in added |
| 125 | assert "other.mid" in deleted |
| 126 | |
| 127 | def test_modified_file(self, workdir: pathlib.Path) -> None: |
| 128 | f = workdir / "beat.mid" |
| 129 | f.write_bytes(b"new content") |
| 130 | last = {"beat.mid": "oldhash"} |
| 131 | added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, last) |
| 132 | assert "beat.mid" in modified |
| 133 | |
| 134 | def test_clean_workdir(self, workdir: pathlib.Path) -> None: |
| 135 | f = workdir / "beat.mid" |
| 136 | f.write_bytes(b"content") |
| 137 | from muse.core.snapshot import hash_file |
| 138 | h = hash_file(f) |
| 139 | added, modified, deleted, untracked = diff_workdir_vs_snapshot(workdir, {"beat.mid": h}) |
| 140 | assert not added and not modified and not deleted and not untracked |