codex-memory 3.0.15

A simple memory storage service with MCP interface for Claude Desktop
Documentation
use chrono::Utc;
use codex_memory::models::{Memory, SearchParams, SearchResult, SearchStrategy};
use uuid::Uuid;

#[test]
fn test_search_params_default() {
    let params = SearchParams::default();

    assert_eq!(params.query, "");
    assert_eq!(params.tag_filter, None);
    assert!(params.use_tag_embedding);
    assert!(params.use_content_embedding);
    assert_eq!(params.similarity_threshold, 0.7);
    assert_eq!(params.max_results, 10);
    assert!(matches!(params.search_strategy, SearchStrategy::Hybrid));
    assert!(!params.boost_recent);
    assert_eq!(params.tag_weight, 0.4);
    assert_eq!(params.content_weight, 0.6);
}

#[test]
fn test_search_params_custom() {
    let params = SearchParams {
        query: "test query".to_string(),
        tag_filter: Some(vec!["tag1".to_string(), "tag2".to_string()]),
        use_tag_embedding: false,
        use_content_embedding: true,
        similarity_threshold: 0.8,
        max_results: 5,
        search_strategy: SearchStrategy::ContentFirst,
        boost_recent: true,
        tag_weight: 0.3,
        content_weight: 0.7,
    };

    assert_eq!(params.query, "test query");
    assert_eq!(
        params.tag_filter,
        Some(vec!["tag1".to_string(), "tag2".to_string()])
    );
    assert!(!params.use_tag_embedding);
    assert!(params.use_content_embedding);
    assert_eq!(params.similarity_threshold, 0.8);
    assert_eq!(params.max_results, 5);
    assert!(matches!(
        params.search_strategy,
        SearchStrategy::ContentFirst
    ));
    assert!(params.boost_recent);
    assert_eq!(params.tag_weight, 0.3);
    assert_eq!(params.content_weight, 0.7);
}

#[test]
fn test_search_strategy_variants() {
    // Test all strategy variants exist
    let strategies = [
        SearchStrategy::TagsFirst,
        SearchStrategy::ContentFirst,
        SearchStrategy::Hybrid,
    ];

    assert_eq!(strategies.len(), 3);
}

#[test]
fn test_search_strategy_serialization() {
    use serde_json;

    // Test serialization
    assert_eq!(
        serde_json::to_string(&SearchStrategy::TagsFirst).unwrap(),
        "\"TagsFirst\""
    );
    assert_eq!(
        serde_json::to_string(&SearchStrategy::ContentFirst).unwrap(),
        "\"ContentFirst\""
    );
    assert_eq!(
        serde_json::to_string(&SearchStrategy::Hybrid).unwrap(),
        "\"Hybrid\""
    );

    // Test deserialization
    assert!(matches!(
        serde_json::from_str::<SearchStrategy>("\"TagsFirst\"").unwrap(),
        SearchStrategy::TagsFirst
    ));
    assert!(matches!(
        serde_json::from_str::<SearchStrategy>("\"ContentFirst\"").unwrap(),
        SearchStrategy::ContentFirst
    ));
    assert!(matches!(
        serde_json::from_str::<SearchStrategy>("\"Hybrid\"").unwrap(),
        SearchStrategy::Hybrid
    ));
}

#[test]
fn test_search_result_scoring() {
    let memory = create_test_memory();

    // Test with both similarities
    let result = SearchResult::new(
        memory.clone(),
        Some(0.8), // tag similarity
        Some(0.9), // content similarity
        Some(1),   // semantic cluster
        0.4,       // tag weight
        0.6,       // content weight
    );

    assert_eq!(result.tag_similarity, Some(0.8));
    assert_eq!(result.content_similarity, Some(0.9));
    assert_eq!(result.semantic_cluster, Some(1));

    // Combined score = (0.8 * 0.4) + (0.9 * 0.6) = 0.32 + 0.54 = 0.86
    assert!((result.combined_score - 0.86).abs() < f64::EPSILON);
}

#[test]
fn test_search_result_scoring_tag_only() {
    let memory = create_test_memory();

    let result = SearchResult::new(
        memory,
        Some(0.8), // tag similarity
        None,      // no content similarity
        None,      // no semantic cluster
        0.4,       // tag weight
        0.6,       // content weight
    );

    assert_eq!(result.tag_similarity, Some(0.8));
    assert_eq!(result.content_similarity, None);
    assert_eq!(result.semantic_cluster, None);

    // Combined score = (0.8 * 0.4) + (0.0 * 0.6) = 0.32
    assert!((result.combined_score - 0.32).abs() < f64::EPSILON);
}

#[test]
fn test_search_result_scoring_content_only() {
    let memory = create_test_memory();

    let result = SearchResult::new(
        memory,
        None,      // no tag similarity
        Some(0.9), // content similarity
        None,      // no semantic cluster
        0.4,       // tag weight
        0.6,       // content weight
    );

    assert_eq!(result.tag_similarity, None);
    assert_eq!(result.content_similarity, Some(0.9));

    // Combined score = (0.0 * 0.4) + (0.9 * 0.6) = 0.54
    assert!((result.combined_score - 0.54).abs() < f64::EPSILON);
}

