do-memory-core 0.1.34

Core episodic learning system for AI agents with pattern extraction, reward scoring, and dual storage backend
Documentation
//! Tests for pattern extraction

use super::*;
use crate::{
    ComplexityLevel, Episode, ExecutionResult, ExecutionStep, OutcomeStats, TaskContext,
    TaskOutcome, TaskType,
    patterns::{Pattern, PatternEffectiveness},
};
use chrono::Duration;

#[test]
fn test_pattern_extractor_creation() {
    let extractor = PatternExtractor::new();
    assert_eq!(extractor.success_threshold, MIN_PATTERN_SUCCESS_RATE);
    assert_eq!(extractor.max_sequence_len, MAX_SEQUENCE_LENGTH);
}

#[test]
fn test_extract_from_incomplete_episode() {
    let extractor = PatternExtractor::new();
    let context = TaskContext::default();
    let episode = Episode::new("Test task".to_string(), context, TaskType::Testing);

    let patterns = extractor.extract(&episode);
    assert_eq!(
        patterns.len(),
        0,
        "Should not extract from incomplete episode"
    );
}

#[test]
fn test_extract_from_complete_episode() {
    let extractor = PatternExtractor::new();
    let context = TaskContext::default();
    let mut episode = Episode::new("Test task".to_string(), context, TaskType::Testing);

    // Add some execution steps
    for i in 0..3 {
        let mut step = ExecutionStep::new(i + 1, format!("tool_{i}"), "Action".to_string());
        step.result = Some(ExecutionResult::Success {
            output: format!("Step {i} completed"),
        });
        episode.add_step(step);
    }

    episode.complete(TaskOutcome::Success {
        verdict: "Done".to_string(),
        artifacts: vec![],
    });

    let patterns = extractor.extract(&episode);
    // Episode has 3 steps with distinct tools, so a ToolSequence pattern should be extracted.
    assert!(
        patterns
            .iter()
            .any(|p| matches!(p, Pattern::ToolSequence { tools, .. } if tools.len() == 3)),
        "expected ToolSequence pattern with 3 tools, got {patterns:?}",
    );
}

#[cfg(test)]
mod utils_tests {
    use super::*;

