aicx 0.6.6

Operator CLI + MCP server: canonical corpus first, optional semantic index second (Claude Code, Codex, Gemini)
Documentation
use super::*;
use chrono::TimeZone;

#[test]
fn test_conversation_first_excludes_reasoning() {
    let entries = vec![
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 0, 0).unwrap(),
            agent: "claude".to_string(),
            session_id: "sess1".to_string(),
            role: "user".to_string(),
            message: "Fix the auth middleware".to_string(),
            branch: Some("main".to_string()),
            cwd: Some("/home/user/myrepo".to_string()),
            frame_kind: None,
        },
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 0, 30).unwrap(),
            agent: "claude".to_string(),
            session_id: "sess1".to_string(),
            role: "assistant".to_string(),
            message: "I'll refactor the auth module to use JWT tokens.".to_string(),
            branch: Some("main".to_string()),
            cwd: Some("/home/user/myrepo".to_string()),
            frame_kind: None,
        },
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 1, 0).unwrap(),
            agent: "codex".to_string(),
            session_id: "sess2".to_string(),
            role: "reasoning".to_string(),
            message: "Thinking about the best approach...".to_string(),
            branch: None,
            cwd: Some("/home/user/myrepo".to_string()),
            frame_kind: None,
        },
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 1, 30).unwrap(),
            agent: "gemini".to_string(),
            session_id: "sess3".to_string(),
            role: "reasoning".to_string(),
            message: "**Analysis**: Checking dependencies".to_string(),
            branch: None,
            cwd: Some("/home/user/myrepo".to_string()),
            frame_kind: None,
        },
    ];

    let conv = to_conversation(&entries, &[]);
    assert_eq!(conv.len(), 2);
    assert_eq!(conv[0].role, "user");
    assert_eq!(conv[0].message, "Fix the auth middleware");
    assert_eq!(conv[1].role, "assistant");
    assert_eq!(
        conv[1].message,
        "I'll refactor the auth module to use JWT tokens."
    );
    assert!(conv.iter().all(|m| m.role != "reasoning"));
}

#[test]
fn test_conversation_first_preserves_full_messages() {
    let long_msg = "A".repeat(50_000);
    let entries = vec![TimelineEntry {
        timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 0, 0).unwrap(),
        agent: "claude".to_string(),
        session_id: "sess1".to_string(),
        role: "user".to_string(),
        message: long_msg.clone(),
        branch: None,
        cwd: None,
        frame_kind: None,
    }];

    let conv = to_conversation(&entries, &[]);
    assert_eq!(conv.len(), 1);
    assert_eq!(conv[0].message.len(), 50_000);
    assert_eq!(conv[0].message, long_msg);
}

#[test]
fn test_conversation_first_repo_project_identity() {
    let entries = vec![
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 0, 0).unwrap(),
            agent: "claude".to_string(),
            session_id: "sess1".to_string(),
            role: "user".to_string(),
            message: "hello".to_string(),
            branch: None,
            cwd: Some("/Users/maciejgad/hosted/VetCoders/ai-contexters".to_string()),
            frame_kind: None,
        },
        TimelineEntry {
            timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 10, 1, 0).unwrap(),
            agent: "codex".to_string(),
            session_id: "sess2".to_string(),
            role: "assistant".to_string(),
            message: "world".to_string(),
            branch: None,
            cwd: None,
            frame_kind: None,
        },
    ];

    let conv = to_conversation(&entries, &["ai-contexters".to_string()]);
    assert_eq!(conv[0].repo_project, "ai-contexters");
    assert_eq!(conv[1].repo_project, "ai-contexters");
    assert_eq!(
        conv[0].source_path.as_deref(),
        Some("/Users/maciejgad/hosted/VetCoders/ai-contexters")
    );
    assert!(conv[1].source_path.is_none());
}

#[test]
fn test_conversation_first_preserves_provenance() {
    let entries = vec![TimelineEntry {
        timestamp: Utc.with_ymd_and_hms(2026, 3, 21, 14, 30, 0).unwrap(),
        agent: "claude".to_string(),
        session_id: "abc12345-6789-session-uuid".to_string(),
        role: "user".to_string(),
        message: "Deploy to production".to_string(),
        branch: Some("release/v2".to_string()),
        cwd: Some("/home/user/project".to_string()),
        frame_kind: None,
    }];

    let conv = to_conversation(&entries, &[]);
    assert_eq!(conv.len(), 1);
    let msg = &conv[0];
    assert_eq!(msg.session_id, "abc12345-6789-session-uuid");
    assert_eq!(msg.agent, "claude");
    assert_eq!(msg.branch.as_deref(), Some("release/v2"));
    assert_eq!(
        msg.timestamp,
        Utc.with_ymd_and_hms(2026, 3, 21, 14, 30, 0).unwrap()
    );
}

#[test]
fn test_extract_claude_excludes_tool_blocks_then_conversation_clean() {
    use std::fs;
    let tmp = std::env::temp_dir().join(format!(
        "ai-ctx-conv-tool-blocks-{}-{}.jsonl",
        std::process::id(),
        Utc::now().timestamp_nanos_opt().unwrap_or_default()
    ));
    let _ = fs::remove_file(&tmp);

    let content = concat!(
        r#"{"type":"user","message":{"role":"user","content":"Hello agent"},"timestamp":"2026-03-21T10:00:00.000Z","sessionId":"s1","cwd":"/tmp"}"#,
        "\n",
        r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Let me check."},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"ls"}},{"type":"text","text":"Here are the files."}]},"timestamp":"2026-03-21T10:00:01.000Z","sessionId":"s1"}"#,
        "\n",
        r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"ok"}]},"timestamp":"2026-03-21T10:00:02.000Z","sessionId":"s1"}"#
    );
    fs::write(&tmp, content).unwrap();

    let cutoff = Utc.timestamp_opt(0, 0).single().unwrap();
    let config = ExtractionConfig {
        project_filter: vec![],
        cutoff,
        include_assistant: true,
        watermark: None,
    };

    let entries = extract_claude_file(&tmp, &config).unwrap();
    assert!(
        entries.len() >= 2,
        "expected at least user + assistant entries, got {}",
        entries.len()
    );
    let user_msgs: Vec<_> = entries
        .iter()
        .filter(|e| e.frame_kind == Some(FrameKind::UserMsg))
        .collect();
    let agent_msgs: Vec<_> = entries
        .iter()
        .filter(|e| e.frame_kind == Some(FrameKind::AgentReply))
        .collect();
    assert!(!user_msgs.is_empty());
    assert!(!agent_msgs.is_empty());
    assert_eq!(user_msgs[0].message, "Hello agent");
    assert!(
        agent_msgs
            .iter()
            .any(|e| e.message.contains("Let me check"))
    );

    let conv = to_conversation(&entries, &[]);
    assert!(
        conv.len() >= 2,
        "conversation should have at least user + assistant, got {}",
        conv.len()
    );
    assert_eq!(conv[0].message, "Hello agent");

    let _ = fs::remove_file(&tmp);
}