#[test]
fn test_search_result_scoring_no_similarities() {
    let memory = create_test_memory();

    let result = SearchResult::new(
        memory, None, // no tag similarity
        None, // no content similarity
        None, // no semantic cluster
        0.4,  // tag weight
        0.6,  // content weight
    );

    assert_eq!(result.tag_similarity, None);
    assert_eq!(result.content_similarity, None);

    // Combined score = (0.0 * 0.4) + (0.0 * 0.6) = 0.0
    assert!((result.combined_score - 0.0).abs() < f64::EPSILON);
}

#[test]
fn test_search_result_weight_boundaries() {
    let memory = create_test_memory();

    // Test with extreme weights
    let result = SearchResult::new(
        memory.clone(),
        Some(0.5),
        Some(0.8),
        None,
        0.0, // no tag weight
        1.0, // full content weight
    );

    // Combined score = (0.5 * 0.0) + (0.8 * 1.0) = 0.8
    assert!((result.combined_score - 0.8).abs() < f64::EPSILON);

    // Test opposite extreme
    let result2 = SearchResult::new(
        memory,
        Some(0.5),
        Some(0.8),
        None,
        1.0, // full tag weight
        0.0, // no content weight
    );

    // Combined score = (0.5 * 1.0) + (0.8 * 0.0) = 0.5
    assert!((result2.combined_score - 0.5).abs() < f64::EPSILON);
}

#[test]
fn test_search_params_serialization() {
    let params = SearchParams {
        query: "test query".to_string(),
        tag_filter: Some(vec!["tag1".to_string()]),
        use_tag_embedding: false,
        use_content_embedding: true,
        similarity_threshold: 0.8,
        max_results: 5,
        search_strategy: SearchStrategy::ContentFirst,
        boost_recent: true,
        tag_weight: 0.3,
        content_weight: 0.7,
    };

    // Test serialization
    let json = serde_json::to_string(&params).unwrap();
    assert!(json.contains("test query"));
    assert!(json.contains("ContentFirst"));

    // Test deserialization
    let deserialized: SearchParams = serde_json::from_str(&json).unwrap();
    assert_eq!(deserialized.query, params.query);
    assert_eq!(deserialized.tag_filter, params.tag_filter);
    assert_eq!(
        deserialized.similarity_threshold,
        params.similarity_threshold
    );
    assert_eq!(deserialized.max_results, params.max_results);
    assert!(matches!(
        deserialized.search_strategy,
        SearchStrategy::ContentFirst
    ));
}

#[test]
fn test_search_result_serialization() {
    let memory = create_test_memory();
    let result = SearchResult::new(memory, Some(0.8), Some(0.9), Some(2), 0.4, 0.6);

    // Test serialization
    let json = serde_json::to_string(&result).unwrap();
    assert!(json.contains("test content"));
    assert!(json.contains("0.8"));
    assert!(json.contains("0.9"));

    // Test deserialization
    let deserialized: SearchResult = serde_json::from_str(&json).unwrap();
    assert_eq!(deserialized.memory.content, "test content");
    assert_eq!(deserialized.tag_similarity, Some(0.8));
    assert_eq!(deserialized.content_similarity, Some(0.9));
    assert_eq!(deserialized.semantic_cluster, Some(2));
}

#[test]
fn test_search_params_clone() {
    let params = SearchParams {
        query: "test".to_string(),
        tag_filter: Some(vec!["tag".to_string()]),
        ..Default::default()
    };

    let cloned = params.clone();
    assert_eq!(params.query, cloned.query);
    assert_eq!(params.tag_filter, cloned.tag_filter);
    assert_eq!(params.similarity_threshold, cloned.similarity_threshold);
}

#[test]
fn test_search_result_clone() {
    let memory = create_test_memory();
    let result = SearchResult::new(memory, Some(0.8), Some(0.9), Some(1), 0.4, 0.6);

    let cloned = result.clone();
    assert_eq!(result.memory.id, cloned.memory.id);
    assert_eq!(result.tag_similarity, cloned.tag_similarity);
    assert_eq!(result.content_similarity, cloned.content_similarity);
    assert_eq!(result.combined_score, cloned.combined_score);
}

#[test]
fn test_search_params_debug() {
    let params = SearchParams::default();
    let debug_str = format!("{:?}", params);

    assert!(debug_str.contains("SearchParams"));
    assert!(debug_str.contains("similarity_threshold"));
    assert!(debug_str.contains("0.7"));
}

#[test]
fn test_search_result_debug() {
    let memory = create_test_memory();
    let result = SearchResult::new(memory, Some(0.8), Some(0.9), Some(1), 0.4, 0.6);
    let debug_str = format!("{:?}", result);

    assert!(debug_str.contains("SearchResult"));
    assert!(debug_str.contains("combined_score"));
    assert!(debug_str.contains("0.86"));
}

// Helper function to create test memory
fn create_test_memory() -> Memory {
    Memory {
        id: Uuid::new_v4(),
        content: "test content".to_string(),
        content_hash: "hash123".to_string(),
        tags: vec!["tag1".to_string(), "tag2".to_string()],
        context: "test context".to_string(),
        summary: "test summary".to_string(),
        chunk_index: None,
        total_chunks: None,
        parent_id: None,
        created_at: Utc::now(),
        updated_at: Utc::now(),
    }
}