1use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct FlowDefinition {
11 pub name: String,
12 pub description: Option<String>,
13 pub steps: Vec<FlowStep>,
14 pub on_error: FlowErrorHandler,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct FlowStep {
19 pub id: String,
20 pub tool: String,
21 pub inputs: HashMap<String, String>,
22 pub outputs: Vec<String>,
23 #[serde(with = "hex_array")]
24 pub content_hash: [u8; 32], }
26
27impl FlowStep {
28 pub fn compute_hash(&self) -> [u8; 32] {
29 use sha2::{Digest, Sha256};
30
31 let mut hasher = Sha256::new();
32 hasher.update(self.id.as_bytes());
33 hasher.update(self.tool.as_bytes());
34
35 let mut sorted_inputs: Vec<_> = self.inputs.iter().collect();
37 sorted_inputs.sort_by_key(|(k, _)| *k);
38 for (key, value) in sorted_inputs {
39 hasher.update(key.as_bytes());
40 hasher.update(value.as_bytes());
41 }
42
43 hasher.finalize().into()
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub enum FlowErrorHandler {
49 Retry { max_attempts: u32 },
50 SkipToNext,
51 Abort,
52 Fallback { step_id: String },
53}
54
55#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct CompiledFlow {
58 pub name: String,
59 #[serde(with = "hex_array")]
60 pub flow_hash: [u8; 32],
61 pub steps: Vec<CompiledFlowStep>,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct CompiledFlowStep {
66 pub id: String,
67 pub tool: String,
68 pub inputs: HashMap<String, String>,
69 pub outputs: Vec<String>,
70 #[serde(with = "hex_array")]
71 pub content_hash: [u8; 32],
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct FlowReceipt {
77 pub receipt_id: uuid::Uuid,
78 pub flow_name: String,
79 #[serde(with = "hex_array")]
80 pub flow_hash: [u8; 32],
81 pub steps_executed: Vec<FlowStepReceipt>,
82 pub started_at: chrono::DateTime<chrono::Utc>,
83 pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
84 pub final_status: FlowStatus,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct FlowStepReceipt {
89 pub step_id: String,
90 #[serde(with = "hex_array")]
91 pub step_hash: [u8; 32],
92 pub started_at: chrono::DateTime<chrono::Utc>,
93 pub completed_at: chrono::DateTime<chrono::Utc>,
94 pub status: StepStatus,
95 #[serde(with = "hex_array")]
96 pub output_hash: [u8; 32],
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub enum FlowStatus {
101 Running,
102 Completed,
103 Failed,
104 Cancelled,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub enum StepStatus {
109 Success,
110 Failed,
111 Skipped,
112}
113
114mod hex_array {
116 use serde::{Deserialize, Deserializer, Serialize, Serializer};
117
118 pub fn serialize<S>(bytes: &[u8; 32], serializer: S) -> Result<S::Ok, S::Error>
119 where
120 S: Serializer,
121 {
122 hex::encode(bytes).serialize(serializer)
123 }
124
125 pub fn deserialize<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error>
126 where
127 D: Deserializer<'de>,
128 {
129 let s = String::deserialize(deserializer)?;
130 let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
131 if bytes.len() != 32 {
132 return Err(serde::de::Error::custom("expected 32 bytes"));
133 }
134 let mut array = [0u8; 32];
135 array.copy_from_slice(&bytes);
136 Ok(array)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use chrono::Utc;
144 use std::collections::HashMap;
145
146 fn make_flow_step(id: &str, tool: &str, inputs: Vec<(&str, &str)>) -> FlowStep {
148 let inputs_map: HashMap<String, String> = inputs
149 .into_iter()
150 .map(|(k, v)| (k.to_string(), v.to_string()))
151 .collect();
152 FlowStep {
153 id: id.to_string(),
154 tool: tool.to_string(),
155 inputs: inputs_map,
156 outputs: vec!["result".to_string()],
157 content_hash: [0u8; 32], }
159 }
160
161 fn make_step_receipt(
163 step_id: &str,
164 step_hash: [u8; 32],
165 output_hash: [u8; 32],
166 ) -> FlowStepReceipt {
167 let now = Utc::now();
168 FlowStepReceipt {
169 step_id: step_id.to_string(),
170 step_hash,
171 started_at: now,
172 completed_at: now,
173 status: StepStatus::Success,
174 output_hash,
175 }
176 }
177
178 fn make_flow_receipt(
180 flow_name: &str,
181 flow_hash: [u8; 32],
182 steps: Vec<FlowStepReceipt>,
183 ) -> FlowReceipt {
184 let now = Utc::now();
185 FlowReceipt {
186 receipt_id: uuid::Uuid::new_v4(),
187 flow_name: flow_name.to_string(),
188 flow_hash,
189 steps_executed: steps,
190 started_at: now,
191 completed_at: Some(now),
192 final_status: FlowStatus::Completed,
193 }
194 }
195
196 #[test]
199 fn test_flow_step_compute_hash_deterministic() {
200 let step = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
202
203 let hash1 = step.compute_hash();
204 let hash2 = step.compute_hash();
205 let hash3 = step.compute_hash();
206
207 assert_eq!(hash1, hash2, "Hash should be deterministic across calls");
208 assert_eq!(hash2, hash3, "Hash should be deterministic across calls");
209 }
210
211 #[test]
212 fn test_flow_step_compute_hash_same_inputs_same_hash() {
213 let step1 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
215 let step2 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
216
217 let hash1 = step1.compute_hash();
218 let hash2 = step2.compute_hash();
219
220 assert_eq!(
221 hash1, hash2,
222 "Identical FlowSteps should produce identical hashes"
223 );
224 }
225
226 #[test]
227 fn test_flow_step_compute_hash_different_id_different_hash() {
228 let step1 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
230 let step2 = make_flow_step("step2", "file_read", vec![("path", "/tmp/test.txt")]);
231
232 let hash1 = step1.compute_hash();
233 let hash2 = step2.compute_hash();
234
235 assert_ne!(
236 hash1, hash2,
237 "Different IDs should produce different hashes"
238 );
239 }
240
241 #[test]
242 fn test_flow_step_compute_hash_different_tool_different_hash() {
243 let step1 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
245 let step2 = make_flow_step("step1", "file_write", vec![("path", "/tmp/test.txt")]);
246
247 let hash1 = step1.compute_hash();
248 let hash2 = step2.compute_hash();
249
250 assert_ne!(
251 hash1, hash2,
252 "Different tools should produce different hashes"
253 );
254 }
255
256 #[test]
257 fn test_flow_step_compute_hash_different_input_values_different_hash() {
258 let step1 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test1.txt")]);
260 let step2 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test2.txt")]);
261
262 let hash1 = step1.compute_hash();
263 let hash2 = step2.compute_hash();
264
265 assert_ne!(
266 hash1, hash2,
267 "Different input values should produce different hashes"
268 );
269 }
270
271 #[test]
272 fn test_flow_step_compute_hash_different_input_keys_different_hash() {
273 let step1 = make_flow_step("step1", "file_read", vec![("path", "/tmp/test.txt")]);
275 let step2 = make_flow_step("step1", "file_read", vec![("file", "/tmp/test.txt")]);
276
277 let hash1 = step1.compute_hash();
278 let hash2 = step2.compute_hash();
279
280 assert_ne!(
281 hash1, hash2,
282 "Different input keys should produce different hashes"
283 );
284 }
285
286 #[test]
287 fn test_flow_step_compute_hash_input_order_independent() {
288 let step1 = make_flow_step(
290 "step1",
291 "http_request",
292 vec![
293 ("url", "http://example.com"),
294 ("method", "GET"),
295 ("timeout", "30"),
296 ],
297 );
298
299 let mut inputs2 = HashMap::new();
301 inputs2.insert("timeout".to_string(), "30".to_string());
302 inputs2.insert("url".to_string(), "http://example.com".to_string());
303 inputs2.insert("method".to_string(), "GET".to_string());
304
305 let step2 = FlowStep {
306 id: "step1".to_string(),
307 tool: "http_request".to_string(),
308 inputs: inputs2,
309 outputs: vec!["result".to_string()],
310 content_hash: [0u8; 32],
311 };
312
313 let hash1 = step1.compute_hash();
314 let hash2 = step2.compute_hash();
315
316 assert_eq!(
317 hash1, hash2,
318 "Input order should not affect hash (inputs are sorted)"
319 );
320 }
321
322 #[test]
323 fn test_flow_step_compute_hash_empty_inputs() {
324 let step1 = make_flow_step("step1", "noop", vec![]);
326 let step2 = make_flow_step("step1", "noop", vec![]);
327
328 let hash1 = step1.compute_hash();
329 let hash2 = step2.compute_hash();
330
331 assert_eq!(
332 hash1, hash2,
333 "Empty inputs should produce deterministic hash"
334 );
335 assert_ne!(hash1, [0u8; 32], "Hash should not be all zeros");
336 }
337
338 #[test]
339 fn test_flow_step_compute_hash_many_inputs() {
340 let inputs: Vec<(&str, &str)> = (0..100)
342 .map(|i| {
343 let key = Box::leak(format!("key{}", i).into_boxed_str());
346 let value = Box::leak(format!("value{}", i).into_boxed_str());
347 (key as &str, value as &str)
348 })
349 .collect();
350
351 let step = make_flow_step("big_step", "complex_tool", inputs);
352
353 let hash1 = step.compute_hash();
354 let hash2 = step.compute_hash();
355
356 assert_eq!(
357 hash1, hash2,
358 "Many inputs should produce deterministic hash"
359 );
360 }
361
362 #[test]
363 fn test_flow_step_compute_hash_unicode_content() {
364 let step1 = make_flow_step(
366 "step1",
367 "echo",
368 vec![("message", "Hello, \u{4e16}\u{754c}!")],
369 );
370 let step2 = make_flow_step(
371 "step1",
372 "echo",
373 vec![("message", "Hello, \u{4e16}\u{754c}!")],
374 );
375
376 let hash1 = step1.compute_hash();
377 let hash2 = step2.compute_hash();
378
379 assert_eq!(
380 hash1, hash2,
381 "Unicode content should produce deterministic hash"
382 );
383 }
384
385 #[test]
386 fn test_flow_step_compute_hash_special_characters() {
387 let step1 = make_flow_step(
389 "step1",
390 "shell",
391 vec![("cmd", "echo 'hello\\nworld' | grep -E '^[a-z]+'")],
392 );
393 let step2 = make_flow_step(
394 "step1",
395 "shell",
396 vec![("cmd", "echo 'hello\\nworld' | grep -E '^[a-z]+'")],
397 );
398
399 let hash1 = step1.compute_hash();
400 let hash2 = step2.compute_hash();
401
402 assert_eq!(
403 hash1, hash2,
404 "Special characters should produce deterministic hash"
405 );
406 }
407
408 #[test]
409 fn test_flow_step_compute_hash_length_is_32_bytes() {
410 let step = make_flow_step("step1", "test", vec![]);
412 let hash = step.compute_hash();
413
414 assert_eq!(hash.len(), 32, "SHA256 hash should be exactly 32 bytes");
415 }
416
417 #[test]
420 fn test_flow_receipt_serialization_preserves_flow_hash() {
421 let flow_hash = [0xABu8; 32];
422 let receipt = make_flow_receipt("test_flow", flow_hash, vec![]);
423
424 let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
426
427 let deserialized: FlowReceipt =
429 serde_json::from_str(&json).expect("Deserialization should succeed");
430
431 assert_eq!(
432 deserialized.flow_hash, flow_hash,
433 "flow_hash should be preserved through serialization"
434 );
435 }
436
437 #[test]
438 fn test_flow_receipt_serialization_preserves_step_hashes() {
439 let flow_hash = [0x11u8; 32];
440 let step_hash = [0x22u8; 32];
441 let output_hash = [0x33u8; 32];
442
443 let step_receipt = make_step_receipt("step1", step_hash, output_hash);
444 let receipt = make_flow_receipt("test_flow", flow_hash, vec![step_receipt]);
445
446 let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
448
449 let deserialized: FlowReceipt =
451 serde_json::from_str(&json).expect("Deserialization should succeed");
452
453 assert_eq!(deserialized.steps_executed.len(), 1);
454 assert_eq!(
455 deserialized.steps_executed[0].step_hash, step_hash,
456 "step_hash should be preserved through serialization"
457 );
458 assert_eq!(
459 deserialized.steps_executed[0].output_hash, output_hash,
460 "output_hash should be preserved through serialization"
461 );
462 }
463
464 #[test]
465 fn test_flow_receipt_serialization_multiple_steps() {
466 let flow_hash = [0x00u8; 32];
467 let steps = vec![
468 make_step_receipt("step1", [0x11u8; 32], [0x1Au8; 32]),
469 make_step_receipt("step2", [0x22u8; 32], [0x2Bu8; 32]),
470 make_step_receipt("step3", [0x33u8; 32], [0x3Cu8; 32]),
471 ];
472 let receipt = make_flow_receipt("multi_step_flow", flow_hash, steps);
473
474 let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
476
477 let deserialized: FlowReceipt =
479 serde_json::from_str(&json).expect("Deserialization should succeed");
480
481 assert_eq!(deserialized.steps_executed.len(), 3);
482 assert_eq!(deserialized.steps_executed[0].step_hash, [0x11u8; 32]);
483 assert_eq!(deserialized.steps_executed[1].step_hash, [0x22u8; 32]);
484 assert_eq!(deserialized.steps_executed[2].step_hash, [0x33u8; 32]);
485 assert_eq!(deserialized.steps_executed[0].output_hash, [0x1Au8; 32]);
486 assert_eq!(deserialized.steps_executed[1].output_hash, [0x2Bu8; 32]);
487 assert_eq!(deserialized.steps_executed[2].output_hash, [0x3Cu8; 32]);
488 }
489
490 #[test]
491 fn test_flow_receipt_json_hash_format_is_hex() {
492 let flow_hash = [
493 0xAB, 0xCD, 0xEF, 0x01, 0x23, 0x45, 0x67, 0x89, 0x00, 0x11, 0x22, 0x33, 0x44, 0x55,
494 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x12, 0x34, 0x56, 0x78,
495 0x9A, 0xBC, 0xDE, 0xF0,
496 ];
497 let receipt = make_flow_receipt("test_flow", flow_hash, vec![]);
498
499 let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
500
501 let expected_hex = "abcdef012345678900112233445566778899aabbccddeeff123456789abcdef0";
503 assert!(
504 json.contains(expected_hex),
505 "JSON should contain hex-encoded flow_hash"
506 );
507 }
508
509 #[test]
510 fn test_flow_receipt_deserialization_from_hex_string() {
511 let json = r#"{
513 "receipt_id": "550e8400-e29b-41d4-a716-446655440000",
514 "flow_name": "test_flow",
515 "flow_hash": "0102030405060708091011121314151617181920212223242526272829303132",
516 "steps_executed": [],
517 "started_at": "2024-01-01T00:00:00Z",
518 "completed_at": "2024-01-01T00:01:00Z",
519 "final_status": "Completed"
520 }"#;
521
522 let receipt: FlowReceipt =
523 serde_json::from_str(json).expect("Should deserialize from hex hash");
524
525 let expected_hash: [u8; 32] = [
526 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14,
527 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
528 0x29, 0x30, 0x31, 0x32,
529 ];
530 assert_eq!(receipt.flow_hash, expected_hash);
531 }
532
533 #[test]
534 fn test_flow_receipt_deserialization_invalid_hex_fails() {
535 let json = r#"{
537 "receipt_id": "550e8400-e29b-41d4-a716-446655440000",
538 "flow_name": "test_flow",
539 "flow_hash": "not_valid_hex_string_at_all_xyz",
540 "steps_executed": [],
541 "started_at": "2024-01-01T00:00:00Z",
542 "completed_at": "2024-01-01T00:01:00Z",
543 "final_status": "Completed"
544 }"#;
545
546 let result: Result<FlowReceipt, _> = serde_json::from_str(json);
547 assert!(result.is_err(), "Invalid hex should fail deserialization");
548 }
549
550 #[test]
551 fn test_flow_receipt_deserialization_wrong_hash_length_fails() {
552 let json = r#"{
554 "receipt_id": "550e8400-e29b-41d4-a716-446655440000",
555 "flow_name": "test_flow",
556 "flow_hash": "0102030405",
557 "steps_executed": [],
558 "started_at": "2024-01-01T00:00:00Z",
559 "completed_at": "2024-01-01T00:01:00Z",
560 "final_status": "Completed"
561 }"#;
562
563 let result: Result<FlowReceipt, _> = serde_json::from_str(json);
564 assert!(
565 result.is_err(),
566 "Hash with wrong length should fail deserialization"
567 );
568 }
569
570 #[test]
573 fn test_flow_step_receipt_hash_matches_step() {
574 let step = make_flow_step("step1", "file_read", vec![("path", "/data/input.csv")]);
576 let computed_hash = step.compute_hash();
577
578 let receipt = make_step_receipt("step1", computed_hash, [0u8; 32]);
580
581 assert_eq!(
583 receipt.step_hash, computed_hash,
584 "FlowStepReceipt should contain the correct step_hash"
585 );
586 }
587
588 #[test]
589 fn test_flow_step_receipt_verification_detects_mismatch() {
590 let step = make_flow_step("step1", "file_read", vec![("path", "/data/input.csv")]);
592 let computed_hash = step.compute_hash();
593
594 let tampered_hash = [0xFFu8; 32];
596 let receipt = make_step_receipt("step1", tampered_hash, [0u8; 32]);
597
598 assert_ne!(
600 receipt.step_hash, computed_hash,
601 "Tampered hash should not match computed hash"
602 );
603 }
604
605 #[test]
606 fn test_flow_step_receipt_output_hash_integrity() {
607 let output_hash = [0x42u8; 32];
609 let receipt = make_step_receipt("step1", [0u8; 32], output_hash);
610
611 assert_eq!(receipt.output_hash, output_hash);
612 }
613
614 #[test]
615 fn test_flow_step_receipt_serialization_roundtrip() {
616 let step_hash = [0xAAu8; 32];
617 let output_hash = [0xBBu8; 32];
618 let receipt = make_step_receipt("step1", step_hash, output_hash);
619
620 let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
622
623 let deserialized: FlowStepReceipt =
625 serde_json::from_str(&json).expect("Deserialization should succeed");
626
627 assert_eq!(deserialized.step_id, receipt.step_id);
628 assert_eq!(deserialized.step_hash, step_hash);
629 assert_eq!(deserialized.output_hash, output_hash);
630 }
631
632 #[test]
635 fn test_compiled_flow_hash_serialization() {
636 let flow_hash = [0x12u8; 32];
637 let step_hash = [0x34u8; 32];
638
639 let compiled_flow = CompiledFlow {
640 name: "test_flow".to_string(),
641 flow_hash,
642 steps: vec![CompiledFlowStep {
643 id: "step1".to_string(),
644 tool: "test_tool".to_string(),
645 inputs: HashMap::new(),
646 outputs: vec![],
647 content_hash: step_hash,
648 }],
649 };
650
651 let json = serde_json::to_string(&compiled_flow).expect("Serialization should succeed");
653
654 let deserialized: CompiledFlow =
656 serde_json::from_str(&json).expect("Deserialization should succeed");
657
658 assert_eq!(deserialized.flow_hash, flow_hash);
659 assert_eq!(deserialized.steps[0].content_hash, step_hash);
660 }
661
662 #[test]
663 fn test_compiled_flow_equality_includes_hashes() {
664 let flow1 = CompiledFlow {
665 name: "test".to_string(),
666 flow_hash: [0x11u8; 32],
667 steps: vec![],
668 };
669
670 let flow2 = CompiledFlow {
671 name: "test".to_string(),
672 flow_hash: [0x11u8; 32],
673 steps: vec![],
674 };
675
676 let flow3 = CompiledFlow {
677 name: "test".to_string(),
678 flow_hash: [0x22u8; 32], steps: vec![],
680 };
681
682 assert_eq!(flow1, flow2, "Flows with same hash should be equal");
683 assert_ne!(
684 flow1, flow3,
685 "Flows with different hash should not be equal"
686 );
687 }
688
689 #[test]
692 fn test_end_to_end_flow_hash_verification() {
693 let step1 = make_flow_step("read_input", "file_read", vec![("path", "/data/input.csv")]);
697 let step2 = make_flow_step("process", "transform", vec![("operation", "uppercase")]);
698 let step3 = make_flow_step(
699 "write_output",
700 "file_write",
701 vec![("path", "/data/output.csv")],
702 );
703
704 let hash1 = step1.compute_hash();
706 let hash2 = step2.compute_hash();
707 let hash3 = step3.compute_hash();
708
709 use sha2::{Digest, Sha256};
711 let mut flow_hasher = Sha256::new();
712 flow_hasher.update(hash1);
713 flow_hasher.update(hash2);
714 flow_hasher.update(hash3);
715 let flow_hash: [u8; 32] = flow_hasher.finalize().into();
716
717 let receipts = vec![
719 make_step_receipt("read_input", hash1, [0x01u8; 32]),
720 make_step_receipt("process", hash2, [0x02u8; 32]),
721 make_step_receipt("write_output", hash3, [0x03u8; 32]),
722 ];
723
724 let flow_receipt = make_flow_receipt("data_pipeline", flow_hash, receipts);
726
727 let json = serde_json::to_string(&flow_receipt).expect("Serialization should succeed");
729 let restored: FlowReceipt =
730 serde_json::from_str(&json).expect("Deserialization should succeed");
731
732 assert_eq!(restored.flow_hash, flow_hash);
734 assert_eq!(restored.steps_executed[0].step_hash, hash1);
735 assert_eq!(restored.steps_executed[1].step_hash, hash2);
736 assert_eq!(restored.steps_executed[2].step_hash, hash3);
737
738 let recomputed1 = step1.compute_hash();
740 let recomputed2 = step2.compute_hash();
741 let recomputed3 = step3.compute_hash();
742
743 assert_eq!(restored.steps_executed[0].step_hash, recomputed1);
744 assert_eq!(restored.steps_executed[1].step_hash, recomputed2);
745 assert_eq!(restored.steps_executed[2].step_hash, recomputed3);
746 }
747
748 #[test]
749 fn test_tamper_detection() {
750 let step = make_flow_step("critical_step", "bank_transfer", vec![("amount", "100")]);
752 let legitimate_hash = step.compute_hash();
753 let receipt = make_step_receipt("critical_step", legitimate_hash, [0u8; 32]);
754
755 let tampered_step = make_flow_step(
757 "critical_step",
758 "bank_transfer",
759 vec![("amount", "1000000")],
760 );
761 let tampered_hash = tampered_step.compute_hash();
762
763 assert_ne!(
765 receipt.step_hash, tampered_hash,
766 "Tampering should be detectable via hash mismatch"
767 );
768
769 assert_eq!(
771 receipt.step_hash, legitimate_hash,
772 "Legitimate hash should still verify"
773 );
774 }
775
776 #[test]
777 fn test_flow_status_serialization() {
778 let statuses = vec![
780 FlowStatus::Running,
781 FlowStatus::Completed,
782 FlowStatus::Failed,
783 FlowStatus::Cancelled,
784 ];
785
786 for status in statuses {
787 let json = serde_json::to_string(&status).expect("Status serialization should succeed");
788 let restored: FlowStatus =
789 serde_json::from_str(&json).expect("Status deserialization should succeed");
790
791 assert_eq!(format!("{:?}", status), format!("{:?}", restored));
793 }
794 }
795
796 #[test]
797 fn test_step_status_serialization() {
798 let statuses = vec![StepStatus::Success, StepStatus::Failed, StepStatus::Skipped];
800
801 for status in statuses {
802 let json = serde_json::to_string(&status).expect("Status serialization should succeed");
803 let restored: StepStatus =
804 serde_json::from_str(&json).expect("Status deserialization should succeed");
805
806 assert_eq!(format!("{:?}", status), format!("{:?}", restored));
807 }
808 }
809}