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