use crate::drift::{DataDriftConfig, DataDriftEngine};
use crate::Result;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LearningConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub mode: LearningMode,
#[serde(default = "default_learning_rate")]
pub sensitivity: f64,
#[serde(default = "default_decay_rate")]
pub decay: f64,
#[serde(default = "default_min_samples")]
pub min_samples: usize,
#[serde(default = "default_update_interval")]
pub update_interval: Duration,
#[serde(default = "default_true")]
pub persona_adaptation: bool,
#[serde(default = "default_true")]
pub traffic_mirroring: bool,
#[serde(default)]
pub endpoint_learning: HashMap<String, bool>,
#[serde(default)]
pub persona_learning: HashMap<String, bool>,
}
fn default_learning_rate() -> f64 {
0.2 }
fn default_decay_rate() -> f64 {
0.05 }
fn default_min_samples() -> usize {
10 }
fn default_update_interval() -> Duration {
Duration::from_secs(60) }
fn default_true() -> bool {
true
}
impl Default for LearningConfig {
fn default() -> Self {
Self {
enabled: false, mode: LearningMode::Behavioral,
sensitivity: default_learning_rate(),
decay: default_decay_rate(),
min_samples: default_min_samples(),
update_interval: default_update_interval(),
persona_adaptation: true,
traffic_mirroring: true,
endpoint_learning: HashMap::new(),
persona_learning: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum LearningMode {
#[default]
Behavioral,
Statistical,
Hybrid,
}
pub struct DriftLearningEngine {
drift_engine: DataDriftEngine,
learning_config: LearningConfig,
_traffic_learner: Option<Arc<RwLock<TrafficPatternLearner>>>,
_persona_learner: Option<Arc<RwLock<PersonaBehaviorLearner>>>,
learned_patterns: Arc<RwLock<HashMap<String, LearnedPattern>>>,
}
#[derive(Debug, Clone)]
pub struct LearnedPattern {
pub pattern_id: String,
pub pattern_type: PatternType,
pub parameters: HashMap<String, Value>,
pub confidence: f64,
pub sample_count: usize,
pub last_updated: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PatternType {
Latency,
ErrorRate,
RequestSequence,
PersonaBehavior,
}
impl DriftLearningEngine {
pub fn new(drift_config: DataDriftConfig, learning_config: LearningConfig) -> Result<Self> {
let drift_engine = DataDriftEngine::new(drift_config)?;
let traffic_learner = if learning_config.traffic_mirroring {
Some(Arc::new(RwLock::new(TrafficPatternLearner::new(learning_config.clone())?)))
} else {
None
};
let persona_learner = if learning_config.persona_adaptation {
Some(Arc::new(RwLock::new(PersonaBehaviorLearner::new(learning_config.clone())?)))
} else {
None
};
Ok(Self {
drift_engine,
learning_config,
_traffic_learner: traffic_learner,
_persona_learner: persona_learner,
learned_patterns: Arc::new(RwLock::new(HashMap::new())),
})
}
pub fn drift_engine(&self) -> &DataDriftEngine {
&self.drift_engine
}
pub fn learning_config(&self) -> &LearningConfig {
&self.learning_config
}
pub fn update_learning_config(&mut self, config: LearningConfig) -> Result<()> {
self.learning_config = config;
Ok(())
}
pub async fn get_learned_patterns(&self) -> HashMap<String, LearnedPattern> {
self.learned_patterns.read().await.clone()
}
pub async fn apply_drift_with_learning(&self, data: Value) -> Result<Value> {
let mut data = self.drift_engine.apply_drift(data).await?;
if !self.learning_config.enabled {
return Ok(data);
}
let patterns = self.learned_patterns.read().await;
for (_pattern_id, pattern) in patterns.iter() {
if pattern.confidence < 0.5 {
continue; }
match pattern.pattern_type {
PatternType::Latency => {
}
PatternType::ErrorRate => {
}
PatternType::RequestSequence => {
}
PatternType::PersonaBehavior => {
if let Some(obj) = data.as_object_mut() {
for (key, value) in &pattern.parameters {
if let Some(existing) = obj.get(key) {
let blended =
self.blend_values(existing, value, pattern.confidence)?;
obj.insert(key.clone(), blended);
}
}
}
}
}
}
Ok(data)
}
fn blend_values(&self, existing: &Value, learned: &Value, confidence: f64) -> Result<Value> {
let weight = confidence * self.learning_config.sensitivity;
match (existing, learned) {
(Value::Number(existing_num), Value::Number(learned_num)) => {
if let (Some(existing_f64), Some(learned_f64)) =
(existing_num.as_f64(), learned_num.as_f64())
{
let blended = existing_f64 * (1.0 - weight) + learned_f64 * weight;
Ok(Value::from(blended))
} else {
Ok(existing.clone())
}
}
_ => Ok(existing.clone()), }
}
}
pub struct TrafficPatternLearner {
_config: LearningConfig,
}
impl TrafficPatternLearner {
pub fn new(config: LearningConfig) -> Result<Self> {
Ok(Self { _config: config })
}
pub async fn analyze_traffic_patterns(
&mut self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
let mut patterns = Vec::new();
patterns.extend(self.detect_latency_patterns_from_requests(requests).await?);
patterns.extend(self.detect_error_patterns_from_requests(requests).await?);
patterns.extend(self.detect_sequence_patterns_from_requests(requests).await?);
Ok(patterns)
}
pub async fn detect_latency_patterns_from_requests(
&self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
use chrono::Utc;
let mut endpoint_latencies: HashMap<String, Vec<i64>> = HashMap::new();
for request in requests {
let method = request.get("method").and_then(|v| v.as_str()).unwrap_or("UNKNOWN");
let path = request.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let duration = request.get("duration_ms").and_then(|v| v.as_i64());
if let Some(duration_ms) = duration {
let key = format!("{} {}", method, path);
endpoint_latencies.entry(key).or_default().push(duration_ms);
}
}
let mut patterns = Vec::new();
for (endpoint_key, latencies) in endpoint_latencies {
if latencies.len() < 10 {
continue;
}
let sum: i64 = latencies.iter().sum();
let count = latencies.len();
let avg_latency = sum as f64 / count as f64;
let mut sorted = latencies.clone();
sorted.sort();
let p50 = sorted[count / 2];
let p95 = sorted[(count * 95) / 100];
let p99 = sorted[(count * 99) / 100];
let recent_avg = if latencies.len() >= 20 {
let recent: Vec<i64> = latencies.iter().rev().take(10).copied().collect();
let recent_sum: i64 = recent.iter().sum();
recent_sum as f64 / recent.len() as f64
} else {
avg_latency
};
let latency_trend = if recent_avg > avg_latency * 1.2 {
"increasing"
} else if recent_avg < avg_latency * 0.8 {
"decreasing"
} else {
"stable"
};
if p99 > p50 * 2 || latency_trend != "stable" {
let mut parameters = HashMap::new();
parameters.insert("endpoint".to_string(), serde_json::json!(endpoint_key));
parameters.insert("avg_latency_ms".to_string(), serde_json::json!(avg_latency));
parameters.insert("p50_ms".to_string(), serde_json::json!(p50));
parameters.insert("p95_ms".to_string(), serde_json::json!(p95));
parameters.insert("p99_ms".to_string(), serde_json::json!(p99));
parameters.insert("sample_count".to_string(), serde_json::json!(count));
parameters.insert("trend".to_string(), serde_json::json!(latency_trend));
let confidence = (count as f64 / 100.0).min(1.0);
patterns.push(LearnedPattern {
pattern_id: format!("latency_{}", endpoint_key.replace(['/', ' '], "_")),
pattern_type: PatternType::Latency,
parameters,
confidence,
sample_count: count,
last_updated: Utc::now(),
});
}
}
Ok(patterns)
}
async fn detect_error_patterns_from_requests(
&self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
use chrono::Utc;
let mut endpoint_errors: HashMap<String, (usize, usize)> = HashMap::new();
for request in requests {
let method = request.get("method").and_then(|v| v.as_str()).unwrap_or("UNKNOWN");
let path = request.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let status_code = request.get("status_code").and_then(|v| v.as_u64());
let key = format!("{} {}", method, path);
let entry = endpoint_errors.entry(key).or_insert((0, 0));
entry.0 += 1;
if let Some(status) = status_code {
if status >= 400 {
entry.1 += 1;
}
}
}
let mut patterns = Vec::new();
for (endpoint_key, (total, errors)) in endpoint_errors {
if total < 20 {
continue;
}
let error_rate = errors as f64 / total as f64;
if error_rate > 0.05 {
let mut parameters = HashMap::new();
parameters.insert("endpoint".to_string(), serde_json::json!(endpoint_key));
parameters.insert("error_rate".to_string(), serde_json::json!(error_rate));
parameters.insert("total_requests".to_string(), serde_json::json!(total));
parameters.insert("error_count".to_string(), serde_json::json!(errors));
let confidence = ((total as f64 / 100.0).min(1.0) * error_rate * 10.0).min(1.0);
patterns.push(LearnedPattern {
pattern_id: format!("error_rate_{}", endpoint_key.replace(['/', ' '], "_")),
pattern_type: PatternType::ErrorRate,
parameters,
confidence,
sample_count: total,
last_updated: Utc::now(),
});
}
}
Ok(patterns)
}
async fn detect_sequence_patterns_from_requests(
&self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
use chrono::Utc;
if requests.len() < 50 {
return Ok(Vec::new());
}
let mut trace_sequences: HashMap<Option<String>, Vec<String>> = HashMap::new();
for request in requests {
let trace_id = request.get("trace_id").and_then(|v| v.as_str()).map(String::from);
let method = request.get("method").and_then(|v| v.as_str()).unwrap_or("UNKNOWN");
let path = request.get("path").and_then(|v| v.as_str()).unwrap_or("/");
let endpoint_key = format!("{} {}", method, path);
trace_sequences.entry(trace_id).or_default().push(endpoint_key);
}
let mut sequence_counts: HashMap<String, usize> = HashMap::new();
for sequence in trace_sequences.values() {
if sequence.len() >= 2 {
let signature: Vec<String> = sequence.iter().take(3).cloned().collect();
let signature_str = signature.join(" -> ");
*sequence_counts.entry(signature_str).or_insert(0) += 1;
}
}
let mut patterns = Vec::new();
for (sequence_str, count) in sequence_counts {
if count >= 5 {
let mut parameters = HashMap::new();
parameters.insert("sequence".to_string(), serde_json::json!(sequence_str));
parameters.insert("occurrence_count".to_string(), serde_json::json!(count));
let confidence = (count as f64 / 20.0).min(1.0);
patterns.push(LearnedPattern {
pattern_id: format!(
"sequence_{}",
sequence_str.replace(['/', ' '], "_").replace("->", "_")
),
pattern_type: PatternType::RequestSequence,
parameters,
confidence,
sample_count: count,
last_updated: Utc::now(),
});
}
}
Ok(patterns)
}
pub async fn detect_latency_patterns(
&mut self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
self.detect_latency_patterns_from_requests(requests).await
}
pub async fn detect_error_patterns(
&mut self,
requests: &[Value],
) -> Result<Vec<LearnedPattern>> {
self.detect_error_patterns_from_requests(requests).await
}
}
pub struct PersonaBehaviorLearner {
config: LearningConfig,
behavior_history: HashMap<String, Vec<BehaviorEvent>>,
}
#[derive(Debug, Clone)]
pub struct BehaviorEvent {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub event_type: BehaviorEventType,
pub data: HashMap<String, Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BehaviorEventType {
Request {
endpoint: String,
method: String,
},
RequestFailed {
endpoint: String,
status_code: u16,
},
RequestSucceededAfterFailure {
endpoint: String,
},
PatternDetected {
pattern: String,
},
}
impl PersonaBehaviorLearner {
pub fn new(config: LearningConfig) -> Result<Self> {
Ok(Self {
config,
behavior_history: HashMap::new(),
})
}
pub fn record_event(&mut self, persona_id: String, event: BehaviorEvent) {
if !self.config.enabled {
return;
}
if let Some(&enabled) = self.config.persona_learning.get(&persona_id) {
if !enabled {
return; }
}
let events = self.behavior_history.entry(persona_id).or_default();
events.push(event);
if events.len() > 1000 {
events.remove(0);
}
}
pub async fn analyze_persona_behavior(
&self,
persona_id: &str,
) -> Result<Option<LearnedPattern>> {
if !self.config.enabled {
return Ok(None);
}
let events = match self.behavior_history.get(persona_id) {
Some(events) => events,
None => return Ok(None),
};
if events.len() < self.config.min_samples {
return Ok(None); }
let mut checkout_after_failure_count = 0;
let mut total_failures = 0;
for i in 1..events.len() {
if let BehaviorEventType::RequestFailed { .. } = events[i - 1].event_type {
total_failures += 1;
if let BehaviorEventType::Request { endpoint, .. } = &events[i].event_type {
if endpoint.contains("/checkout") {
checkout_after_failure_count += 1;
}
}
}
}
if total_failures > 0 && checkout_after_failure_count as f64 / total_failures as f64 > 0.5 {
let mut parameters = HashMap::new();
parameters.insert("retry_checkout_after_failure".to_string(), Value::from(true));
parameters.insert(
"retry_probability".to_string(),
Value::from(checkout_after_failure_count as f64 / total_failures as f64),
);
return Ok(Some(LearnedPattern {
pattern_id: format!("persona_{}_checkout_retry", persona_id),
pattern_type: PatternType::PersonaBehavior,
parameters,
confidence: (checkout_after_failure_count as f64 / total_failures as f64).min(1.0),
sample_count: total_failures,
last_updated: chrono::Utc::now(),
}));
}
Ok(None)
}
pub fn get_behavior_history(&self, persona_id: &str) -> Option<&Vec<BehaviorEvent>> {
self.behavior_history.get(persona_id)
}
pub async fn apply_learned_patterns_to_persona(
&self,
persona_id: &str,
persona_registry: &crate::PersonaRegistry,
) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
if let Some(pattern) = self.analyze_persona_behavior(persona_id).await? {
let mut learned_traits = HashMap::new();
for (key, value) in &pattern.parameters {
let trait_key = format!("learned_{}", key);
let trait_value = if let Some(s) = value.as_str() {
s.to_string()
} else if let Some(n) = value.as_f64() {
n.to_string()
} else if let Some(b) = value.as_bool() {
b.to_string()
} else {
value.to_string()
};
learned_traits.insert(trait_key, trait_value);
}
if !learned_traits.is_empty() {
persona_registry.update_persona(persona_id, learned_traits)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_learning_config_default() {
let config = LearningConfig::default();
assert!(!config.enabled); assert_eq!(config.sensitivity, 0.2);
assert_eq!(config.min_samples, 10);
assert_eq!(config.decay, 0.05);
assert!(config.persona_adaptation);
assert!(config.traffic_mirroring);
}
#[test]
fn test_learning_config_serialize() {
let config = LearningConfig {
enabled: true,
mode: LearningMode::Statistical,
..Default::default()
};
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("true"));
assert!(json.contains("statistical"));
}
#[test]
fn test_learning_config_deserialize() {
let json = r#"{"enabled": true, "mode": "hybrid", "sensitivity": 0.5}"#;
let config: LearningConfig = serde_json::from_str(json).unwrap();
assert!(config.enabled);
assert_eq!(config.mode, LearningMode::Hybrid);
assert!((config.sensitivity - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_learning_config_clone() {
let config = LearningConfig {
enabled: true,
sensitivity: 0.3,
..Default::default()
};
let cloned = config.clone();
assert!(cloned.enabled);
assert!((cloned.sensitivity - 0.3).abs() < f64::EPSILON);
}
#[test]
fn test_learning_config_debug() {
let config = LearningConfig::default();
let debug_str = format!("{:?}", config);
assert!(debug_str.contains("sensitivity"));
assert!(debug_str.contains("enabled"));
}
#[test]
fn test_learning_config_endpoint_learning() {
let mut config = LearningConfig::default();
config.endpoint_learning.insert("/api/users".to_string(), true);
config.endpoint_learning.insert("/api/orders".to_string(), false);
assert_eq!(config.endpoint_learning.get("/api/users"), Some(&true));
assert_eq!(config.endpoint_learning.get("/api/orders"), Some(&false));
}
#[test]
fn test_learning_config_persona_learning() {
let mut config = LearningConfig::default();
config.persona_learning.insert("persona-1".to_string(), true);
config.persona_learning.insert("persona-2".to_string(), false);
assert_eq!(config.persona_learning.get("persona-1"), Some(&true));
assert_eq!(config.persona_learning.get("persona-2"), Some(&false));
}
#[test]
fn test_learning_mode_default() {
let mode = LearningMode::default();
assert_eq!(mode, LearningMode::Behavioral);
}
#[test]
fn test_learning_mode_eq() {
assert_eq!(LearningMode::Statistical, LearningMode::Statistical);
assert_ne!(LearningMode::Behavioral, LearningMode::Hybrid);
}
#[test]
fn test_learning_mode_serialize() {
let mode = LearningMode::Hybrid;
let json = serde_json::to_string(&mode).unwrap();
assert_eq!(json, "\"hybrid\"");
}
#[test]
fn test_learning_mode_deserialize() {
let json = "\"statistical\"";
let mode: LearningMode = serde_json::from_str(json).unwrap();
assert_eq!(mode, LearningMode::Statistical);
}
#[test]
fn test_learning_mode_clone() {
let mode = LearningMode::Hybrid;
let cloned = mode.clone();
assert_eq!(cloned, LearningMode::Hybrid);
}
#[test]
fn test_learning_mode_debug() {
let debug_str = format!("{:?}", LearningMode::Behavioral);
assert!(debug_str.contains("Behavioral"));
}
#[test]
fn test_drift_learning_engine_creation() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig::default();
let engine = DriftLearningEngine::new(drift_config, learning_config);
assert!(engine.is_ok());
}
#[test]
fn test_drift_learning_engine_with_traffic_mirroring_disabled() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
traffic_mirroring: false,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
assert!(engine._traffic_learner.is_none());
}
#[test]
fn test_drift_learning_engine_with_persona_adaptation_disabled() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
persona_adaptation: false,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
assert!(engine._persona_learner.is_none());
}
#[test]
fn test_drift_learning_engine_get_drift_engine() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig::default();
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let _ = engine.drift_engine();
}
#[test]
fn test_drift_learning_engine_get_learning_config() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
enabled: true,
sensitivity: 0.5,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
assert!(engine.learning_config().enabled);
assert!((engine.learning_config().sensitivity - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_drift_learning_engine_update_learning_config() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig::default();
let mut engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let new_config = LearningConfig {
enabled: true,
sensitivity: 0.8,
..Default::default()
};
engine.update_learning_config(new_config).unwrap();
assert!(engine.learning_config().enabled);
assert!((engine.learning_config().sensitivity - 0.8).abs() < f64::EPSILON);
}
#[tokio::test]
async fn test_drift_learning_engine_get_learned_patterns_empty() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig::default();
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let patterns = engine.get_learned_patterns().await;
assert!(patterns.is_empty());
}
#[tokio::test]
async fn test_drift_learning_engine_apply_drift_with_learning_disabled() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
enabled: false,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let data = serde_json::json!({"value": 100});
let result = engine.apply_drift_with_learning(data.clone()).await.unwrap();
assert!(result.is_object());
}
#[tokio::test]
async fn test_drift_learning_engine_apply_drift_with_learning_enabled() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
enabled: true,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let data = serde_json::json!({"value": 100});
let result = engine.apply_drift_with_learning(data).await.unwrap();
assert!(result.is_object());
}
#[test]
fn test_drift_learning_engine_blend_values_numeric() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
sensitivity: 0.5,
..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let existing = serde_json::json!(100.0);
let learned = serde_json::json!(200.0);
let result = engine.blend_values(&existing, &learned, 0.5).unwrap();
if let Some(n) = result.as_f64() {
assert!((n - 125.0).abs() < 1.0);
}
}
#[test]
fn test_drift_learning_engine_blend_values_non_numeric() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig::default();
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let existing = serde_json::json!("original");
let learned = serde_json::json!("learned");
let result = engine.blend_values(&existing, &learned, 0.5).unwrap();
assert_eq!(result, serde_json::json!("original"));
}
#[test]
fn test_learned_pattern_creation() {
let pattern = LearnedPattern {
pattern_id: "test-pattern".to_string(),
pattern_type: PatternType::Latency,
parameters: HashMap::new(),
confidence: 0.9,
sample_count: 100,
last_updated: chrono::Utc::now(),
};
assert_eq!(pattern.pattern_id, "test-pattern");
assert!((pattern.confidence - 0.9).abs() < f64::EPSILON);
}
#[test]
fn test_learned_pattern_clone() {
let pattern = LearnedPattern {
pattern_id: "cloneable".to_string(),
pattern_type: PatternType::ErrorRate,
parameters: HashMap::new(),
confidence: 0.75,
sample_count: 50,
last_updated: chrono::Utc::now(),
};
let cloned = pattern.clone();
assert_eq!(cloned.pattern_id, "cloneable");
}
#[test]
fn test_learned_pattern_debug() {
let pattern = LearnedPattern {
pattern_id: "debug-pattern".to_string(),
pattern_type: PatternType::PersonaBehavior,
parameters: HashMap::new(),
confidence: 0.5,
sample_count: 25,
last_updated: chrono::Utc::now(),
};
let debug_str = format!("{:?}", pattern);
assert!(debug_str.contains("debug-pattern"));
}
#[test]
fn test_pattern_type_eq() {
assert_eq!(PatternType::Latency, PatternType::Latency);
assert_ne!(PatternType::Latency, PatternType::ErrorRate);
}
#[test]
fn test_pattern_type_clone() {
let pt = PatternType::RequestSequence;
let cloned = pt.clone();
assert_eq!(cloned, PatternType::RequestSequence);
}
#[test]
fn test_pattern_type_debug() {
let debug_str = format!("{:?}", PatternType::PersonaBehavior);
assert!(debug_str.contains("PersonaBehavior"));
}
#[test]
fn test_traffic_pattern_learner_new() {
let config = LearningConfig::default();
let learner = TrafficPatternLearner::new(config);
assert!(learner.is_ok());
}
#[tokio::test]
async fn test_traffic_pattern_learner_analyze_empty() {
let config = LearningConfig::default();
let mut learner = TrafficPatternLearner::new(config).unwrap();
let patterns = learner.analyze_traffic_patterns(&[]).await.unwrap();
assert!(patterns.is_empty());
}
#[tokio::test]
async fn test_traffic_pattern_learner_detect_latency_patterns() {
let config = LearningConfig::default();
let mut learner = TrafficPatternLearner::new(config).unwrap();
let requests: Vec<Value> = (0..5)
.map(|i| {
serde_json::json!({
"method": "GET",
"path": "/api/test",
"duration_ms": 100 + i * 10,
})
})
.collect();
let patterns = learner.detect_latency_patterns(&requests).await.unwrap();
assert!(patterns.is_empty());
}
#[tokio::test]
async fn test_traffic_pattern_learner_detect_error_patterns() {
let config = LearningConfig::default();
let mut learner = TrafficPatternLearner::new(config).unwrap();
let requests: Vec<Value> = (0..10)
.map(|_| {
serde_json::json!({
"method": "GET",
"path": "/api/test",
"status_code": 500,
})
})
.collect();
let patterns = learner.detect_error_patterns(&requests).await.unwrap();
assert!(patterns.is_empty());
}
#[test]
fn test_persona_behavior_learner_new() {
let config = LearningConfig::default();
let learner = PersonaBehaviorLearner::new(config);
assert!(learner.is_ok());
}
#[test]
fn test_persona_behavior_learner_record_event_disabled() {
let config = LearningConfig {
enabled: false,
..Default::default()
};
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
learner.record_event(
"persona-1".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/test".to_string(),
method: "GET".to_string(),
},
data: HashMap::new(),
},
);
assert!(learner.get_behavior_history("persona-1").is_none());
}
#[test]
fn test_persona_behavior_learner_record_event_persona_disabled() {
let mut config = LearningConfig {
enabled: true,
..Default::default()
};
config.persona_learning.insert("persona-1".to_string(), false);
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
learner.record_event(
"persona-1".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/test".to_string(),
method: "GET".to_string(),
},
data: HashMap::new(),
},
);
assert!(learner.get_behavior_history("persona-1").is_none());
}
#[tokio::test]
async fn test_persona_behavior_learner() {
let config = LearningConfig {
enabled: true,
persona_adaptation: true,
..Default::default()
};
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
learner.record_event(
"persona-1".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::RequestFailed {
endpoint: "/api/checkout".to_string(),
status_code: 500,
},
data: HashMap::new(),
},
);
learner.record_event(
"persona-1".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/checkout".to_string(),
method: "POST".to_string(),
},
data: HashMap::new(),
},
);
let pattern = learner.analyze_persona_behavior("persona-1").await.unwrap();
assert!(pattern.is_none()); }
#[tokio::test]
async fn test_persona_behavior_learner_get_behavior_history() {
let config = LearningConfig {
enabled: true,
..Default::default()
};
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
learner.record_event(
"persona-test".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/users".to_string(),
method: "GET".to_string(),
},
data: HashMap::new(),
},
);
let history = learner.get_behavior_history("persona-test");
assert!(history.is_some());
assert_eq!(history.unwrap().len(), 1);
}
#[tokio::test]
async fn test_persona_behavior_learner_analyze_nonexistent_persona() {
let config = LearningConfig {
enabled: true,
..Default::default()
};
let learner = PersonaBehaviorLearner::new(config).unwrap();
let pattern = learner.analyze_persona_behavior("nonexistent").await.unwrap();
assert!(pattern.is_none());
}
#[tokio::test]
async fn test_persona_behavior_learner_analyze_disabled() {
let config = LearningConfig {
enabled: false,
..Default::default()
};
let learner = PersonaBehaviorLearner::new(config).unwrap();
let pattern = learner.analyze_persona_behavior("any").await.unwrap();
assert!(pattern.is_none());
}
#[test]
fn test_persona_behavior_learner_event_limit() {
let config = LearningConfig {
enabled: true,
..Default::default()
};
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
for i in 0..1050 {
learner.record_event(
"persona-limit".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: format!("/api/test/{}", i),
method: "GET".to_string(),
},
data: HashMap::new(),
},
);
}
let history = learner.get_behavior_history("persona-limit").unwrap();
assert_eq!(history.len(), 1000); }
#[test]
fn test_behavior_event_creation() {
let event = BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/test".to_string(),
method: "POST".to_string(),
},
data: HashMap::new(),
};
assert!(event.data.is_empty());
}
#[test]
fn test_behavior_event_clone() {
let event = BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::PatternDetected {
pattern: "test-pattern".to_string(),
},
data: HashMap::new(),
};
let cloned = event.clone();
if let BehaviorEventType::PatternDetected { pattern } = cloned.event_type {
assert_eq!(pattern, "test-pattern");
} else {
panic!("Wrong event type after clone");
}
}
#[test]
fn test_behavior_event_debug() {
let event = BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::RequestSucceededAfterFailure {
endpoint: "/api/retry".to_string(),
},
data: HashMap::new(),
};
let debug_str = format!("{:?}", event);
assert!(debug_str.contains("RequestSucceededAfterFailure"));
}
#[test]
fn test_behavior_event_type_request() {
let event_type = BehaviorEventType::Request {
endpoint: "/api/users".to_string(),
method: "GET".to_string(),
};
if let BehaviorEventType::Request { endpoint, method } = event_type {
assert_eq!(endpoint, "/api/users");
assert_eq!(method, "GET");
}
}
#[test]
fn test_behavior_event_type_request_failed() {
let event_type = BehaviorEventType::RequestFailed {
endpoint: "/api/orders".to_string(),
status_code: 500,
};
if let BehaviorEventType::RequestFailed {
endpoint,
status_code,
} = event_type
{
assert_eq!(endpoint, "/api/orders");
assert_eq!(status_code, 500);
}
}
#[test]
fn test_behavior_event_type_eq() {
let a = BehaviorEventType::PatternDetected {
pattern: "a".to_string(),
};
let b = BehaviorEventType::PatternDetected {
pattern: "a".to_string(),
};
assert_eq!(a, b);
}
#[test]
fn test_behavior_event_type_clone() {
let event_type = BehaviorEventType::RequestFailed {
endpoint: "/test".to_string(),
status_code: 404,
};
let cloned = event_type.clone();
assert_eq!(cloned, event_type);
}
#[test]
fn test_behavior_event_type_debug() {
let event_type = BehaviorEventType::Request {
endpoint: "/debug".to_string(),
method: "DELETE".to_string(),
};
let debug_str = format!("{:?}", event_type);
assert!(debug_str.contains("Request"));
assert!(debug_str.contains("/debug"));
}
#[tokio::test]
async fn test_full_learning_workflow() {
let drift_config = DataDriftConfig::new();
let learning_config = LearningConfig {
enabled: true,
min_samples: 2, ..Default::default()
};
let engine = DriftLearningEngine::new(drift_config, learning_config).unwrap();
let data = serde_json::json!({
"user_id": "123",
"amount": 100.0,
"status": "pending"
});
let result = engine.apply_drift_with_learning(data).await.unwrap();
assert!(result.is_object());
}
#[tokio::test]
async fn test_persona_behavior_pattern_detection() {
let config = LearningConfig {
enabled: true,
min_samples: 5,
..Default::default()
};
let mut learner = PersonaBehaviorLearner::new(config).unwrap();
for _ in 0..10 {
learner.record_event(
"retry-persona".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::RequestFailed {
endpoint: "/api/payment".to_string(),
status_code: 503,
},
data: HashMap::new(),
},
);
learner.record_event(
"retry-persona".to_string(),
BehaviorEvent {
timestamp: chrono::Utc::now(),
event_type: BehaviorEventType::Request {
endpoint: "/api/checkout".to_string(),
method: "POST".to_string(),
},
data: HashMap::new(),
},
);
}
let pattern = learner.analyze_persona_behavior("retry-persona").await.unwrap();
assert!(pattern.is_some());
let p = pattern.unwrap();
assert_eq!(p.pattern_type, PatternType::PersonaBehavior);
assert!(p.confidence > 0.0);
}
}