gabriel / musehub public

test_musehub_contracts.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
1 """Unit tests for musehub/contracts/hash_utils.py.
2
3 The contract hashing module enforces deterministic SHA-256 fingerprinting of
4 music generation contracts. These tests lock down:
5
6 - canonical_contract_dict field exclusions
7 - _normalize_value handling of nested types
8 - hash stability across runs
9 - contract_hash exclusion prevents circular dependency
10 """
11 from __future__ import annotations
12
13 import dataclasses
14 import json
15
16 import pytest
17 from muse.core.types import content_hash
18
19 from musehub.types.hash_utils import (
20 _HASH_EXCLUDED_FIELDS,
21 _normalize_value,
22 canonical_contract_dict,
23 )
24
25
26 # ---------------------------------------------------------------------------
27 # Minimal dataclass fixtures
28 # ---------------------------------------------------------------------------
29
30 @dataclasses.dataclass(frozen=True)
31 class SimpleContract:
32 tempo: int = 120
33 key: str = "C major"
34 # advisory fields that should be excluded from hashes
35 contract_hash: str = ""
36 contract_version: str = "1.0"
37 region_name: str = ""
38
39
40 @dataclasses.dataclass(frozen=True)
41 class NestedContract:
42 name: str = "outer"
43 inner: SimpleContract = dataclasses.field(default_factory=SimpleContract)
44
45
46 # ---------------------------------------------------------------------------
47 # _HASH_EXCLUDED_FIELDS
48 # ---------------------------------------------------------------------------
49
50 class TestHashExcludedFields:
51 def test_advisory_fields_excluded(self) -> None:
52 for field in (
53 "contract_hash",
54 "parent_contract_hash",
55 "contract_version",
56 "execution_hash",
57 "l2_generate_prompt",
58 "region_name",
59 "gm_guidance",
60 "assigned_color",
61 "existing_track_id",
62 ):
63 assert field in _HASH_EXCLUDED_FIELDS, f"{field!r} should be excluded"
64
65
66 # ---------------------------------------------------------------------------
67 # _normalize_value
68 # ---------------------------------------------------------------------------
69
70 class TestNormalizeValue:
71 def test_primitives_passthrough(self) -> None:
72 assert _normalize_value(42) == 42
73 assert _normalize_value(3.14) == 3.14
74 assert _normalize_value("hello") == "hello"
75 assert _normalize_value(True) is True
76 assert _normalize_value(None) is None
77
78 def test_list_normalized(self) -> None:
79 result = _normalize_value([3, 1, 2])
80 assert result == [3, 1, 2]
81
82 def test_tuple_becomes_list(self) -> None:
83 result = _normalize_value((1, 2, 3))
84 assert result == [1, 2, 3]
85
86 def test_dict_keys_sorted(self) -> None:
87 result = _normalize_value({"z": 1, "a": 2})
88 assert isinstance(result, dict)
89 assert list(result.keys()) == ["a", "z"]
90
91 def test_dataclass_converted_to_dict(self) -> None:
92 obj = SimpleContract(tempo=90, key="D minor")
93 result = _normalize_value(obj)
94 assert isinstance(result, dict)
95 assert "tempo" in result
96 assert "key" in result
97 # Excluded fields should not appear
98 assert "contract_hash" not in result
99
100 def test_unknown_type_stringified(self) -> None:
101 class Weird:
102 def __str__(self) -> str:
103 return "weird-value"
104 result = _normalize_value(Weird())
105 assert result == "weird-value"
106
107
108 # ---------------------------------------------------------------------------
109 # canonical_contract_dict
110 # ---------------------------------------------------------------------------
111
112 class TestCanonicalContractDict:
113 def test_excluded_fields_absent(self) -> None:
114 obj = SimpleContract(tempo=120, key="G major")
115 d = canonical_contract_dict(obj)
116 for excluded in _HASH_EXCLUDED_FIELDS:
117 assert excluded not in d, f"{excluded!r} should not appear in canonical dict"
118
119 def test_included_fields_present(self) -> None:
120 obj = SimpleContract(tempo=120, key="A minor")
121 d = canonical_contract_dict(obj)
122 assert "tempo" in d
123 assert "key" in d
124 assert d["tempo"] == 120
125 assert d["key"] == "A minor"
126
127 def test_deterministic_across_calls(self) -> None:
128 obj = SimpleContract(tempo=100, key="F major")
129 d1 = canonical_contract_dict(obj)
130 d2 = canonical_contract_dict(obj)
131 assert d1 == d2
132
133 def test_same_data_same_json(self) -> None:
134 obj = SimpleContract(tempo=140, key="B♭ major")
135 d = canonical_contract_dict(obj)
136 j1 = json.dumps(d, sort_keys=True)
137 j2 = json.dumps(canonical_contract_dict(obj), sort_keys=True)
138 assert j1 == j2
139
140 def test_nested_dataclass_recursed(self) -> None:
141 obj = NestedContract(name="outer", inner=SimpleContract(tempo=70))
142 d = canonical_contract_dict(obj)
143 assert "name" in d
144 assert "inner" in d
145 inner = d["inner"]
146 assert isinstance(inner, dict)
147 assert inner["tempo"] == 70
148 assert "contract_hash" not in inner
149
150 def test_different_values_different_hash(self) -> None:
151 obj1 = SimpleContract(tempo=120)
152 obj2 = SimpleContract(tempo=140)
153 d1 = canonical_contract_dict(obj1)
154 d2 = canonical_contract_dict(obj2)
155 assert content_hash(d1) != content_hash(d2)