Skip to main content

briefcase_core/replay/
sync.rs

1//! Synchronous replay engine implementation
2//!
3//! This module provides a synchronous variant of the replay engine
4//! for use cases that don't require async functionality.
5
6use super::{
7    Comparator, PolicyViolation, ReplayError, ReplayMode, ReplayPolicy, ReplayResult, ReplayStats,
8    ReplayStatus,
9};
10use crate::models::DecisionSnapshot;
11use crate::storage::sync::SyncStorageBackend;
12use std::time::Instant;
13
14/// Synchronous replay engine
15pub struct SyncReplayEngine<S: SyncStorageBackend> {
16    storage: S,
17    default_mode: ReplayMode,
18}
19
20impl<S: SyncStorageBackend> SyncReplayEngine<S> {
21    /// Create a new synchronous replay engine
22    pub fn new(storage: S) -> Self {
23        Self {
24            storage,
25            default_mode: ReplayMode::Tolerant,
26        }
27    }
28
29    /// Create a replay engine with a specific default mode
30    pub fn with_mode(storage: S, mode: ReplayMode) -> Self {
31        Self {
32            storage,
33            default_mode: mode,
34        }
35    }
36
37    /// Get the default replay mode
38    pub fn default_mode(&self) -> ReplayMode {
39        self.default_mode.clone()
40    }
41
42    /// Replay a snapshot by ID
43    pub fn replay(
44        &self,
45        snapshot_id: &str,
46        mode: Option<ReplayMode>,
47        _context_overrides: Option<serde_json::Value>,
48    ) -> Result<ReplayResult, ReplayError> {
49        let start_time = Instant::now();
50        let replay_mode = mode.unwrap_or_else(|| self.default_mode());
51
52        let original_snapshot = self
53            .storage
54            .load_decision(snapshot_id)
55            .map_err(|e| ReplayError::StorageError(e.to_string()))?;
56
57        match replay_mode {
58            ReplayMode::ValidationOnly => {
59                // Just validate without re-executing
60                Ok(ReplayResult {
61                    status: ReplayStatus::Success,
62                    original_snapshot,
63                    replay_output: None,
64                    outputs_match: true, // Assume match for validation-only
65                    diff: None,
66                    policy_violations: Vec::new(),
67                    execution_time_ms: start_time.elapsed().as_millis() as f64,
68                })
69            }
70            ReplayMode::Strict | ReplayMode::Tolerant => {
71                // For sync implementation, we'll simulate replay
72                // In a real implementation, this would re-execute the decision
73                let simulated_output = simulate_execution(&original_snapshot);
74                let outputs_match =
75                    compare_outputs(&original_snapshot, &simulated_output, &replay_mode);
76
77                Ok(ReplayResult {
78                    status: if outputs_match {
79                        ReplayStatus::Success
80                    } else {
81                        ReplayStatus::Failed
82                    },
83                    original_snapshot,
84                    replay_output: Some(simulated_output),
85                    outputs_match,
86                    diff: None, // TODO: Implement diff calculation
87                    policy_violations: Vec::new(),
88                    execution_time_ms: start_time.elapsed().as_millis() as f64,
89                })
90            }
91        }
92    }
93
94    /// Replay with policy validation
95    pub fn replay_with_policy(
96        &self,
97        snapshot_id: &str,
98        policy: &ReplayPolicy,
99        mode: Option<ReplayMode>,
100    ) -> Result<ReplayResult, ReplayError> {
101        let mut result = self.replay(snapshot_id, mode, None)?;
102
103        // Validate against policy
104        let violations = self.validate(snapshot_id, policy)?;
105        result.policy_violations = violations;
106
107        if !result.policy_violations.is_empty() {
108            result.status = ReplayStatus::Failed;
109        }
110
111        Ok(result)
112    }
113
114    /// Validate a snapshot against a policy
115    pub fn validate(
116        &self,
117        snapshot_id: &str,
118        policy: &ReplayPolicy,
119    ) -> Result<Vec<PolicyViolation>, ReplayError> {
120        let snapshot = self
121            .storage
122            .load_decision(snapshot_id)
123            .map_err(|e| ReplayError::StorageError(e.to_string()))?;
124
125        let mut violations = Vec::new();
126
127        // Apply policy rules
128        for rule in &policy.rules {
129            match rule.comparator {
130                Comparator::ExactMatch => {
131                    // For exact match, we'd need to compare with expected values
132                    // This is a simplified implementation
133                    if rule.field == "function_name" {
134                        // Example validation logic
135                        if snapshot.function_name.is_empty() {
136                            violations.push(PolicyViolation {
137                                rule_name: format!("exact_match_{}", rule.field),
138                                field: rule.field.clone(),
139                                expected: "non-empty function name".to_string(),
140                                actual: "empty".to_string(),
141                                message: "Function name cannot be empty".to_string(),
142                            });
143                        }
144                    }
145                }
146                Comparator::SemanticSimilarity => {
147                    // For similarity threshold, we'd need reference values
148                    // This is a simplified implementation
149                    if rule.field == "output" && snapshot.outputs.is_empty() {
150                        violations.push(PolicyViolation {
151                            rule_name: format!("similarity_{}", rule.field),
152                            field: rule.field.clone(),
153                            expected: "at least one output".to_string(),
154                            actual: "no outputs".to_string(),
155                            message: "At least one output is required".to_string(),
156                        });
157                    }
158                }
159                Comparator::MaxIncreasePercent => {
160                    // Placeholder for cost/performance checks
161                    // Would need historical data for comparison
162                }
163                Comparator::MaxDecreasePercent => {
164                    // Placeholder for cost/performance checks
165                    // Would need historical data for comparison
166                }
167                Comparator::WithinRange => {
168                    // Placeholder for range checks
169                    // Would need expected ranges for comparison
170                }
171            }
172        }
173
174        Ok(violations)
175    }
176
177    /// Get replay statistics for a list of snapshots
178    pub fn get_replay_stats(&self, snapshot_ids: &[String]) -> Result<ReplayStats, ReplayError> {
179        let mut total_replays = 0;
180        let mut successful_replays = 0;
181        let mut failed_replays = 0;
182        let mut exact_matches = 0;
183        let mut mismatches = 0;
184        let mut total_execution_time_ms = 0.0;
185
186        for snapshot_id in snapshot_ids {
187            match self.replay(snapshot_id, None, None) {
188                Ok(result) => {
189                    total_replays += 1;
190                    total_execution_time_ms += result.execution_time_ms;
191
192                    match result.status {
193                        ReplayStatus::Success => {
194                            successful_replays += 1;
195                            if result.outputs_match {
196                                exact_matches += 1;
197                            } else {
198                                mismatches += 1;
199                            }
200                        }
201                        _ => {
202                            failed_replays += 1;
203                            mismatches += 1;
204                        }
205                    }
206                }
207                Err(_) => {
208                    total_replays += 1;
209                    failed_replays += 1;
210                    mismatches += 1;
211                }
212            }
213        }
214
215        let average_execution_time_ms = if total_replays > 0 {
216            total_execution_time_ms / total_replays as f64
217        } else {
218            0.0
219        };
220
221        Ok(ReplayStats {
222            total_replays,
223            successful_replays,
224            failed_replays,
225            exact_matches,
226            mismatches,
227            average_execution_time_ms,
228            total_execution_time_ms,
229        })
230    }
231}
232
233impl<S: SyncStorageBackend> Clone for SyncReplayEngine<S>
234where
235    S: Clone,
236{
237    fn clone(&self) -> Self {
238        Self {
239            storage: self.storage.clone(),
240            default_mode: self.default_mode.clone(),
241        }
242    }
243}
244
245/// Simulate execution for demo purposes
246/// In a real implementation, this would re-execute the decision
247fn simulate_execution(decision: &DecisionSnapshot) -> serde_json::Value {
248    // For now, just return the first output if available
249    if let Some(output) = decision.outputs.first() {
250        output.value.clone()
251    } else {
252        serde_json::Value::Null
253    }
254}
255
256/// Compare outputs based on replay mode
257fn compare_outputs(
258    decision: &DecisionSnapshot,
259    simulated_output: &serde_json::Value,
260    mode: &ReplayMode,
261) -> bool {
262    if let Some(original_output) = decision.outputs.first() {
263        match mode {
264            ReplayMode::Strict => {
265                // Exact match required
266                original_output.value == *simulated_output
267            }
268            ReplayMode::Tolerant => {
269                // Allow some differences (simplified implementation)
270                if original_output.value == *simulated_output {
271                    true
272                } else {
273                    // Could implement fuzzy matching here
274                    false
275                }
276            }
277            ReplayMode::ValidationOnly => {
278                // Always true for validation-only mode
279                true
280            }
281        }
282    } else {
283        simulated_output.is_null()
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290    use crate::models::*;
291    use crate::storage::sync::MemoryStorageBackend;
292    use serde_json::json;
293
294    fn create_test_decision() -> DecisionSnapshot {
295        let input = Input::new("test_input", json!("value"), "string");
296        let output = Output::new("test_output", json!("result"), "string");
297        let model_params = ModelParameters::new("gpt-4");
298
299        DecisionSnapshot::new("test_function")
300            .with_module("test_module")
301            .add_input(input)
302            .add_output(output)
303            .with_model_parameters(model_params)
304            .add_tag("env", "test")
305    }
306
307    #[test]
308    fn test_sync_replay_validation_only() {
309        let storage = MemoryStorageBackend::new();
310        let engine = SyncReplayEngine::new(storage);
311
312        let decision = create_test_decision();
313        let decision_id = engine.storage.save_decision(&decision).unwrap();
314
315        let result = engine
316            .replay(&decision_id, Some(ReplayMode::ValidationOnly), None)
317            .unwrap();
318
319        assert_eq!(result.status, ReplayStatus::Success);
320        assert!(result.outputs_match);
321        assert!(result.replay_output.is_none());
322    }
323
324    #[test]
325    fn test_sync_replay_tolerant_mode() {
326        let storage = MemoryStorageBackend::new();
327        let engine = SyncReplayEngine::new(storage);
328
329        let decision = create_test_decision();
330        let decision_id = engine.storage.save_decision(&decision).unwrap();
331
332        let result = engine
333            .replay(&decision_id, Some(ReplayMode::Tolerant), None)
334            .unwrap();
335
336        assert_eq!(result.status, ReplayStatus::Success);
337        assert!(result.replay_output.is_some());
338    }
339
340    #[test]
341    fn test_sync_replay_stats() {
342        let storage = MemoryStorageBackend::new();
343        let engine = SyncReplayEngine::new(storage);
344
345        let decision1 = create_test_decision();
346        let decision2 = create_test_decision();
347
348        let id1 = engine.storage.save_decision(&decision1).unwrap();
349        let id2 = engine.storage.save_decision(&decision2).unwrap();
350
351        let stats = engine.get_replay_stats(&[id1, id2]).unwrap();
352
353        assert_eq!(stats.total_replays, 2);
354        assert!(stats.total_execution_time_ms >= 0.0);
355        assert!(stats.average_execution_time_ms >= 0.0);
356    }
357
358    #[test]
359    fn test_sync_replay_policy_validation() {
360        let storage = MemoryStorageBackend::new();
361        let engine = SyncReplayEngine::new(storage);
362
363        let policy = ReplayPolicy::new("test_policy".to_string())
364            .with_exact_match("function_name".to_string());
365
366        let decision = create_test_decision();
367        let decision_id = engine.storage.save_decision(&decision).unwrap();
368
369        let violations = engine.validate(&decision_id, &policy).unwrap();
370
371        // Should pass validation since function name is not empty
372        assert!(violations.is_empty());
373    }
374}