mockforge_core/reality_continuum/
engine.rs

1//! Reality Continuum Engine
2//!
3//! Manages blend ratios for gradually transitioning from mock to real data sources.
4//! Supports time-based progression, manual configuration, and per-route/group/global settings.
5
6use super::blender::ResponseBlender;
7use super::config::{ContinuumConfig, ContinuumRule, TransitionMode};
8use super::schedule::TimeSchedule;
9use chrono::{DateTime, Utc};
10use std::collections::HashMap;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13use tracing::{debug, info};
14
15/// Reality Continuum Engine
16///
17/// Manages blend ratios for routes, groups, and global settings.
18/// Calculates current blend ratios based on transition mode and time.
19#[derive(Debug, Clone)]
20pub struct RealityContinuumEngine {
21    /// Configuration
22    config: Arc<RwLock<ContinuumConfig>>,
23    /// Response blender
24    blender: ResponseBlender,
25    /// Optional virtual clock for time-based progression
26    virtual_clock: Option<Arc<crate::time_travel::VirtualClock>>,
27    /// Manual blend ratio overrides (path -> ratio)
28    manual_overrides: Arc<RwLock<HashMap<String, f64>>>,
29}
30
31impl RealityContinuumEngine {
32    /// Create a new continuum engine with the given configuration
33    pub fn new(config: ContinuumConfig) -> Self {
34        let blender = ResponseBlender::new(config.merge_strategy);
35        Self {
36            config: Arc::new(RwLock::new(config)),
37            blender,
38            virtual_clock: None,
39            manual_overrides: Arc::new(RwLock::new(HashMap::new())),
40        }
41    }
42
43    /// Create a new continuum engine with virtual clock integration
44    pub fn with_virtual_clock(
45        config: ContinuumConfig,
46        virtual_clock: Arc<crate::time_travel::VirtualClock>,
47    ) -> Self {
48        let blender = ResponseBlender::new(config.merge_strategy);
49        Self {
50            config: Arc::new(RwLock::new(config)),
51            blender,
52            virtual_clock: Some(virtual_clock),
53            manual_overrides: Arc::new(RwLock::new(HashMap::new())),
54        }
55    }
56
57    /// Set the virtual clock (can be called after construction)
58    pub fn set_virtual_clock(&mut self, virtual_clock: Arc<crate::time_travel::VirtualClock>) {
59        self.virtual_clock = Some(virtual_clock);
60    }
61
62    /// Get the current blend ratio for a path
63    ///
64    /// Checks in order:
65    /// 1. Manual overrides
66    /// 2. Route-specific rules
67    /// 3. Group-level overrides
68    /// 4. Time-based schedule (if enabled)
69    /// 5. Default ratio
70    pub async fn get_blend_ratio(&self, path: &str) -> f64 {
71        // Check manual overrides first
72        {
73            let overrides = self.manual_overrides.read().await;
74            if let Some(&ratio) = overrides.get(path) {
75                debug!("Using manual override for {}: {}", path, ratio);
76                return ratio;
77            }
78        }
79
80        let config = self.config.read().await;
81
82        // Check route-specific rules, but prioritize group-level overrides
83        for rule in &config.routes {
84            if rule.matches_path(path) {
85                // If this route has a group and the group has a ratio, use the group ratio
86                if let Some(ref group) = rule.group {
87                    if let Some(&group_ratio) = config.groups.get(group) {
88                        debug!(
89                            "Using group override for {} (group {}): {}",
90                            path, group, group_ratio
91                        );
92                        return group_ratio;
93                    }
94                }
95                // Otherwise, use the route ratio
96                debug!("Using route rule for {}: {}", path, rule.ratio);
97                return rule.ratio;
98            }
99        }
100
101        // Check time-based schedule if enabled
102        if config.transition_mode == TransitionMode::TimeBased
103            || config.transition_mode == TransitionMode::Scheduled
104        {
105            if let Some(ref schedule) = config.time_schedule {
106                let current_time = self.get_current_time().await;
107                let ratio = schedule.calculate_ratio(current_time);
108                debug!("Using time-based ratio for {}: {} (time: {})", path, ratio, current_time);
109                return ratio;
110            }
111        }
112
113        // Return default ratio
114        debug!("Using default ratio for {}: {}", path, config.default_ratio);
115        config.default_ratio
116    }
117
118    /// Set a manual blend ratio override for a path
119    pub async fn set_blend_ratio(&self, path: &str, ratio: f64) {
120        let ratio = ratio.clamp(0.0, 1.0);
121        let mut overrides = self.manual_overrides.write().await;
122        overrides.insert(path.to_string(), ratio);
123        info!("Set manual blend ratio for {}: {}", path, ratio);
124    }
125
126    /// Remove a manual blend ratio override
127    pub async fn remove_blend_ratio(&self, path: &str) {
128        let mut overrides = self.manual_overrides.write().await;
129        if overrides.remove(path).is_some() {
130            info!("Removed manual blend ratio override for {}", path);
131        }
132    }
133
134    /// Set blend ratio for a group
135    pub async fn set_group_ratio(&self, group: &str, ratio: f64) {
136        let ratio = ratio.clamp(0.0, 1.0);
137        let mut config = self.config.write().await;
138        config.groups.insert(group.to_string(), ratio);
139        info!("Set group blend ratio for {}: {}", group, ratio);
140    }
141
142    /// Update blend ratios based on current time
143    ///
144    /// This should be called periodically when using time-based progression.
145    pub async fn update_from_time(&self, _time: DateTime<Utc>) {
146        // The blend ratio calculation happens on-demand in get_blend_ratio,
147        // so this method is mainly for logging/observability purposes
148        debug!("Continuum engine updated from time: {}", _time);
149    }
150
151    /// Get the response blender
152    pub fn blender(&self) -> &ResponseBlender {
153        &self.blender
154    }
155
156    /// Get the current configuration
157    pub async fn get_config(&self) -> ContinuumConfig {
158        self.config.read().await.clone()
159    }
160
161    /// Update the configuration
162    pub async fn update_config(&self, config: ContinuumConfig) {
163        let mut current_config = self.config.write().await;
164        *current_config = config;
165        info!("Continuum configuration updated");
166    }
167
168    /// Check if continuum is enabled
169    pub async fn is_enabled(&self) -> bool {
170        self.config.read().await.enabled
171    }
172
173    /// Enable or disable continuum
174    pub async fn set_enabled(&self, enabled: bool) {
175        let mut config = self.config.write().await;
176        config.enabled = enabled;
177        if enabled {
178            info!("Reality Continuum enabled");
179        } else {
180            info!("Reality Continuum disabled");
181        }
182    }
183
184    /// Get the time schedule
185    pub async fn get_time_schedule(&self) -> Option<TimeSchedule> {
186        self.config.read().await.time_schedule.clone()
187    }
188
189    /// Update the time schedule
190    pub async fn set_time_schedule(&self, schedule: TimeSchedule) {
191        let mut config = self.config.write().await;
192        config.time_schedule = Some(schedule);
193        config.transition_mode = TransitionMode::TimeBased;
194        info!("Time schedule updated");
195    }
196
197    /// Get current time (virtual or real)
198    async fn get_current_time(&self) -> DateTime<Utc> {
199        if let Some(ref clock) = self.virtual_clock {
200            clock.now()
201        } else {
202            Utc::now()
203        }
204    }
205
206    /// Advance the blend ratio manually (for testing/debugging)
207    ///
208    /// This increments the default ratio by a small amount.
209    pub async fn advance_ratio(&self, increment: f64) {
210        let mut config = self.config.write().await;
211        let new_ratio = (config.default_ratio + increment).clamp(0.0, 1.0);
212        config.default_ratio = new_ratio;
213        info!("Advanced default blend ratio to {}", new_ratio);
214    }
215
216    /// Get all manual overrides
217    pub async fn get_manual_overrides(&self) -> HashMap<String, f64> {
218        self.manual_overrides.read().await.clone()
219    }
220
221    /// Clear all manual overrides
222    pub async fn clear_manual_overrides(&self) {
223        let mut overrides = self.manual_overrides.write().await;
224        overrides.clear();
225        info!("Cleared all manual blend ratio overrides");
226    }
227}
228
229impl Default for RealityContinuumEngine {
230    fn default() -> Self {
231        Self::new(ContinuumConfig::default())
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238
239    #[tokio::test]
240    async fn test_get_blend_ratio_default() {
241        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
242        let ratio = engine.get_blend_ratio("/api/test").await;
243        assert_eq!(ratio, 0.0); // Default is 0.0 (100% mock)
244    }
245
246    #[tokio::test]
247    async fn test_set_get_blend_ratio() {
248        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
249        engine.set_blend_ratio("/api/test", 0.75).await;
250        let ratio = engine.get_blend_ratio("/api/test").await;
251        assert_eq!(ratio, 0.75);
252    }
253
254    #[tokio::test]
255    async fn test_route_rule_matching() {
256        let mut config = ContinuumConfig::default();
257        config.routes.push(ContinuumRule::new("/api/users/*".to_string(), 0.5));
258        let engine = RealityContinuumEngine::new(config);
259
260        let ratio = engine.get_blend_ratio("/api/users/123").await;
261        assert_eq!(ratio, 0.5);
262    }
263
264    #[tokio::test]
265    async fn test_group_ratio() {
266        let mut config = ContinuumConfig::default();
267        config.groups.insert("api-v1".to_string(), 0.3);
268        config.routes.push(
269            ContinuumRule::new("/api/users/*".to_string(), 0.5).with_group("api-v1".to_string()),
270        );
271        let engine = RealityContinuumEngine::new(config);
272
273        // Group ratio should override route ratio
274        let ratio = engine.get_blend_ratio("/api/users/123").await;
275        assert_eq!(ratio, 0.3);
276    }
277
278    #[tokio::test]
279    async fn test_time_based_ratio() {
280        let start = Utc::now();
281        let end = start + chrono::Duration::days(30);
282        let schedule = TimeSchedule::new(start, end, 0.0, 1.0);
283
284        let mut config = ContinuumConfig::default();
285        config.transition_mode = TransitionMode::TimeBased;
286        config.time_schedule = Some(schedule);
287        let engine = RealityContinuumEngine::new(config);
288
289        // At start time, should return start_ratio
290        let ratio = engine.get_blend_ratio("/api/test").await;
291        // Should be close to 0.0 (start_ratio)
292        assert!(ratio < 0.1);
293    }
294
295    #[tokio::test]
296    async fn test_remove_blend_ratio() {
297        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
298        engine.set_blend_ratio("/api/test", 0.75).await;
299        assert_eq!(engine.get_blend_ratio("/api/test").await, 0.75);
300
301        engine.remove_blend_ratio("/api/test").await;
302        assert_eq!(engine.get_blend_ratio("/api/test").await, 0.0); // Back to default
303    }
304
305    #[tokio::test]
306    async fn test_group_ratio_override() {
307        let mut config = ContinuumConfig::default();
308        config.groups.insert("api-v1".to_string(), 0.8);
309        config.routes.push(
310            ContinuumRule::new("/api/users/*".to_string(), 0.5).with_group("api-v1".to_string()),
311        );
312        let engine = RealityContinuumEngine::new(config);
313
314        // Group ratio should override route ratio
315        let ratio = engine.get_blend_ratio("/api/users/123").await;
316        assert_eq!(ratio, 0.8);
317    }
318
319    #[tokio::test]
320    async fn test_enable_disable() {
321        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
322        assert!(!engine.is_enabled().await);
323
324        engine.set_enabled(true).await;
325        assert!(engine.is_enabled().await);
326
327        engine.set_enabled(false).await;
328        assert!(!engine.is_enabled().await);
329    }
330
331    #[tokio::test]
332    async fn test_advance_ratio() {
333        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
334        assert_eq!(engine.get_config().await.default_ratio, 0.0);
335
336        engine.advance_ratio(0.2).await;
337        assert_eq!(engine.get_config().await.default_ratio, 0.2);
338
339        engine.advance_ratio(0.5).await;
340        assert_eq!(engine.get_config().await.default_ratio, 0.7);
341
342        // Should clamp at 1.0
343        engine.advance_ratio(0.5).await;
344        assert_eq!(engine.get_config().await.default_ratio, 1.0);
345    }
346
347    #[tokio::test]
348    async fn test_clear_manual_overrides() {
349        let engine = RealityContinuumEngine::new(ContinuumConfig::default());
350        engine.set_blend_ratio("/api/test1", 0.5).await;
351        engine.set_blend_ratio("/api/test2", 0.7).await;
352
353        let overrides = engine.get_manual_overrides().await;
354        assert_eq!(overrides.len(), 2);
355
356        engine.clear_manual_overrides().await;
357        let overrides = engine.get_manual_overrides().await;
358        assert_eq!(overrides.len(), 0);
359    }
360}