    #[test]
    fn test_deduplicate_patterns_no_duplicates() {
        let patterns = vec![
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool1".to_string(), "tool2".to_string()],
                context: TaskContext::default(),
                success_rate: 0.9,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool3".to_string(), "tool4".to_string()],
                context: TaskContext::default(),
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(200),
                occurrence_count: 3,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        let deduplicated = super::utils::deduplicate_patterns(patterns.clone());
        assert_eq!(deduplicated.len(), 2);
    }

    #[test]
    fn test_deduplicate_patterns_with_duplicates() {
        let pattern1 = Pattern::ToolSequence {
            id: uuid::Uuid::new_v4(),
            tools: vec!["tool1".to_string(), "tool2".to_string()],
            context: TaskContext::default(),
            success_rate: 0.9,
            avg_latency: Duration::milliseconds(100),
            occurrence_count: 5,
            effectiveness: PatternEffectiveness::default(),
        };

        let pattern2 = Pattern::ToolSequence {
            id: uuid::Uuid::new_v4(),
            tools: vec!["tool1".to_string(), "tool2".to_string()], // Same tools
            context: TaskContext::default(),                       // Same context
            success_rate: 0.8,                                     // Different success rate
            avg_latency: Duration::milliseconds(200),              // Different latency
            occurrence_count: 3,                                   // Different count
            effectiveness: PatternEffectiveness::default(),
        };

        let extracted_patterns = vec![pattern1, pattern2];
        let deduplicated = super::utils::deduplicate_patterns(extracted_patterns);
        assert_eq!(deduplicated.len(), 1);
    }

    #[test]
    fn test_rank_patterns_by_success_rate() {
        let patterns = vec![
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool1".to_string()],
                context: TaskContext::default(),
                success_rate: 0.5,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool2".to_string()],
                context: TaskContext::default(),
                success_rate: 0.9,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        let context = TaskContext::default();
        let ranked = super::utils::rank_patterns(patterns, &context);

        assert_eq!(ranked.len(), 2);
        // Higher success rate should come first
        match &ranked[0] {
            Pattern::ToolSequence { success_rate, .. } => {
                assert!((*success_rate - 0.9).abs() < 0.01);
            }
            _ => panic!("Expected ToolSequence"),
        }
    }

    #[test]
    fn test_rank_patterns_by_context_relevance() {
        let context1 = TaskContext {
            language: Some("rust".to_string()),
            domain: "web-api".to_string(),
            ..Default::default()
        };

        let context2 = TaskContext {
            language: Some("python".to_string()),
            domain: "data-science".to_string(),
            ..Default::default()
        };

        let patterns = vec![
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool1".to_string()],
                context: context1,
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool2".to_string()],
                context: context2,
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        let query_context = TaskContext {
            language: Some("rust".to_string()),
            domain: "web-api".to_string(),
            ..Default::default()
        };

        let ranked = super::utils::rank_patterns(patterns, &query_context);

        assert_eq!(ranked.len(), 2);
        // Pattern with matching context should come first
        match &ranked[0] {
            Pattern::ToolSequence { context, .. } => {
                assert_eq!(context.language, Some("rust".to_string()));
                assert_eq!(context.domain, "web-api");
            }
            _ => panic!("Expected ToolSequence"),
        }
    }

    #[test]
    fn test_rank_patterns_empty() {
        let patterns: Vec<Pattern> = vec![];
        let context = TaskContext::default();
        let ranked = super::utils::rank_patterns(patterns, &context);
        assert_eq!(ranked.len(), 0);
    }

    #[test]
    fn test_rank_patterns_with_tags() {
        let patterns = vec![
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool1".to_string()],
                context: TaskContext {
                    tags: vec!["async".to_string(), "performance".to_string()],
                    ..Default::default()
                },
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool2".to_string()],
                context: TaskContext {
                    tags: vec!["other".to_string()],
                    ..Default::default()
                },
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        let query_context = TaskContext {
            tags: vec!["async".to_string()],
            ..Default::default()
        };

        let ranked = super::utils::rank_patterns(patterns, &query_context);

        assert_eq!(ranked.len(), 2);
        // Pattern with matching tags should come first
        match &ranked[0] {
            Pattern::ToolSequence { context, .. } => {
                assert!(context.tags.contains(&"async".to_string()));
            }
            _ => panic!("Expected ToolSequence"),
        }
    }

    #[test]
    fn test_rank_patterns_comprehensive() {
        let context = TaskContext {
            language: Some("rust".to_string()),
            framework: Some("tokio".to_string()),
            complexity: ComplexityLevel::Moderate,
            domain: "testing".to_string(),
            tags: vec!["async".to_string()],
        };

        let patterns = vec![
            Pattern::DecisionPoint {
                id: uuid::Uuid::new_v4(),
                condition: "cond".to_string(),
                action: "act".to_string(),
                outcome_stats: OutcomeStats {
                    total_count: 10,
                    success_count: 8,
                    failure_count: 2,
                    avg_duration_secs: 1.0,
                },
                context: context.clone(),
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ErrorRecovery {
                id: uuid::Uuid::new_v4(),
                error_type: "err".to_string(),
                recovery_steps: vec!["step".to_string()],
                success_rate: 0.9,
                context: context.clone(),
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ContextPattern {
                id: uuid::Uuid::new_v4(),
                context_features: vec!["feat".to_string()],
                recommended_approach: "approach".to_string(),
                evidence: vec![uuid::Uuid::new_v4()],
                success_rate: 0.7,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        let ranked = super::utils::rank_patterns(patterns, &context);
        assert_eq!(ranked.len(), 3);
    }

    #[test]
    fn test_rank_patterns_negative_delta() {
        let effectiveness = PatternEffectiveness {
            avg_reward_delta: -0.1,
            times_applied: 1,
            ..Default::default()
        };

        let patterns = vec![Pattern::ToolSequence {
            id: uuid::Uuid::new_v4(),
            tools: vec!["tool1".to_string()],
            context: TaskContext::default(),
            success_rate: 0.8,
            avg_latency: Duration::milliseconds(100),
            occurrence_count: 5,
            effectiveness,
        }];

        let ranked = super::utils::rank_patterns(patterns, &TaskContext::default());
        assert_eq!(ranked.len(), 1);
    }

    #[test]
    fn test_rank_patterns_coverage_gap() {
        // Test case for duplicate tools and tags to cover optimized paths
        let patterns = vec![
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool1".to_string(), "tool1".to_string()], // Duplicate tool
                context: TaskContext {
                    tags: vec!["async".to_string(), "async".to_string()], // Duplicate tag
                    ..Default::default()
                },
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
            Pattern::ToolSequence {
                id: uuid::Uuid::new_v4(),
                tools: vec!["tool2".to_string()],
                context: TaskContext {
                    tags: vec![], // Empty tags
                    ..Default::default()
                },
                success_rate: 0.8,
                avg_latency: Duration::milliseconds(100),
                occurrence_count: 5,
                effectiveness: PatternEffectiveness::default(),
            },
        ];

        // 1. Query with non-empty tags against pattern with empty tags
        let query_context_with_tags = TaskContext {
            tags: vec!["async".to_string()],
            ..Default::default()
        };
        let ranked1 = super::utils::rank_patterns(patterns.clone(), &query_context_with_tags);
        assert_eq!(ranked1.len(), 2);

        // 2. Query with empty tags against pattern with non-empty tags
        let query_context_empty_tags = TaskContext::default();
        let ranked2 = super::utils::rank_patterns(patterns, &query_context_empty_tags);
        assert_eq!(ranked2.len(), 2);
    }
}