1#[cfg(test)]
13mod tests {
14 use crate::hash;
15 use crate::ir::{Edge, Graph, Node};
16 use crate::patch::{Operation, Patch};
17 use crate::types::*;
18 use proptest::prelude::*;
19 use std::collections::BTreeMap;
20
21 fn arb_node_kind() -> impl Strategy<Value = NodeKind> {
24 prop_oneof![
25 Just(NodeKind::Source),
26 Just(NodeKind::Processor),
27 Just(NodeKind::Analyzer),
28 Just(NodeKind::Sink),
29 Just(NodeKind::Junction),
30 ]
31 }
32
33 fn arb_node() -> impl Strategy<Value = Node> {
34 (arb_node_kind(), "[a-z]{3,8}").prop_map(|(kind, name)| {
35 let ports = match kind {
36 NodeKind::Source => vec![TypedPort {
37 name: "out".into(),
38 direction: PortDirection::Output,
39 domain: ExecutionDomain::Sample,
40 data_type: DataType::Audio { channels: 2 },
41 semantic: PortSemantic::Signal,
42 polarity: PortPolarity::Bipolar,
43 }],
44 NodeKind::Sink => vec![TypedPort {
45 name: "in".into(),
46 direction: PortDirection::Input,
47 domain: ExecutionDomain::Sample,
48 data_type: DataType::Audio { channels: 2 },
49 semantic: PortSemantic::Signal,
50 polarity: PortPolarity::Bipolar,
51 }],
52 _ => vec![
53 TypedPort {
54 name: "in".into(),
55 direction: PortDirection::Input,
56 domain: ExecutionDomain::Sample,
57 data_type: DataType::Audio { channels: 2 },
58 semantic: PortSemantic::Signal,
59 polarity: PortPolarity::Bipolar,
60 },
61 TypedPort {
62 name: "out".into(),
63 direction: PortDirection::Output,
64 domain: ExecutionDomain::Sample,
65 data_type: DataType::Audio { channels: 2 },
66 semantic: PortSemantic::Signal,
67 polarity: PortPolarity::Bipolar,
68 },
69 ],
70 };
71 let mut config = BTreeMap::new();
72 config.insert("name".into(), ConfigValue::String(name));
73 Node {
74 id: StableId::new(),
75 kind,
76 ports,
77 config,
78 metadata: MetadataRef(None),
79 confidence: ConfidenceScore::Verified,
80 }
81 })
82 }
83
84 fn arb_config_value() -> impl Strategy<Value = ConfigValue> {
85 prop_oneof![
86 any::<f64>().prop_map(ConfigValue::Float),
87 any::<i64>().prop_map(ConfigValue::Int),
88 any::<bool>().prop_map(ConfigValue::Bool),
89 "[a-z]{1,10}".prop_map(ConfigValue::String),
90 ]
91 }
92
93 proptest! {
99 #[test]
100 fn constitution_replayability(
101 node_count in 1usize..8,
102 ) {
103 let rt = proptest::test_runner::TestRunner::new(Default::default());
105 let mut nodes = Vec::new();
106 for _ in 0..node_count {
107 nodes.push(Node::new_processor(&format!("node_{}", nodes.len())));
109 }
110
111 let patch = Patch::from_operations(
112 nodes.iter().map(|n| Operation::AddNode(n.clone())).collect()
113 );
114
115 let mut graph = Graph::new();
117 graph.apply(&patch).unwrap();
118
119 let replayed = Graph::replay(&[patch]).unwrap();
121
122 let h1 = hash::hash_graph(&graph);
124 let h2 = hash::hash_graph(&replayed);
125 prop_assert_eq!(h1, h2, "Replayability violation: graph hashes differ");
126 }
127 }
128
129 #[test]
135 fn constitution_hash_stability() {
136 let node = Node::new_processor("TestGain");
141 let ops = vec![Operation::AddNode(node.clone())];
142
143 let patch1 = Patch::from_operations(ops.clone());
144
145 let recomputed_hash = hash::hash_patch(&patch1);
147
148 assert_eq!(
149 patch1.deterministic_hash, recomputed_hash,
150 "Hash stability violation"
151 );
152 }
153
154 proptest! {
161 #[test]
162 fn constitution_explainability(
163 patch_count in 1usize..6,
164 ) {
165 let mut graph = Graph::new();
166 let mut all_patches = Vec::new();
167
168 for i in 0..patch_count {
169 let node = Node::new_processor(&format!("node_{}", i));
170 let patch = Patch::from_operations(vec![Operation::AddNode(node)]);
171 graph.apply(&patch).unwrap();
172 all_patches.push(patch);
173 }
174
175 prop_assert_eq!(
177 graph.applied_patches.len(),
178 all_patches.len(),
179 "Explainability violation: patch count mismatch"
180 );
181
182 for (applied_id, patch) in graph.applied_patches.iter().zip(all_patches.iter()) {
183 prop_assert_eq!(
184 applied_id, &patch.identity,
185 "Explainability violation: patch ID mismatch"
186 );
187 }
188
189 let replayed = Graph::replay(&all_patches).unwrap();
191 let h1 = hash::hash_graph(&graph);
192 let h2 = hash::hash_graph(&replayed);
193 prop_assert_eq!(h1, h2,
194 "Explainability violation: replay produces different state"
195 );
196 }
197 }
198
199 #[test]
204 fn constitution_modify_replay() {
205 let node = Node::new_processor("Gain");
206 let p1 = Patch::from_operations(vec![Operation::AddNode(node.clone())]);
207
208 let mut delta = BTreeMap::new();
209 delta.insert(
210 "gain_db".into(),
211 ConfigChange {
212 old: None,
213 new: Some(ConfigValue::Float(3.5)),
214 },
215 );
216 let p2 = Patch::from_operations(vec![Operation::ModifyConfig {
217 node_id: node.id,
218 delta,
219 }]);
220
221 let mut graph = Graph::new();
223 graph.apply(&p1).unwrap();
224 graph.apply(&p2).unwrap();
225
226 let replayed = Graph::replay(&[p1, p2]).unwrap();
228
229 assert_eq!(
230 hash::hash_graph(&graph),
231 hash::hash_graph(&replayed),
232 "Modify-replay round-trip failed"
233 );
234 }
235
236 #[test]
239 fn constitution_edge_replay() {
240 let src = Node::new_source("Sine");
241 let gain = Node::new_processor("Gain");
242 let sink = Node::new_sink("Output");
243
244 let edge1 = Edge::new(
245 PortRef {
246 node_id: src.id,
247 port_name: "out".into(),
248 },
249 PortRef {
250 node_id: gain.id,
251 port_name: "in".into(),
252 },
253 );
254 let edge2 = Edge::new(
255 PortRef {
256 node_id: gain.id,
257 port_name: "out".into(),
258 },
259 PortRef {
260 node_id: sink.id,
261 port_name: "in".into(),
262 },
263 );
264
265 let p1 = Patch::from_operations(vec![
266 Operation::AddNode(src),
267 Operation::AddNode(gain),
268 Operation::AddNode(sink),
269 ]);
270 let p2 = Patch::from_operations(vec![Operation::AddEdge(edge1), Operation::AddEdge(edge2)]);
271
272 let mut graph = Graph::new();
273 graph.apply(&p1).unwrap();
274 graph.apply(&p2).unwrap();
275
276 let replayed = Graph::replay(&[p1, p2]).unwrap();
277
278 assert_eq!(
279 hash::hash_graph(&graph),
280 hash::hash_graph(&replayed),
281 "Edge replay round-trip failed"
282 );
283 }
284}