gabriel / musehub public
test_musehub_muse_cli.py python
105 lines 4.5 KB
58180686 dev → main: wire protocol fixes + musehub_publish_domain MCP tool (#18) Gabriel Cardona <cgcardona@gmail.com> 2d ago
1 """Unit tests for muse_cli snapshot hashing utilities.
2
3 muse_cli/snapshot.py provides deterministic ID functions used by test
4 fixtures across the suite — yet it had no tests of its own. These
5 tests lock down the hashing contract so that any accidental change to
6 the algorithm is immediately caught.
7 """
8 from __future__ import annotations
9
10 import hashlib
11
12 import pytest
13
14 from musehub.muse_cli.snapshot import compute_commit_id, compute_snapshot_id
15
16
17 class TestComputeSnapshotId:
18 def test_empty_manifest_is_deterministic(self) -> None:
19 result = compute_snapshot_id({})
20 assert isinstance(result, str)
21 assert len(result) == 64 # SHA-256 hex
22
23 def test_single_entry(self) -> None:
24 result = compute_snapshot_id({"tracks/bass.mid": "sha256:abc"})
25 assert len(result) == 64
26
27 def test_same_manifest_same_id(self) -> None:
28 manifest = {"a": "1", "b": "2"}
29 assert compute_snapshot_id(manifest) == compute_snapshot_id(manifest)
30
31 def test_insertion_order_does_not_matter(self) -> None:
32 m1 = {"a": "1", "b": "2"}
33 m2 = {"b": "2", "a": "1"}
34 assert compute_snapshot_id(m1) == compute_snapshot_id(m2)
35
36 def test_different_manifests_different_ids(self) -> None:
37 assert compute_snapshot_id({"a": "1"}) != compute_snapshot_id({"a": "2"})
38 assert compute_snapshot_id({"a": "1"}) != compute_snapshot_id({"b": "1"})
39
40 def test_known_value(self) -> None:
41 """Regression: algorithm must not change without updating this test.
42
43 Uses the null-byte separator (\\x00) introduced when the CLI migrated
44 from the old ``|``/``:`` scheme to prevent separator-injection attacks.
45 """
46 _SEP = "\x00"
47 manifest = {"tracks/piano.mid": "sha256:deadbeef"}
48 parts = sorted(f"{k}{_SEP}{v}" for k, v in manifest.items())
49 payload = _SEP.join(parts).encode()
50 expected = hashlib.sha256(payload).hexdigest()
51 assert compute_snapshot_id(manifest) == expected
52
53 def test_multiple_entries_sorted(self) -> None:
54 _SEP = "\x00"
55 m = {"z/file": "oid-z", "a/file": "oid-a", "m/file": "oid-m"}
56 result = compute_snapshot_id(m)
57 # Manually compute expected value using null-byte separator
58 parts = sorted(f"{k}{_SEP}{v}" for k, v in m.items())
59 expected = hashlib.sha256(_SEP.join(parts).encode()).hexdigest()
60 assert result == expected
61
62
63 class TestComputeCommitId:
64 def test_returns_sha256_hex(self) -> None:
65 cid = compute_commit_id([], "snap-id", "init", "2026-01-01T00:00:00+00:00")
66 assert len(cid) == 64
67
68 def test_deterministic(self) -> None:
69 kwargs = dict(
70 parent_ids=["p1", "p2"],
71 snapshot_id="snap-abc",
72 message="feat: add piano",
73 committed_at_iso="2026-03-01T12:00:00+00:00",
74 )
75 assert compute_commit_id(**kwargs) == compute_commit_id(**kwargs)
76
77 def test_parent_order_does_not_matter(self) -> None:
78 base = dict(snapshot_id="s", message="m", committed_at_iso="2026-01-01T00:00:00")
79 id1 = compute_commit_id(parent_ids=["a", "b"], **base)
80 id2 = compute_commit_id(parent_ids=["b", "a"], **base)
81 assert id1 == id2
82
83 def test_different_messages_different_ids(self) -> None:
84 base = dict(parent_ids=[], snapshot_id="s", committed_at_iso="2026-01-01T00:00:00")
85 assert compute_commit_id(message="msg-1", **base) != compute_commit_id(message="msg-2", **base)
86
87 def test_different_timestamps_different_ids(self) -> None:
88 base = dict(parent_ids=[], snapshot_id="s", message="m")
89 t1 = compute_commit_id(committed_at_iso="2026-01-01T00:00:00", **base)
90 t2 = compute_commit_id(committed_at_iso="2026-01-02T00:00:00", **base)
91 assert t1 != t2
92
93 def test_different_snapshots_different_ids(self) -> None:
94 base = dict(parent_ids=[], message="m", committed_at_iso="2026-01-01T00:00:00")
95 assert compute_commit_id(snapshot_id="s1", **base) != compute_commit_id(snapshot_id="s2", **base)
96
97 def test_known_value(self) -> None:
98 """Regression: algorithm must not change without updating this test."""
99 parents = ["parent-a", "parent-b"]
100 snap = "snapshot-xyz"
101 msg = "feat: add groove"
102 ts = "2026-01-01T00:00:00+00:00"
103 parts = ["|".join(sorted(parents)), snap, msg, ts]
104 expected = hashlib.sha256("|".join(parts).encode()).hexdigest()
105 assert compute_commit_id(parent_ids=parents, snapshot_id=snap, message=msg, committed_at_iso=ts) == expected