car-agents 0.15.2

Built-in commodity agents for Common Agent Runtime
Documentation
//! Integration tests for built-in agents — requires API keys.
//! Run with: cargo test -p car-agents --test integration -- --ignored --nocapture

use car_agents::*;
use car_agents::{coordinator, planner, researcher, summarizer, verifier};
use car_inference::{InferenceConfig, InferenceEngine};
use std::sync::Arc;

/// Use gpt-4.1-mini for tests — fast, cheap, remote (no local model loading).
const TEST_MODEL: &str = "gpt-4.1-mini";

fn make_ctx() -> AgentContext {
    // Load .env from repo root
    let mut dir = std::env::current_dir().ok();
    while let Some(d) = dir {
        let env_file = d.join(".env");
        if env_file.exists() {
            if let Ok(content) = std::fs::read_to_string(&env_file) {
                for line in content.lines() {
                    let line = line.trim();
                    if line.is_empty() || line.starts_with('#') {
                        continue;
                    }
                    if let Some((k, v)) = line.split_once('=') {
                        if std::env::var(k.trim()).is_err() {
                            std::env::set_var(k.trim(), v.trim());
                        }
                    }
                }
            }
            break;
        }
        dir = d.parent().map(|p| p.to_path_buf());
    }

    let engine = InferenceEngine::new(InferenceConfig::default());
    AgentContext {
        inference: Arc::new(engine),
    }
}

#[tokio::test]
#[ignore] // requires API keys
async fn test_researcher() {
    let ctx = make_ctx();
    let r = Researcher::with_config(
        ctx,
        researcher::ResearchConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let result = r.research("What is Rust's ownership model?", None).await;
    println!(
        "Researcher: {} (model: {}, {}ms)",
        &result.output[..100.min(result.output.len())],
        result.model_used,
        result.latency_ms
    );
    assert!(!result.output.is_empty());
    assert!(result.confidence > 0.0);
}

#[tokio::test]
#[ignore]
async fn test_planner() {
    let ctx = make_ctx();
    let p = PlannerAgent::with_config(
        ctx,
        planner::PlanConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let result = p
        .plan("Deploy a Rust web service to production", None)
        .await;
    println!(
        "Planner: {} (model: {}, {}ms)",
        &result.output[..100.min(result.output.len())],
        result.model_used,
        result.latency_ms
    );
    assert!(!result.output.is_empty());
    assert!(result.confidence > 0.0);
}

#[tokio::test]
#[ignore]
async fn test_verifier() {
    let ctx = make_ctx();
    let v = Verifier::with_config(
        ctx,
        verifier::VerifyConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let result = v
        .verify(
            "fn add(a: i32, b: i32) -> i32 { a + b }",
            "Function must add two integers and return the sum",
        )
        .await;
    println!(
        "Verifier: {} (model: {}, {}ms)",
        &result.output[..100.min(result.output.len())],
        result.model_used,
        result.latency_ms
    );
    assert!(result.output.contains("PASS") || result.output.contains("FAIL"));
}

#[tokio::test]
#[ignore]
async fn test_summarizer() {
    let ctx = make_ctx();
    let s = Summarizer::with_config(
        ctx,
        summarizer::SummaryConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let long_text =
        "The Common Agent Runtime (CAR) is a deterministic execution layer for AI agents. "
            .repeat(50);
    let result = s.summarize(&long_text, Some("architecture")).await;
    println!(
        "Summarizer: {} (model: {}, {}ms, compression: {:.0}%)",
        &result.output[..100.min(result.output.len())],
        result.model_used,
        result.latency_ms,
        (1.0 - result.output.len() as f64 / long_text.len() as f64) * 100.0
    );
    assert!(result.output.len() < long_text.len());
}

#[tokio::test]
#[ignore]
async fn test_coordinator() {
    let ctx = make_ctx();
    let c = Coordinator::with_config(
        ctx,
        coordinator::CoordinatorConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let (plan, result) = c
        .coordinate("Review a pull request for security vulnerabilities")
        .await;
    println!(
        "Coordinator: pattern={:?}, agents={:?} (model: {}, {}ms)",
        plan.pattern, plan.agents, result.model_used, result.latency_ms
    );
    assert!(!plan.agents.is_empty());
    assert!(!plan.reasoning.is_empty());
}

#[tokio::test]
#[ignore]
async fn test_full_pipeline() {
    let ctx = make_ctx();

    // Step 1: Coordinator decides how to handle the goal
    let c = Coordinator::with_config(
        ctx.clone(),
        coordinator::CoordinatorConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let (plan, coord_result) = c.coordinate("Explain how memory works in CAR").await;
    println!("Coordinator: {:?}{:?}", plan.pattern, plan.agents);

    // Step 2: Researcher gathers information
    let r = Researcher::with_config(
        ctx.clone(),
        researcher::ResearchConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let research = r
        .research("How does memory work in the Common Agent Runtime?", None)
        .await;
    println!(
        "Researcher: {}... ({}ms)",
        &research.output[..80.min(research.output.len())],
        research.latency_ms
    );

    // Step 3: Summarizer compresses for output
    let s = Summarizer::with_config(
        ctx.clone(),
        summarizer::SummaryConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let summary = s
        .summarize(&research.output, Some("key architecture decisions"))
        .await;
    println!(
        "Summarizer: {}... ({}ms)",
        &summary.output[..80.min(summary.output.len())],
        summary.latency_ms
    );

    // Step 4: Verifier checks the summary
    let v = Verifier::with_config(
        ctx.clone(),
        verifier::VerifyConfig {
            model: Some(TEST_MODEL.into()),
            ..Default::default()
        },
    );
    let check = v
        .verify(
            &summary.output,
            "Must explain graph-based memory, spreading activation, and context assembly",
        )
        .await;
    println!(
        "Verifier: {} ({}ms)",
        if check.output.contains("PASS") {
            "PASS"
        } else {
            "FAIL"
        },
        check.latency_ms
    );

    // All agents should have produced output
    assert!(coord_result.confidence > 0.0);
    assert!(!research.output.is_empty());
    assert!(!summary.output.is_empty());
    assert!(!check.output.is_empty());

    let total_ms =
        coord_result.latency_ms + research.latency_ms + summary.latency_ms + check.latency_ms;
    println!("\nFull pipeline: {}ms total", total_ms);
}