cellstate_pipeline/pack/
flow.rs

1//! Flow definition and execution receipts
2//!
3//! Re-export path: cellstate_pipeline::pack::flow::*
4
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// Flow definition from ```flow fence blocks
9#[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], // SHA-256(id + tool + inputs)
25}
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        // Sort inputs for determinism
36        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/// Compiled flow ready for execution
56#[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/// Flow execution receipt (Feedback #6 - audit trail)
75#[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
114// Hex array serde helper module
115mod 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    /// Helper to create a FlowStep with given parameters
147    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], // Placeholder, will be computed
158        }
159    }
160
161    /// Helper to create a FlowStepReceipt with given parameters
162    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    /// Helper to create a FlowReceipt with given parameters
179    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    // ==================== FlowStep::compute_hash() Tests ====================
197
198    #[test]
199    fn test_flow_step_compute_hash_deterministic() {
200        // Calling compute_hash multiple times on the same FlowStep should yield identical results
201        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        // Two FlowSteps with identical content should produce the same hash
214        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        // Different step IDs should produce different hashes
229        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        // Different tools should produce different hashes
244        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        // Different input values should produce different hashes
259        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        // Different input keys should produce different hashes
274        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        // Hash should be the same regardless of insertion order (inputs are sorted)
289        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        // Create step2 with inputs in different order
300        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        // Empty inputs should still produce a valid, deterministic hash
325        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        // Many inputs should still produce a deterministic hash
341        let inputs: Vec<(&str, &str)> = (0..100)
342            .map(|i| {
343                // We need to leak these strings to get &str with 'static lifetime
344                // In tests this is acceptable
345                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        // Unicode content should be handled correctly
365        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        // Special characters should be handled correctly
388        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        // SHA256 should produce exactly 32 bytes
411        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    // ==================== FlowReceipt Serialization Tests ====================
418
419    #[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        // Serialize to JSON
425        let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
426
427        // Deserialize back
428        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        // Serialize to JSON
447        let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
448
449        // Deserialize back
450        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        // Serialize to JSON
475        let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
476
477        // Deserialize back
478        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        // The hash should be serialized as lowercase hex
502        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        // Test that we can deserialize from a JSON with hex-encoded hash
512        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        // Invalid hex should fail deserialization
536        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        // Hash with wrong length should fail deserialization
553        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    // ==================== FlowStepReceipt Hash Verification Tests ====================
571
572    #[test]
573    fn test_flow_step_receipt_hash_matches_step() {
574        // Create a FlowStep and compute its hash
575        let step = make_flow_step("step1", "file_read", vec![("path", "/data/input.csv")]);
576        let computed_hash = step.compute_hash();
577
578        // Create a FlowStepReceipt with the same step_hash
579        let receipt = make_step_receipt("step1", computed_hash, [0u8; 32]);
580
581        // Verify the hashes match
582        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        // Create a FlowStep and compute its hash
591        let step = make_flow_step("step1", "file_read", vec![("path", "/data/input.csv")]);
592        let computed_hash = step.compute_hash();
593
594        // Create a FlowStepReceipt with a DIFFERENT step_hash (simulating tampering)
595        let tampered_hash = [0xFFu8; 32];
596        let receipt = make_step_receipt("step1", tampered_hash, [0u8; 32]);
597
598        // Verify the hashes don't match
599        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        // Output hash should be preserved correctly
608        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        // Serialize
621        let json = serde_json::to_string(&receipt).expect("Serialization should succeed");
622
623        // Deserialize
624        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    // ==================== CompiledFlow Hash Tests ====================
633
634    #[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        // Serialize
652        let json = serde_json::to_string(&compiled_flow).expect("Serialization should succeed");
653
654        // Deserialize
655        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], // Different hash
679            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    // ==================== Integration-style Tests ====================
690
691    #[test]
692    fn test_end_to_end_flow_hash_verification() {
693        // Simulate a complete flow execution with hash verification
694
695        // 1. Define flow steps
696        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        // 2. Compute hashes for each step
705        let hash1 = step1.compute_hash();
706        let hash2 = step2.compute_hash();
707        let hash3 = step3.compute_hash();
708
709        // 3. Compute a flow hash (in practice, this might be a hash of all step hashes)
710        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        // 4. Create receipts for executed steps
718        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        // 5. Create the flow receipt
725        let flow_receipt = make_flow_receipt("data_pipeline", flow_hash, receipts);
726
727        // 6. Serialize and deserialize (simulating storage/retrieval)
728        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        // 7. Verify all hashes match
733        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        // 8. Verify by recomputing
739        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        // Create a legitimate flow step and receipt
751        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        // Simulate an attacker trying to claim they ran with different inputs
756        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        // The hashes should not match, detecting the tampering
764        assert_ne!(
765            receipt.step_hash, tampered_hash,
766            "Tampering should be detectable via hash mismatch"
767        );
768
769        // The legitimate hash should still match
770        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        // Test all FlowStatus variants
779        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            // Check we got the same variant back (using debug string as proxy since no PartialEq)
792            assert_eq!(format!("{:?}", status), format!("{:?}", restored));
793        }
794    }
795
796    #[test]
797    fn test_step_status_serialization() {
798        // Test all StepStatus variants
799        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}