1#[allow(unused_imports)]
34pub use evorule_reactor::{
35 compute_chain_hash, content_hash, fact_hash, fact_to_stable_json, HashError,
36};
37
38#[cfg(test)]
50mod tests {
51 #![allow(deprecated, clippy::unwrap_used, clippy::panic, clippy::expect_used)]
52 use super::*;
53 use evorule_reactor::{Fact, FactId, IoType};
54 use evorule_tcb::JsonValue;
55
56 #[test]
57 fn test_fact_to_stable_json_format() {
58 let command = Fact::Command {
59 id: FactId(1),
60 instruction: JsonValue::object_from_pairs(&[
61 ("type", JsonValue::string("increment")),
62 (
63 "params",
64 JsonValue::object_from_pairs(&[
65 ("attr", JsonValue::string("x")),
66 ("delta", JsonValue::Integer(5)),
67 ]),
68 ),
69 ]),
70 };
71
72 let json_value = fact_to_stable_json(&command).unwrap();
73
74 assert_eq!(json_value.get("type").unwrap().as_str().unwrap(), "Command");
75 assert_eq!(json_value.get("id").unwrap().as_u64().unwrap(), 1);
76 assert!(json_value.get("instruction").is_some());
77 }
78
79 #[test]
80 fn test_fact_hash_all_variants() {
81 let test_facts = vec![
82 Fact::Command {
83 id: FactId(1),
84 instruction: JsonValue::empty_object(),
85 },
86 Fact::PayloadUpdate {
87 id: FactId(2),
88 path: "test.path".into(),
89 value: JsonValue::string("test_value"),
90 },
91 Fact::StateTransition {
92 id: FactId(3),
93 cause: FactId(1),
94 new_payload: JsonValue::empty_object(),
95 new_queue: vec![],
96 },
97 Fact::IoRequest {
98 id: FactId(4),
99 cause: FactId(3),
100 io_type: IoType::http_get(),
101 params: JsonValue::empty_object(),
102 },
103 Fact::IoResponse {
104 id: FactId(5),
105 request_id: FactId(4),
106 result: JsonValue::string("response"),
107 error: None,
108 },
109 Fact::IoResponse {
110 id: FactId(6),
111 request_id: FactId(4),
112 result: JsonValue::Null,
113 error: Some("timeout".to_string()),
114 },
115 Fact::Stable {
116 id: FactId(7),
117 version: 1,
118 },
119 Fact::Error {
120 id: FactId(8),
121 message: "test error".into(),
122 },
123 ];
124
125 for fact in test_facts {
126 let hash = fact_hash(&fact).unwrap();
127 assert_eq!(hash.len(), 64);
128
129 let hash2 = fact_hash(&fact).unwrap();
130 assert_eq!(
131 hash,
132 hash2,
133 "fact_hash should be deterministic for {}",
134 fact.type_name()
135 );
136 }
137 }
138
139 #[test]
140 fn test_fact_hash_identity() {
141 let fact1 = Fact::Command {
142 id: FactId(1),
143 instruction: JsonValue::string("same"),
144 };
145 let fact2 = Fact::Command {
146 id: FactId(1),
147 instruction: JsonValue::string("same"),
148 };
149
150 assert_eq!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
151 }
152
153 #[test]
154 fn test_fact_hash_different_ids() {
155 let fact1 = Fact::Command {
156 id: FactId(1),
157 instruction: JsonValue::string("same"),
158 };
159 let fact2 = Fact::Command {
160 id: FactId(2),
161 instruction: JsonValue::string("same"),
162 };
163
164 assert_ne!(fact_hash(&fact1).unwrap(), fact_hash(&fact2).unwrap());
165 }
166
167 #[test]
173 fn test_cross_validate_with_tier1() {
174 let test_facts = [
175 Fact::Command {
176 id: FactId(1),
177 instruction: JsonValue::empty_object(),
178 },
179 Fact::PayloadUpdate {
180 id: FactId(2),
181 path: "test.path".into(),
182 value: JsonValue::string("test_value"),
183 },
184 Fact::StateTransition {
185 id: FactId(3),
186 cause: FactId(1),
187 new_payload: JsonValue::empty_object(),
188 new_queue: vec![],
189 },
190 Fact::IoRequest {
191 id: FactId(4),
192 cause: FactId(3),
193 io_type: IoType::http_get(),
194 params: JsonValue::empty_object(),
195 },
196 Fact::IoResponse {
197 id: FactId(5),
198 request_id: FactId(4),
199 result: JsonValue::string("response"),
200 error: None,
201 },
202 Fact::Stable {
203 id: FactId(6),
204 version: 1,
205 },
206 Fact::Error {
207 id: FactId(7),
208 message: "test error".into(),
209 },
210 ];
211
212 let cli_hashes: Vec<String> = test_facts.iter().map(|f| fact_hash(f).unwrap()).collect();
214
215 let tier1_hashes: Vec<String> = test_facts
217 .iter()
218 .map(|f| evorule_reactor::fact_hash(f).unwrap())
219 .collect();
220
221 assert_eq!(
222 cli_hashes, tier1_hashes,
223 "CLI re-export 的哈希与 tier1 直接计算的哈希不一致!\
224 这违反了两套 WAL 合并的单一真相源原则。"
225 );
226
227 let cli_chain = compute_chain_hash(&test_facts).unwrap();
229 let tier1_chain = evorule_reactor::compute_chain_hash(&test_facts).unwrap();
230 assert_eq!(
231 cli_chain, tier1_chain,
232 "CLI re-export 的链哈希与 tier1 直接计算的不一致!"
233 );
234 }
235
236 #[test]
237 fn test_hash_chain_stability() {
238 let facts = vec![
239 Fact::Command {
240 id: FactId(1),
241 instruction: JsonValue::empty_object(),
242 },
243 Fact::StateTransition {
244 id: FactId(2),
245 cause: FactId(1),
246 new_payload: JsonValue::empty_object(),
247 new_queue: vec![],
248 },
249 ];
250
251 let result1 = compute_chain_hash(&facts).unwrap();
253 let result2 = compute_chain_hash(&facts).unwrap();
254 assert_eq!(result1, result2);
255 }
256
257 #[test]
258 fn test_compute_chain_hash_empty() {
259 let facts: Vec<Fact> = vec![];
260 let chain_hash = compute_chain_hash(&facts).unwrap();
261 assert_eq!(chain_hash, "genesis");
262 }
263
264 #[test]
265 fn test_compute_chain_hash_order_sensitive() {
266 let fact1 = Fact::Command {
267 id: FactId(1),
268 instruction: JsonValue::empty_object(),
269 };
270 let fact2 = Fact::StateTransition {
271 id: FactId(2),
272 cause: FactId(1),
273 new_payload: JsonValue::empty_object(),
274 new_queue: vec![],
275 };
276
277 let chain1 = compute_chain_hash(&[fact1.clone(), fact2.clone()]).unwrap();
278 let chain2 = compute_chain_hash(&[fact2, fact1]).unwrap();
279 assert_ne!(chain1, chain2, "链哈希应对 Fact 顺序敏感");
280 }
281}