Skip to main content

mockforge_data/
drift_learning.rs

1//! Drift Learning System
2//!
3//! This module extends the DataDriftEngine with learning capabilities that allow
4//! mocks to gradually learn from recorded traffic and adapt their behavior.
5//!
6//! Features:
7//! - Traffic pattern learning from recorded requests
8//! - Persona behavior adaptation based on request patterns
9//! - Configurable learning rate and sensitivity
10//! - Opt-in per persona/endpoint learning
11
12use crate::drift::{DataDriftConfig, DataDriftEngine};
13use crate::Result;
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16use std::collections::HashMap;
17use std::sync::Arc;
18use std::time::Duration;
19use tokio::sync::RwLock;
20
21/// Learning configuration
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct LearningConfig {
24    /// Enable drift learning
25    #[serde(default)]
26    pub enabled: bool,
27
28    /// Learning mode
29    #[serde(default)]
30    pub mode: LearningMode,
31
32    /// Learning rate (0.0 to 1.0) - how quickly mocks learn from patterns
33    #[serde(default = "default_learning_rate")]
34    pub sensitivity: f64,
35
36    /// Decay rate (0.0 to 1.0) - drift resets if upstream patterns reverse
37    #[serde(default = "default_decay_rate")]
38    pub decay: f64,
39
40    /// Minimum number of samples before learning starts
41    #[serde(default = "default_min_samples")]
42    pub min_samples: usize,
43
44    /// Update interval for learning
45    #[serde(default = "default_update_interval")]
46    pub update_interval: Duration,
47
48    /// Enable persona adaptation
49    #[serde(default = "default_true")]
50    pub persona_adaptation: bool,
51
52    /// Enable traffic pattern mirroring
53    #[serde(default = "default_true")]
54    pub traffic_mirroring: bool,
55
56    /// Per-endpoint opt-in learning (endpoint pattern -> enabled)
57    #[serde(default)]
58    pub endpoint_learning: HashMap<String, bool>,
59
60    /// Per-persona opt-in learning (persona_id -> enabled)
61    #[serde(default)]
62    pub persona_learning: HashMap<String, bool>,
63}
64
65fn default_learning_rate() -> f64 {
66    0.2 // 20% learning rate - conservative default
67}
68
69fn default_decay_rate() -> f64 {
70    0.05 // 5% decay rate
71}
72
73fn default_min_samples() -> usize {
74    10 // Need at least 10 samples before learning
75}
76
77fn default_update_interval() -> Duration {
78    Duration::from_secs(60) // Update every minute
79}
80
81fn default_true() -> bool {
82    true
83}
84
85impl Default for LearningConfig {
86    fn default() -> Self {
87        Self {
88            enabled: false, // Opt-in by default
89            mode: LearningMode::Behavioral,
90            sensitivity: default_learning_rate(),
91            decay: default_decay_rate(),
92            min_samples: default_min_samples(),
93            update_interval: default_update_interval(),
94            persona_adaptation: true,
95            traffic_mirroring: true,
96            endpoint_learning: HashMap::new(),
97            persona_learning: HashMap::new(),
98        }
99    }
100}
101
102/// Learning mode
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum LearningMode {
106    /// Behavioral learning - adapts to behavior patterns
107    #[default]
108    Behavioral,
109    /// Statistical learning - adapts to statistical patterns
110    Statistical,
111    /// Hybrid - combines behavioral and statistical
112    Hybrid,
113}
114
115/// Drift Learning Engine
116///
117/// Extends DataDriftEngine with learning capabilities from recorded traffic.
118#[allow(dead_code)]
119pub struct DriftLearningEngine {
120    /// Base drift engine
121    drift_engine: DataDriftEngine,
122    /// Learning configuration
123    learning_config: LearningConfig,
124    /// Traffic pattern learner
125    traffic_learner: Option<Arc<RwLock<TrafficPatternLearner>>>,
126    /// Persona behavior learner
127    persona_learner: Option<Arc<RwLock<PersonaBehaviorLearner>>>,
128    /// Learned patterns cache
129    learned_patterns: Arc<RwLock<HashMap<String, LearnedPattern>>>,
130}
131
132/// Learned pattern from traffic analysis
133#[derive(Debug, Clone)]
134pub struct LearnedPattern {
135    /// Pattern identifier
136    pub pattern_id: String,
137    /// Pattern type
138    pub pattern_type: PatternType,
139    /// Learned parameters
140    pub parameters: HashMap<String, Value>,
141    /// Confidence score (0.0 to 1.0)
142    pub confidence: f64,
143    /// Sample count used for learning
144    pub sample_count: usize,
145    /// Last updated timestamp
146    pub last_updated: chrono::DateTime<chrono::Utc>,
147}
148
149/// Pattern type
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub enum PatternType {
152    /// Latency pattern
153    Latency,
154    /// Error rate pattern
155    ErrorRate,
156    /// Request sequence pattern
157    RequestSequence,
158    /// Persona behavior pattern
159    PersonaBehavior,
160}
161
162impl DriftLearningEngine {
163    /// Create a new drift learning engine
164    pub fn new(drift_config: DataDriftConfig, learning_config: LearningConfig) -> Result<Self> {
165        let drift_engine = DataDriftEngine::new(drift_config)?;
166
167        let traffic_learner = if learning_config.traffic_mirroring {
168            Some(Arc::new(RwLock::new(TrafficPatternLearner::new(learning_config.clone())?)))
169        } else {
170            None
171        };
172
173        let persona_learner = if learning_config.persona_adaptation {
174            Some(Arc::new(RwLock::new(PersonaBehaviorLearner::new(learning_config.clone())?)))
175        } else {
176            None
177        };
178
179        Ok(Self {
180            drift_engine,
181            learning_config,
182            traffic_learner,
183            persona_learner,
184            learned_patterns: Arc::new(RwLock::new(HashMap::new())),
185        })
186    }
187
188    /// Get the base drift engine
189    pub fn drift_engine(&self) -> &DataDriftEngine {
190        &self.drift_engine
191    }
192
193    /// Get learning configuration
194    pub fn learning_config(&self) -> &LearningConfig {
195        &self.learning_config
196    }
197
198    /// Update learning configuration
199    pub fn update_learning_config(&mut self, config: LearningConfig) -> Result<()> {
200        self.learning_config = config;
201        Ok(())
202    }
203
204    /// Get learned patterns
205    pub async fn get_learned_patterns(&self) -> HashMap<String, LearnedPattern> {
206        self.learned_patterns.read().await.clone()
207    }
208
209    /// Apply drift with learning
210    pub async fn apply_drift_with_learning(&self, data: Value) -> Result<Value> {
211        // First apply base drift
212        let mut data = self.drift_engine.apply_drift(data).await?;
213
214        // Then apply learned patterns if learning is enabled
215        if !self.learning_config.enabled {
216            return Ok(data);
217        }
218
219        // Apply learned patterns
220        let patterns = self.learned_patterns.read().await;
221        for (_pattern_id, pattern) in patterns.iter() {
222            // Check if pattern should be applied based on confidence and decay
223            if pattern.confidence < 0.5 {
224                continue; // Low confidence, skip
225            }
226
227            // Apply pattern based on type
228            match pattern.pattern_type {
229                PatternType::Latency => {
230                    // Latency patterns are handled separately
231                }
232                PatternType::ErrorRate => {
233                    // Error rate patterns are handled separately
234                }
235                PatternType::RequestSequence => {
236                    // Request sequence patterns affect persona behavior
237                }
238                PatternType::PersonaBehavior => {
239                    // Persona behavior patterns affect data generation
240                    if let Some(obj) = data.as_object_mut() {
241                        for (key, value) in &pattern.parameters {
242                            if let Some(existing) = obj.get(key) {
243                                // Blend learned value with existing value
244                                let blended =
245                                    self.blend_values(existing, value, pattern.confidence)?;
246                                obj.insert(key.clone(), blended);
247                            }
248                        }
249                    }
250                }
251            }
252        }
253
254        Ok(data)
255    }
256
257    /// Blend two values based on confidence
258    fn blend_values(&self, existing: &Value, learned: &Value, confidence: f64) -> Result<Value> {
259        // Simple blending: existing * (1 - confidence * sensitivity) + learned * (confidence * sensitivity)
260        let weight = confidence * self.learning_config.sensitivity;
261
262        match (existing, learned) {
263            (Value::Number(existing_num), Value::Number(learned_num)) => {
264                if let (Some(existing_f64), Some(learned_f64)) =
265                    (existing_num.as_f64(), learned_num.as_f64())
266                {
267                    let blended = existing_f64 * (1.0 - weight) + learned_f64 * weight;
268                    Ok(Value::from(blended))
269                } else {
270                    Ok(existing.clone())
271                }
272            }
273            _ => Ok(existing.clone()), // For non-numeric, keep existing
274        }
275    }
276}
277
278/// Traffic Pattern Learner
279///
280/// Analyzes recorded traffic to detect patterns and trends.
281#[allow(dead_code)]
282pub struct TrafficPatternLearner {
283    /// Learning configuration
284    config: LearningConfig,
285    /// Pattern window for analysis
286    pattern_window: Duration,
287    /// Detected patterns
288    patterns: HashMap<String, TrafficPattern>,
289}
290
291/// Traffic pattern detected from analysis
292#[derive(Debug, Clone)]
293#[allow(dead_code)]
294struct TrafficPattern {
295    /// Pattern identifier
296    pattern_id: String,
297    /// Pattern type
298    pattern_type: PatternType,
299    /// Pattern parameters
300    parameters: HashMap<String, Value>,
301    /// Sample count
302    sample_count: usize,
303    /// First seen timestamp
304    first_seen: chrono::DateTime<chrono::Utc>,
305    /// Last seen timestamp
306    last_seen: chrono::DateTime<chrono::Utc>,
307}
308
309impl TrafficPatternLearner {
310    /// Create a new traffic pattern learner
311    pub fn new(config: LearningConfig) -> Result<Self> {
312        Ok(Self {
313            config,
314            pattern_window: Duration::from_secs(3600), // 1 hour window
315            patterns: HashMap::new(),
316        })
317    }
318
319    /// Analyze traffic patterns from recorded requests
320    ///
321    /// NOTE: This method is disabled to break circular dependencies.
322    /// The recorder integration has been moved to a higher-level crate.
323    pub async fn analyze_traffic_patterns(
324        &mut self,
325        _database: &dyn std::any::Any, // Use Any to avoid dependency on mockforge-recorder
326    ) -> Result<Vec<LearnedPattern>> {
327        // Disabled to break circular dependency
328        Ok(Vec::new())
329    }
330
331    /// Internal method to detect latency patterns from requests
332    ///
333    /// NOTE: This method is disabled to break circular dependency.
334    /// The recorder integration has been moved to a higher-level crate.
335    #[allow(dead_code)]
336    pub async fn detect_latency_patterns_from_requests(
337        &self,
338        _requests: &[Value],
339    ) -> Result<Vec<LearnedPattern>> {
340        // Disabled to break circular dependency
341        Ok(Vec::new())
342    }
343
344    // NOTE: The following code is disabled to break circular dependency
345    // Original implementation would process requests here
346    /*
347    fn _detect_latency_patterns_original(
348        &self,
349        requests: &[serde_json::Value],
350    ) -> Result<Vec<LearnedPattern>> {
351        use std::collections::HashMap;
352        use chrono::Utc;
353
354        // Group requests by endpoint/method
355        let mut endpoint_latencies: HashMap<String, Vec<i64>> = HashMap::new();
356
357        for request in requests {
358            if let Some(duration) = request.duration_ms {
359                let key = format!("{} {}", request.method, request.path);
360                endpoint_latencies.entry(key).or_insert_with(Vec::new).push(duration);
361            }
362        }
363
364        let mut patterns = Vec::new();
365
366        for (endpoint_key, latencies) in endpoint_latencies {
367            if latencies.len() < 10 {
368                // Need at least 10 samples for meaningful analysis
369                continue;
370            }
371
372            // Calculate statistics
373            let sum: i64 = latencies.iter().sum();
374            let count = latencies.len();
375            let avg_latency = sum as f64 / count as f64;
376
377            // Calculate percentiles
378            let mut sorted = latencies.clone();
379            sorted.sort();
380            let p50 = sorted[sorted.len() / 2];
381            let p95 = sorted[(sorted.len() * 95) / 100];
382            let p99 = sorted[(sorted.len() * 99) / 100];
383
384            // Detect if latency is increasing (trend analysis)
385            let recent_avg = if latencies.len() >= 20 {
386                let recent: Vec<i64> = latencies.iter().rev().take(10).copied().collect();
387                let recent_sum: i64 = recent.iter().sum();
388                recent_sum as f64 / recent.len() as f64
389            } else {
390                avg_latency
391            };
392
393            let latency_trend = if recent_avg > avg_latency * 1.2 {
394                "increasing"
395            } else if recent_avg < avg_latency * 0.8 {
396                "decreasing"
397            } else {
398                "stable"
399            };
400
401            // Create pattern if there's significant variation or trend
402            if p99 > p50 * 2.0 || latency_trend != "stable" {
403                let mut parameters = HashMap::new();
404                parameters.insert("endpoint".to_string(), serde_json::json!(endpoint_key));
405                parameters.insert("avg_latency_ms".to_string(), serde_json::json!(avg_latency));
406                parameters.insert("p50_ms".to_string(), serde_json::json!(p50));
407                parameters.insert("p95_ms".to_string(), serde_json::json!(p95));
408                parameters.insert("p99_ms".to_string(), serde_json::json!(p99));
409                parameters.insert("sample_count".to_string(), serde_json::json!(count));
410                parameters.insert("trend".to_string(), serde_json::json!(latency_trend));
411
412                // Confidence based on sample size
413                let confidence = (count as f64 / 100.0).min(1.0);
414
415                patterns.push(LearnedPattern {
416                    pattern_id: format!("latency_{}", endpoint_key.replace('/', "_").replace(' ', "_")),
417                    pattern_type: PatternType::Latency,
418                    parameters,
419                    confidence,
420                    sample_count: count,
421                    last_updated: Utc::now(),
422                });
423            }
424        }
425
426        Ok(patterns)
427    }
428    */
429
430    /// Internal method to detect error rate patterns from requests
431    /// NOTE: Disabled to break circular dependency
432    #[allow(dead_code)]
433    async fn detect_error_patterns_internal(
434        &self,
435        _requests: &[Value],
436    ) -> Result<Vec<LearnedPattern>> {
437        use chrono::Utc;
438        use std::collections::HashMap;
439
440        // Disabled to break circular dependency
441        let endpoint_errors: HashMap<String, (usize, usize)> = HashMap::new(); // (total, errors)
442
443        // Disabled - would iterate over requests here
444        /*
445        for request in requests {
446            let key = format!("{} {}", request.method, request.path);
447            let entry = endpoint_errors.entry(key).or_insert((0, 0));
448            entry.0 += 1;
449
450            // Consider 4xx and 5xx as errors
451            if let Some(status) = request.status_code {
452                if status >= 400 {
453                    entry.1 += 1;
454                }
455            }
456        }
457        */
458
459        let mut patterns = Vec::new();
460
461        for (endpoint_key, (total, errors)) in endpoint_errors {
462            if total < 20 {
463                // Need at least 20 samples for meaningful analysis
464                continue;
465            }
466
467            let error_rate = errors as f64 / total as f64;
468
469            // Create pattern if error rate is significant (>5%) or increasing
470            if error_rate > 0.05 {
471                let mut parameters = HashMap::new();
472                parameters.insert("endpoint".to_string(), serde_json::json!(endpoint_key));
473                parameters.insert("error_rate".to_string(), serde_json::json!(error_rate));
474                parameters.insert("total_requests".to_string(), serde_json::json!(total));
475                parameters.insert("error_count".to_string(), serde_json::json!(errors));
476
477                // Confidence based on sample size and error rate
478                let confidence = ((total as f64 / 100.0).min(1.0) * error_rate * 10.0).min(1.0);
479
480                patterns.push(LearnedPattern {
481                    pattern_id: format!("error_rate_{}", endpoint_key.replace(['/', ' '], "_")),
482                    pattern_type: PatternType::ErrorRate,
483                    parameters,
484                    confidence,
485                    sample_count: total,
486                    last_updated: Utc::now(),
487                });
488            }
489        }
490
491        Ok(patterns)
492    }
493
494    /// Internal method to detect request sequence patterns
495    /// NOTE: Disabled to break circular dependency
496    #[allow(dead_code)]
497    async fn detect_sequence_patterns_internal(
498        &self,
499        _requests: &[Value],
500    ) -> Result<Vec<LearnedPattern>> {
501        use chrono::Utc;
502        use std::collections::HashMap;
503
504        // Disabled to break circular dependency
505        if _requests.len() < 50 {
506            // Need sufficient data for sequence detection
507            return Ok(Vec::new());
508        }
509
510        // Disabled - would process requests here
511        let trace_sequences: HashMap<Option<String>, Vec<String>> = HashMap::new();
512
513        /*
514        for request in requests {
515            let trace_id = request.trace_id.clone();
516            let endpoint_key = format!("{} {}", request.method, request.path);
517            trace_sequences
518                .entry(trace_id)
519                .or_insert_with(Vec::new)
520                .push(endpoint_key);
521        }
522        */
523
524        // Find common sequences (patterns that appear multiple times)
525        let mut sequence_counts: HashMap<String, usize> = HashMap::new();
526
527        for sequence in trace_sequences.values() {
528            if sequence.len() >= 2 {
529                // Create sequence signature (first 3 endpoints)
530                let signature: Vec<String> = sequence.iter().take(3).cloned().collect();
531                let signature_str = signature.join(" -> ");
532                *sequence_counts.entry(signature_str).or_insert(0) += 1;
533            }
534        }
535
536        let mut patterns = Vec::new();
537
538        for (sequence_str, count) in sequence_counts {
539            if count >= 5 {
540                // Pattern appears at least 5 times
541                let mut parameters = HashMap::new();
542                parameters.insert("sequence".to_string(), serde_json::json!(sequence_str));
543                parameters.insert("occurrence_count".to_string(), serde_json::json!(count));
544
545                // Confidence based on occurrence frequency
546                let confidence = (count as f64 / 20.0).min(1.0);
547
548                patterns.push(LearnedPattern {
549                    pattern_id: format!(
550                        "sequence_{}",
551                        sequence_str.replace(['/', ' '], "_").replace("->", "_")
552                    ),
553                    pattern_type: PatternType::RequestSequence,
554                    parameters,
555                    confidence,
556                    sample_count: count,
557                    last_updated: Utc::now(),
558                });
559            }
560        }
561
562        Ok(patterns)
563    }
564
565    /// Detect latency patterns
566    ///
567    /// This method is a convenience wrapper that requires a database.
568    /// Use `analyze_traffic_patterns` with a RecorderDatabase for full analysis.
569    pub async fn detect_latency_patterns(&mut self) -> Result<Vec<LearnedPattern>> {
570        // This method now requires database access - use analyze_traffic_patterns instead
571        Ok(Vec::new())
572    }
573
574    /// Detect error rate patterns
575    ///
576    /// This method is a convenience wrapper that requires a database.
577    /// Use `analyze_traffic_patterns` with a RecorderDatabase for full analysis.
578    pub async fn detect_error_patterns(&mut self) -> Result<Vec<LearnedPattern>> {
579        // This method now requires database access - use analyze_traffic_patterns instead
580        Ok(Vec::new())
581    }
582}
583
584/// Persona Behavior Learner
585///
586/// Adapts persona profiles based on request patterns.
587pub struct PersonaBehaviorLearner {
588    /// Learning configuration
589    config: LearningConfig,
590    /// Behavior history (persona_id -> behavior events)
591    behavior_history: HashMap<String, Vec<BehaviorEvent>>,
592}
593
594/// Behavior event for a persona
595#[derive(Debug, Clone)]
596pub struct BehaviorEvent {
597    /// Event timestamp
598    pub timestamp: chrono::DateTime<chrono::Utc>,
599    /// Event type
600    pub event_type: BehaviorEventType,
601    /// Event data
602    pub data: HashMap<String, Value>,
603}
604
605/// Behavior event type
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub enum BehaviorEventType {
608    /// Request made to an endpoint
609    Request {
610        /// Endpoint path
611        endpoint: String,
612        /// HTTP method
613        method: String,
614    },
615    /// Request failed
616    RequestFailed {
617        /// Endpoint path
618        endpoint: String,
619        /// HTTP status code
620        status_code: u16,
621    },
622    /// Request succeeded after failure
623    RequestSucceededAfterFailure {
624        /// Endpoint path
625        endpoint: String,
626    },
627    /// Pattern detected
628    PatternDetected {
629        /// Pattern identifier
630        pattern: String,
631    },
632}
633
634impl PersonaBehaviorLearner {
635    /// Create a new persona behavior learner
636    pub fn new(config: LearningConfig) -> Result<Self> {
637        Ok(Self {
638            config,
639            behavior_history: HashMap::new(),
640        })
641    }
642
643    /// Record a behavior event for a persona
644    pub fn record_event(&mut self, persona_id: String, event: BehaviorEvent) {
645        if !self.config.enabled {
646            return;
647        }
648
649        // Check if persona learning is enabled for this persona
650        if let Some(&enabled) = self.config.persona_learning.get(&persona_id) {
651            if !enabled {
652                return; // Learning disabled for this persona
653            }
654        }
655
656        let events = self.behavior_history.entry(persona_id).or_default();
657        events.push(event);
658
659        // Keep only recent events (last 1000)
660        if events.len() > 1000 {
661            events.remove(0);
662        }
663    }
664
665    /// Analyze behavior patterns for a persona
666    pub async fn analyze_persona_behavior(
667        &self,
668        persona_id: &str,
669    ) -> Result<Option<LearnedPattern>> {
670        if !self.config.enabled {
671            return Ok(None);
672        }
673
674        let events = match self.behavior_history.get(persona_id) {
675            Some(events) => events,
676            None => return Ok(None),
677        };
678
679        if events.len() < self.config.min_samples {
680            return Ok(None); // Not enough samples
681        }
682
683        // Analyze patterns
684        // Example: If persona repeatedly requests /checkout after failure, learn this pattern
685        let mut checkout_after_failure_count = 0;
686        let mut total_failures = 0;
687
688        for i in 1..events.len() {
689            if let BehaviorEventType::RequestFailed { .. } = events[i - 1].event_type {
690                total_failures += 1;
691                if let BehaviorEventType::Request { endpoint, .. } = &events[i].event_type {
692                    if endpoint.contains("/checkout") {
693                        checkout_after_failure_count += 1;
694                    }
695                }
696            }
697        }
698
699        if total_failures > 0 && checkout_after_failure_count as f64 / total_failures as f64 > 0.5 {
700            // Pattern detected: persona requests /checkout after failure > 50% of the time
701            let mut parameters = HashMap::new();
702            parameters.insert("retry_checkout_after_failure".to_string(), Value::from(true));
703            parameters.insert(
704                "retry_probability".to_string(),
705                Value::from(checkout_after_failure_count as f64 / total_failures as f64),
706            );
707
708            return Ok(Some(LearnedPattern {
709                pattern_id: format!("persona_{}_checkout_retry", persona_id),
710                pattern_type: PatternType::PersonaBehavior,
711                parameters,
712                confidence: (checkout_after_failure_count as f64 / total_failures as f64).min(1.0),
713                sample_count: total_failures,
714                last_updated: chrono::Utc::now(),
715            }));
716        }
717
718        Ok(None)
719    }
720
721    /// Get behavior history for a persona
722    pub fn get_behavior_history(&self, persona_id: &str) -> Option<&Vec<BehaviorEvent>> {
723        self.behavior_history.get(persona_id)
724    }
725
726    /// Apply learned patterns to a persona in PersonaRegistry
727    ///
728    /// This method should be called periodically to update persona profiles
729    /// based on learned behavior patterns.
730    pub async fn apply_learned_patterns_to_persona(
731        &self,
732        persona_id: &str,
733        persona_registry: &crate::PersonaRegistry,
734    ) -> Result<()> {
735        if !self.config.enabled {
736            return Ok(());
737        }
738
739        // Analyze behavior for this persona
740        if let Some(pattern) = self.analyze_persona_behavior(persona_id).await? {
741            // Convert learned pattern parameters to traits
742            let mut learned_traits = HashMap::new();
743            for (key, value) in &pattern.parameters {
744                let trait_key = format!("learned_{}", key);
745                let trait_value = if let Some(s) = value.as_str() {
746                    s.to_string()
747                } else if let Some(n) = value.as_f64() {
748                    n.to_string()
749                } else if let Some(b) = value.as_bool() {
750                    b.to_string()
751                } else {
752                    value.to_string()
753                };
754                learned_traits.insert(trait_key, trait_value);
755            }
756
757            // Update persona traits in registry
758            if !learned_traits.is_empty() {
759                persona_registry.update_persona(persona_id, learned_traits)?;
760            }
761        }
762
763        Ok(())
764    }
765}
766
767#[cfg(test)]
768mod tests {
769    use super::*;
770
771    // =========================================================================
772    // LearningConfig tests
773    // =========================================================================
774
775    #[test]
776    fn test_learning_config_default() {
777        let config = LearningConfig::default();
778        assert!(!config.enabled); // Opt-in by default
779        assert_eq!(config.sensitivity, 0.2);
780        assert_eq!(config.min_samples, 10);
781        assert_eq!(config.decay, 0.05);
782        assert!(config.persona_adaptation);
783        assert!(config.traffic_mirroring);
784    }
785
786    #[test]
787    fn test_learning_config_serialize() {
788        let config = LearningConfig {
789            enabled: true,
790            mode: LearningMode::Statistical,
791            ..Default::default()
792        };
793        let json = serde_json::to_string(&config).unwrap();
794        assert!(json.contains("true"));
795        assert!(json.contains("statistical"));
796    }
797
798    #[test]
799    fn test_learning_config_deserialize() {
800        let json = r#"{"enabled": true, "mode": "hybrid", "sensitivity": 0.5}"#;
801        let config: LearningConfig = serde_json::from_str(json).unwrap();
802        assert!(config.enabled);
803        assert_eq!(config.mode, LearningMode::Hybrid);
804        assert!((config.sensitivity - 0.5).abs() < f64::EPSILON);
805    }
806
807    #[test]
808    fn test_learning_config_clone() {
809        let config = LearningConfig {
810            enabled: true,
811            sensitivity: 0.3,
812            ..Default::default()
813        };
814        let cloned = config.clone();
815        assert!(cloned.enabled);
816        assert!((cloned.sensitivity - 0.3).abs() < f64::EPSILON);
817    }
818
819    #[test]
820    fn test_learning_config_debug() {
821        let config = LearningConfig::default();
822        let debug_str = format!("{:?}", config);
823        assert!(debug_str.contains("sensitivity"));
824        assert!(debug_str.contains("enabled"));
825    }
826
827    #[test]
828    fn test_learning_config_endpoint_learning() {
829        let mut config = LearningConfig::default();
830        config.endpoint_learning.insert("/api/users".to_string(), true);
831        config.endpoint_learning.insert("/api/orders".to_string(), false);
832
833        assert_eq!(config.endpoint_learning.get("/api/users"), Some(&true));
834        assert_eq!(config.endpoint_learning.get("/api/orders"), Some(&false));
835    }
836
837    #[test]
838    fn test_learning_config_persona_learning() {
839        let mut config = LearningConfig::default();
840        config.persona_learning.insert("persona-1".to_string(), true);
841        config.persona_learning.insert("persona-2".to_string(), false);
842
843        assert_eq!(config.persona_learning.get("persona-1"), Some(&true));
844        assert_eq!(config.persona_learning.get("persona-2"), Some(&false));
845    }
846
847    // =========================================================================
848    // LearningMode tests
849    // =========================================================================
850
851    #[test]
852    fn test_learning_mode_default() {
853        let mode = LearningMode::default();
854        assert_eq!(mode, LearningMode::Behavioral);
855    }
856
857    #[test]
858    fn test_learning_mode_eq() {
859        assert_eq!(LearningMode::Statistical, LearningMode::Statistical);
860        assert_ne!(LearningMode::Behavioral, LearningMode::Hybrid);
861    }
862
863    #[test]
864    fn test_learning_mode_serialize() {
865        let mode = LearningMode::Hybrid;
866        let json = serde_json::to_string(&mode).unwrap();
867        assert_eq!(json, "\"hybrid\"");
868    }
869
870    #[test]
871    fn test_learning_mode_deserialize() {
872        let json = "\"statistical\"";
873        let mode: LearningMode = serde_json::from_str(json).unwrap();
874        assert_eq!(mode, LearningMode::Statistical);
875    }
876
877    #[test]
878    fn test_learning_mode_clone() {
879        let mode = LearningMode::Hybrid;
880        let cloned = mode.clone();
881        assert_eq!(cloned, LearningMode::Hybrid);
882    }
883
884    #[test]
885    fn test_learning_mode_debug() {
886        let debug_str = format!("{:?}", LearningMode::Behavioral);
887        assert!(debug_str.contains("Behavioral"));
888    }
889
890    // =========================================================================
891    // DriftLearningEngine tests
892    // =========================================================================
893
894    #[test]
895    fn test_drift_learning_engine_creation() {
896        let drift_config = DataDriftConfig::new();
897        let learning_config = LearningConfig::default();
898        let engine = DriftLearningEngine::new(drift_config, learning_config);
899        assert!(engine.is_ok());
900    }
901
902    #[test]
903    fn test_drift_learning_engine_with_traffic_mirroring_disabled() {
904        let drift_config = DataDriftConfig::new();
905        let learning_config = LearningConfig {
906            traffic_mirroring: false,
907            ..Default::default()
908        };
909        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
910        assert!(engine.traffic_learner.is_none());
911    }
912
913    #[test]
914    fn test_drift_learning_engine_with_persona_adaptation_disabled() {
915        let drift_config = DataDriftConfig::new();
916        let learning_config = LearningConfig {
917            persona_adaptation: false,
918            ..Default::default()
919        };
920        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
921        assert!(engine.persona_learner.is_none());
922    }
923
924    #[test]
925    fn test_drift_learning_engine_get_drift_engine() {
926        let drift_config = DataDriftConfig::new();
927        let learning_config = LearningConfig::default();
928        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
929        let _ = engine.drift_engine();
930    }
931
932    #[test]
933    fn test_drift_learning_engine_get_learning_config() {
934        let drift_config = DataDriftConfig::new();
935        let learning_config = LearningConfig {
936            enabled: true,
937            sensitivity: 0.5,
938            ..Default::default()
939        };
940        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
941        assert!(engine.learning_config().enabled);
942        assert!((engine.learning_config().sensitivity - 0.5).abs() < f64::EPSILON);
943    }
944
945    #[test]
946    fn test_drift_learning_engine_update_learning_config() {
947        let drift_config = DataDriftConfig::new();
948        let learning_config = LearningConfig::default();
949        let mut engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
950
951        let new_config = LearningConfig {
952            enabled: true,
953            sensitivity: 0.8,
954            ..Default::default()
955        };
956        engine.update_learning_config(new_config).unwrap();
957
958        assert!(engine.learning_config().enabled);
959        assert!((engine.learning_config().sensitivity - 0.8).abs() < f64::EPSILON);
960    }
961
962    #[tokio::test]
963    async fn test_drift_learning_engine_get_learned_patterns_empty() {
964        let drift_config = DataDriftConfig::new();
965        let learning_config = LearningConfig::default();
966        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
967
968        let patterns = engine.get_learned_patterns().await;
969        assert!(patterns.is_empty());
970    }
971
972    #[tokio::test]
973    async fn test_drift_learning_engine_apply_drift_with_learning_disabled() {
974        let drift_config = DataDriftConfig::new();
975        let learning_config = LearningConfig {
976            enabled: false,
977            ..Default::default()
978        };
979        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
980
981        let data = serde_json::json!({"value": 100});
982        let result = engine.apply_drift_with_learning(data.clone()).await.unwrap();
983        // Should return data without learning modifications
984        assert!(result.is_object());
985    }
986
987    #[tokio::test]
988    async fn test_drift_learning_engine_apply_drift_with_learning_enabled() {
989        let drift_config = DataDriftConfig::new();
990        let learning_config = LearningConfig {
991            enabled: true,
992            ..Default::default()
993        };
994        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
995
996        let data = serde_json::json!({"value": 100});
997        let result = engine.apply_drift_with_learning(data).await.unwrap();
998        assert!(result.is_object());
999    }
1000
1001    #[test]
1002    fn test_drift_learning_engine_blend_values_numeric() {
1003        let drift_config = DataDriftConfig::new();
1004        let learning_config = LearningConfig {
1005            sensitivity: 0.5,
1006            ..Default::default()
1007        };
1008        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
1009
1010        let existing = serde_json::json!(100.0);
1011        let learned = serde_json::json!(200.0);
1012        let result = engine.blend_values(&existing, &learned, 0.5).unwrap();
1013
1014        // With sensitivity 0.5 and confidence 0.5, weight = 0.25
1015        // blended = 100 * 0.75 + 200 * 0.25 = 75 + 50 = 125
1016        if let Some(n) = result.as_f64() {
1017            assert!((n - 125.0).abs() < 1.0);
1018        }
1019    }
1020
1021    #[test]
1022    fn test_drift_learning_engine_blend_values_non_numeric() {
1023        let drift_config = DataDriftConfig::new();
1024        let learning_config = LearningConfig::default();
1025        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
1026
1027        let existing = serde_json::json!("original");
1028        let learned = serde_json::json!("learned");
1029        let result = engine.blend_values(&existing, &learned, 0.5).unwrap();
1030
1031        // Non-numeric values keep existing
1032        assert_eq!(result, serde_json::json!("original"));
1033    }
1034
1035    // =========================================================================
1036    // LearnedPattern tests
1037    // =========================================================================
1038
1039    #[test]
1040    fn test_learned_pattern_creation() {
1041        let pattern = LearnedPattern {
1042            pattern_id: "test-pattern".to_string(),
1043            pattern_type: PatternType::Latency,
1044            parameters: HashMap::new(),
1045            confidence: 0.9,
1046            sample_count: 100,
1047            last_updated: chrono::Utc::now(),
1048        };
1049        assert_eq!(pattern.pattern_id, "test-pattern");
1050        assert!((pattern.confidence - 0.9).abs() < f64::EPSILON);
1051    }
1052
1053    #[test]
1054    fn test_learned_pattern_clone() {
1055        let pattern = LearnedPattern {
1056            pattern_id: "cloneable".to_string(),
1057            pattern_type: PatternType::ErrorRate,
1058            parameters: HashMap::new(),
1059            confidence: 0.75,
1060            sample_count: 50,
1061            last_updated: chrono::Utc::now(),
1062        };
1063        let cloned = pattern.clone();
1064        assert_eq!(cloned.pattern_id, "cloneable");
1065    }
1066
1067    #[test]
1068    fn test_learned_pattern_debug() {
1069        let pattern = LearnedPattern {
1070            pattern_id: "debug-pattern".to_string(),
1071            pattern_type: PatternType::PersonaBehavior,
1072            parameters: HashMap::new(),
1073            confidence: 0.5,
1074            sample_count: 25,
1075            last_updated: chrono::Utc::now(),
1076        };
1077        let debug_str = format!("{:?}", pattern);
1078        assert!(debug_str.contains("debug-pattern"));
1079    }
1080
1081    // =========================================================================
1082    // PatternType tests
1083    // =========================================================================
1084
1085    #[test]
1086    fn test_pattern_type_eq() {
1087        assert_eq!(PatternType::Latency, PatternType::Latency);
1088        assert_ne!(PatternType::Latency, PatternType::ErrorRate);
1089    }
1090
1091    #[test]
1092    fn test_pattern_type_clone() {
1093        let pt = PatternType::RequestSequence;
1094        let cloned = pt.clone();
1095        assert_eq!(cloned, PatternType::RequestSequence);
1096    }
1097
1098    #[test]
1099    fn test_pattern_type_debug() {
1100        let debug_str = format!("{:?}", PatternType::PersonaBehavior);
1101        assert!(debug_str.contains("PersonaBehavior"));
1102    }
1103
1104    // =========================================================================
1105    // TrafficPatternLearner tests
1106    // =========================================================================
1107
1108    #[test]
1109    fn test_traffic_pattern_learner_new() {
1110        let config = LearningConfig::default();
1111        let learner = TrafficPatternLearner::new(config);
1112        assert!(learner.is_ok());
1113    }
1114
1115    #[tokio::test]
1116    async fn test_traffic_pattern_learner_analyze_traffic_patterns() {
1117        let config = LearningConfig::default();
1118        let mut learner = TrafficPatternLearner::new(config).unwrap();
1119        // Returns empty since database integration is disabled
1120        let patterns = learner.analyze_traffic_patterns(&()).await.unwrap();
1121        assert!(patterns.is_empty());
1122    }
1123
1124    #[tokio::test]
1125    async fn test_traffic_pattern_learner_detect_latency_patterns() {
1126        let config = LearningConfig::default();
1127        let mut learner = TrafficPatternLearner::new(config).unwrap();
1128        let patterns = learner.detect_latency_patterns().await.unwrap();
1129        assert!(patterns.is_empty()); // Returns empty since disabled
1130    }
1131
1132    #[tokio::test]
1133    async fn test_traffic_pattern_learner_detect_error_patterns() {
1134        let config = LearningConfig::default();
1135        let mut learner = TrafficPatternLearner::new(config).unwrap();
1136        let patterns = learner.detect_error_patterns().await.unwrap();
1137        assert!(patterns.is_empty()); // Returns empty since disabled
1138    }
1139
1140    // =========================================================================
1141    // PersonaBehaviorLearner tests
1142    // =========================================================================
1143
1144    #[test]
1145    fn test_persona_behavior_learner_new() {
1146        let config = LearningConfig::default();
1147        let learner = PersonaBehaviorLearner::new(config);
1148        assert!(learner.is_ok());
1149    }
1150
1151    #[test]
1152    fn test_persona_behavior_learner_record_event_disabled() {
1153        let config = LearningConfig {
1154            enabled: false,
1155            ..Default::default()
1156        };
1157        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1158
1159        learner.record_event(
1160            "persona-1".to_string(),
1161            BehaviorEvent {
1162                timestamp: chrono::Utc::now(),
1163                event_type: BehaviorEventType::Request {
1164                    endpoint: "/api/test".to_string(),
1165                    method: "GET".to_string(),
1166                },
1167                data: HashMap::new(),
1168            },
1169        );
1170
1171        // Should not record when disabled
1172        assert!(learner.get_behavior_history("persona-1").is_none());
1173    }
1174
1175    #[test]
1176    fn test_persona_behavior_learner_record_event_persona_disabled() {
1177        let mut config = LearningConfig {
1178            enabled: true,
1179            ..Default::default()
1180        };
1181        config.persona_learning.insert("persona-1".to_string(), false);
1182        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1183
1184        learner.record_event(
1185            "persona-1".to_string(),
1186            BehaviorEvent {
1187                timestamp: chrono::Utc::now(),
1188                event_type: BehaviorEventType::Request {
1189                    endpoint: "/api/test".to_string(),
1190                    method: "GET".to_string(),
1191                },
1192                data: HashMap::new(),
1193            },
1194        );
1195
1196        // Should not record when persona learning is disabled
1197        assert!(learner.get_behavior_history("persona-1").is_none());
1198    }
1199
1200    #[tokio::test]
1201    async fn test_persona_behavior_learner() {
1202        let config = LearningConfig {
1203            enabled: true,
1204            persona_adaptation: true,
1205            ..Default::default()
1206        };
1207        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1208
1209        // Record failure
1210        learner.record_event(
1211            "persona-1".to_string(),
1212            BehaviorEvent {
1213                timestamp: chrono::Utc::now(),
1214                event_type: BehaviorEventType::RequestFailed {
1215                    endpoint: "/api/checkout".to_string(),
1216                    status_code: 500,
1217                },
1218                data: HashMap::new(),
1219            },
1220        );
1221
1222        // Record checkout request after failure
1223        learner.record_event(
1224            "persona-1".to_string(),
1225            BehaviorEvent {
1226                timestamp: chrono::Utc::now(),
1227                event_type: BehaviorEventType::Request {
1228                    endpoint: "/api/checkout".to_string(),
1229                    method: "POST".to_string(),
1230                },
1231                data: HashMap::new(),
1232            },
1233        );
1234
1235        // Analyze (won't find pattern with only 2 samples, need min_samples)
1236        let pattern = learner.analyze_persona_behavior("persona-1").await.unwrap();
1237        assert!(pattern.is_none()); // Not enough samples
1238    }
1239
1240    #[tokio::test]
1241    async fn test_persona_behavior_learner_get_behavior_history() {
1242        let config = LearningConfig {
1243            enabled: true,
1244            ..Default::default()
1245        };
1246        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1247
1248        learner.record_event(
1249            "persona-test".to_string(),
1250            BehaviorEvent {
1251                timestamp: chrono::Utc::now(),
1252                event_type: BehaviorEventType::Request {
1253                    endpoint: "/api/users".to_string(),
1254                    method: "GET".to_string(),
1255                },
1256                data: HashMap::new(),
1257            },
1258        );
1259
1260        let history = learner.get_behavior_history("persona-test");
1261        assert!(history.is_some());
1262        assert_eq!(history.unwrap().len(), 1);
1263    }
1264
1265    #[tokio::test]
1266    async fn test_persona_behavior_learner_analyze_nonexistent_persona() {
1267        let config = LearningConfig {
1268            enabled: true,
1269            ..Default::default()
1270        };
1271        let learner = PersonaBehaviorLearner::new(config).unwrap();
1272
1273        let pattern = learner.analyze_persona_behavior("nonexistent").await.unwrap();
1274        assert!(pattern.is_none());
1275    }
1276
1277    #[tokio::test]
1278    async fn test_persona_behavior_learner_analyze_disabled() {
1279        let config = LearningConfig {
1280            enabled: false,
1281            ..Default::default()
1282        };
1283        let learner = PersonaBehaviorLearner::new(config).unwrap();
1284
1285        let pattern = learner.analyze_persona_behavior("any").await.unwrap();
1286        assert!(pattern.is_none());
1287    }
1288
1289    #[test]
1290    fn test_persona_behavior_learner_event_limit() {
1291        let config = LearningConfig {
1292            enabled: true,
1293            ..Default::default()
1294        };
1295        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1296
1297        // Record more than 1000 events
1298        for i in 0..1050 {
1299            learner.record_event(
1300                "persona-limit".to_string(),
1301                BehaviorEvent {
1302                    timestamp: chrono::Utc::now(),
1303                    event_type: BehaviorEventType::Request {
1304                        endpoint: format!("/api/test/{}", i),
1305                        method: "GET".to_string(),
1306                    },
1307                    data: HashMap::new(),
1308                },
1309            );
1310        }
1311
1312        let history = learner.get_behavior_history("persona-limit").unwrap();
1313        assert_eq!(history.len(), 1000); // Should be capped at 1000
1314    }
1315
1316    // =========================================================================
1317    // BehaviorEvent tests
1318    // =========================================================================
1319
1320    #[test]
1321    fn test_behavior_event_creation() {
1322        let event = BehaviorEvent {
1323            timestamp: chrono::Utc::now(),
1324            event_type: BehaviorEventType::Request {
1325                endpoint: "/api/test".to_string(),
1326                method: "POST".to_string(),
1327            },
1328            data: HashMap::new(),
1329        };
1330        assert!(event.data.is_empty());
1331    }
1332
1333    #[test]
1334    fn test_behavior_event_clone() {
1335        let event = BehaviorEvent {
1336            timestamp: chrono::Utc::now(),
1337            event_type: BehaviorEventType::PatternDetected {
1338                pattern: "test-pattern".to_string(),
1339            },
1340            data: HashMap::new(),
1341        };
1342        let cloned = event.clone();
1343        if let BehaviorEventType::PatternDetected { pattern } = cloned.event_type {
1344            assert_eq!(pattern, "test-pattern");
1345        } else {
1346            panic!("Wrong event type after clone");
1347        }
1348    }
1349
1350    #[test]
1351    fn test_behavior_event_debug() {
1352        let event = BehaviorEvent {
1353            timestamp: chrono::Utc::now(),
1354            event_type: BehaviorEventType::RequestSucceededAfterFailure {
1355                endpoint: "/api/retry".to_string(),
1356            },
1357            data: HashMap::new(),
1358        };
1359        let debug_str = format!("{:?}", event);
1360        assert!(debug_str.contains("RequestSucceededAfterFailure"));
1361    }
1362
1363    // =========================================================================
1364    // BehaviorEventType tests
1365    // =========================================================================
1366
1367    #[test]
1368    fn test_behavior_event_type_request() {
1369        let event_type = BehaviorEventType::Request {
1370            endpoint: "/api/users".to_string(),
1371            method: "GET".to_string(),
1372        };
1373        if let BehaviorEventType::Request { endpoint, method } = event_type {
1374            assert_eq!(endpoint, "/api/users");
1375            assert_eq!(method, "GET");
1376        }
1377    }
1378
1379    #[test]
1380    fn test_behavior_event_type_request_failed() {
1381        let event_type = BehaviorEventType::RequestFailed {
1382            endpoint: "/api/orders".to_string(),
1383            status_code: 500,
1384        };
1385        if let BehaviorEventType::RequestFailed {
1386            endpoint,
1387            status_code,
1388        } = event_type
1389        {
1390            assert_eq!(endpoint, "/api/orders");
1391            assert_eq!(status_code, 500);
1392        }
1393    }
1394
1395    #[test]
1396    fn test_behavior_event_type_eq() {
1397        let a = BehaviorEventType::PatternDetected {
1398            pattern: "a".to_string(),
1399        };
1400        let b = BehaviorEventType::PatternDetected {
1401            pattern: "a".to_string(),
1402        };
1403        assert_eq!(a, b);
1404    }
1405
1406    #[test]
1407    fn test_behavior_event_type_clone() {
1408        let event_type = BehaviorEventType::RequestFailed {
1409            endpoint: "/test".to_string(),
1410            status_code: 404,
1411        };
1412        let cloned = event_type.clone();
1413        assert_eq!(cloned, event_type);
1414    }
1415
1416    #[test]
1417    fn test_behavior_event_type_debug() {
1418        let event_type = BehaviorEventType::Request {
1419            endpoint: "/debug".to_string(),
1420            method: "DELETE".to_string(),
1421        };
1422        let debug_str = format!("{:?}", event_type);
1423        assert!(debug_str.contains("Request"));
1424        assert!(debug_str.contains("/debug"));
1425    }
1426
1427    // =========================================================================
1428    // Integration tests
1429    // =========================================================================
1430
1431    #[tokio::test]
1432    async fn test_full_learning_workflow() {
1433        // Create learning engine
1434        let drift_config = DataDriftConfig::new();
1435        let learning_config = LearningConfig {
1436            enabled: true,
1437            min_samples: 2, // Lower threshold for testing
1438            ..Default::default()
1439        };
1440        let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
1441
1442        // Apply drift with learning
1443        let data = serde_json::json!({
1444            "user_id": "123",
1445            "amount": 100.0,
1446            "status": "pending"
1447        });
1448        let result = engine.apply_drift_with_learning(data).await.unwrap();
1449        assert!(result.is_object());
1450    }
1451
1452    #[tokio::test]
1453    async fn test_persona_behavior_pattern_detection() {
1454        let config = LearningConfig {
1455            enabled: true,
1456            min_samples: 5,
1457            ..Default::default()
1458        };
1459        let mut learner = PersonaBehaviorLearner::new(config).unwrap();
1460
1461        // Simulate pattern: failure followed by checkout retry
1462        for _ in 0..10 {
1463            learner.record_event(
1464                "retry-persona".to_string(),
1465                BehaviorEvent {
1466                    timestamp: chrono::Utc::now(),
1467                    event_type: BehaviorEventType::RequestFailed {
1468                        endpoint: "/api/payment".to_string(),
1469                        status_code: 503,
1470                    },
1471                    data: HashMap::new(),
1472                },
1473            );
1474            learner.record_event(
1475                "retry-persona".to_string(),
1476                BehaviorEvent {
1477                    timestamp: chrono::Utc::now(),
1478                    event_type: BehaviorEventType::Request {
1479                        endpoint: "/api/checkout".to_string(),
1480                        method: "POST".to_string(),
1481                    },
1482                    data: HashMap::new(),
1483                },
1484            );
1485        }
1486
1487        // Now analyze - should detect pattern
1488        let pattern = learner.analyze_persona_behavior("retry-persona").await.unwrap();
1489        assert!(pattern.is_some());
1490        let p = pattern.unwrap();
1491        assert_eq!(p.pattern_type, PatternType::PersonaBehavior);
1492        assert!(p.confidence > 0.0);
1493    }
1494}