debtmap 0.20.0

Code complexity and technical debt analyzer
Documentation
//! End-to-End Integration Tests for Multi-Signal Classification
//!
//! Tests the complete workflow from code input to responsibility classification.

use debtmap::analysis::io_detection::Language;
use debtmap::analysis::multi_signal_aggregation::{
    AggregationConfig, ResponsibilityAggregator, ResponsibilityCategory, SignalSet,
};

#[test]
fn test_end_to_end_file_io_classification() {
    let code = r#"
fn read_user_profile(user_id: &str) -> Result<Profile> {
    let path = format!("profiles/{}.json", user_id);
    let contents = fs::read_to_string(&path)?;
    serde_json::from_str(&contents)
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        io_signal: aggregator.collect_io_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("read_user_profile")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    assert_eq!(result.primary, ResponsibilityCategory::FileIO);
    // With only I/O (35%) and name (10%) signals, max realistic confidence is ~0.45
    assert!(
        result.confidence > 0.30,
        "Expected confidence > 0.30, got {}",
        result.confidence
    );
    assert!(!result.evidence.is_empty());
}

#[test]
fn test_end_to_end_pure_computation() {
    let code = r#"
fn calculate_fibonacci(n: u64) -> u64 {
    if n <= 1 {
        n
    } else {
        calculate_fibonacci(n - 1) + calculate_fibonacci(n - 2)
    }
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        purity_signal: aggregator.collect_purity_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("calculate_fibonacci")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    // Should detect as pure computation due to "calculate" prefix
    assert_eq!(result.primary, ResponsibilityCategory::PureComputation);
    // With only purity (5%) and name (10%) signals, expect low confidence
    assert!(
        result.confidence > 0.10,
        "Expected confidence > 0.10, got {}",
        result.confidence
    );
}

#[test]
fn test_end_to_end_http_handler() {
    let code = r#"
async fn handle_create_user(
    Json(payload): Json<CreateUser>,
    State(db): State<Database>,
) -> Result<Json<User>, StatusCode> {
    let user = db.create_user(payload).await?;
    Ok(Json(user))
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        io_signal: aggregator.collect_io_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("handle_create_user")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    // This is an edge case: the snippet lacks imports and framework annotations that would
    // normally trigger framework pattern detection. Without that context, name-based classification
    // may not match "handle_create_user" to HTTP patterns, resulting in Unknown.
    // In real code with proper imports (e.g., `use axum::Json`), framework detection would succeed.
    assert!(
        result.primary == ResponsibilityCategory::HttpRequestHandler
            || result.primary == ResponsibilityCategory::DatabaseIO
            || result.primary == ResponsibilityCategory::Orchestration
            || result.primary == ResponsibilityCategory::Unknown,
        "Expected HTTP Handler, Database I/O, Orchestration, or Unknown (for minimal context), got {:?}",
        result.primary
    );
}

#[test]
fn test_weighted_aggregation_prefers_io() {
    let code = r#"
fn process_transactions(file_path: &Path) -> Result<Summary> {
    let transactions = read_from_csv(file_path)?;
    let total = transactions.iter().map(|t| t.amount).sum();
    Summary { total }
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    // Both I/O and purity signals present
    let signals = SignalSet {
        io_signal: aggregator.collect_io_signal(code, Language::Rust),
        purity_signal: aggregator.collect_purity_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("process_transactions")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    // I/O signal should dominate due to higher weight (35% vs 5% for purity)
    assert!(
        result.primary == ResponsibilityCategory::FileIO
            || result.primary == ResponsibilityCategory::Orchestration
    );
}

#[test]
fn test_evidence_collection() {
    let code = r#"
fn validate_email_format(email: &str) -> bool {
    let re = Regex::new(r"^[^@]+@[^@]+\.[^@]+$").unwrap();
    re.is_match(email)
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        purity_signal: aggregator.collect_purity_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("validate_email_format")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    // Should classify as validation or formatting (both reasonable for email format checking)
    // Name heuristics may interpret "format" differently than "validate"
    assert!(
        result.primary == ResponsibilityCategory::Validation
            || result.primary == ResponsibilityCategory::Formatting,
        "Expected Validation or Formatting, got {:?}",
        result.primary
    );

    // Check that evidence is collected
    assert!(!result.evidence.is_empty(), "Evidence should not be empty");

    // At least one evidence should mention "Name pattern" or similar
    let has_name_evidence = result
        .evidence
        .iter()
        .any(|e| e.description.contains("Name pattern"));
    assert!(
        has_name_evidence,
        "Expected name-based evidence in {:?}",
        result.evidence
    );
}

#[test]
fn test_alternative_classifications() {
    let code = r#"
fn transform_user_data(user: User) -> UserDTO {
    UserDTO {
        id: user.id,
        name: format!("{} {}", user.first, user.last),
    }
}
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        purity_signal: aggregator.collect_purity_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("transform_user_data")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    assert_eq!(result.primary, ResponsibilityCategory::Transformation);

    // Should have some alternative classifications
    assert!(!result.alternatives.is_empty());
}

#[test]
fn test_custom_config() {
    let code = r#"
fn calculate_discount(price: f64, rate: f64) -> f64 {
    price * (1.0 - rate)
}
"#;

    // Create custom config with different weights
    let mut config = AggregationConfig::default();
    config.weights.name_heuristics = 0.30;
    config.weights.purity_side_effects = 0.20;
    config.weights.io_detection = 0.30;
    config.weights.call_graph = 0.10;
    config.weights.type_signatures = 0.05;
    config.weights.framework_patterns = 0.05;

    let aggregator = ResponsibilityAggregator::with_config(config);

    let signals = SignalSet {
        purity_signal: aggregator.collect_purity_signal(code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("calculate_discount")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    assert_eq!(result.primary, ResponsibilityCategory::PureComputation);
}

#[test]
fn test_multi_language_support() {
    // Python code
    let python_code = r#"
def fetch_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()
"#;

    let aggregator = ResponsibilityAggregator::new();

    let signals = SignalSet {
        io_signal: aggregator.collect_io_signal(python_code, Language::Python),
        name_signal: Some(aggregator.collect_name_signal("fetch_user_data")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    assert_eq!(result.primary, ResponsibilityCategory::NetworkIO);

    // Rust code
    let rust_code = r#"
fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let url = format!("https://api.example.com/users/{}", user_id);
    reqwest::get(&url)?.json()
}
"#;

    let signals = SignalSet {
        io_signal: aggregator.collect_io_signal(rust_code, Language::Rust),
        name_signal: Some(aggregator.collect_name_signal("fetch_user_data")),
        ..Default::default()
    };

    let result = aggregator.aggregate(&signals);

    assert_eq!(result.primary, ResponsibilityCategory::NetworkIO);
}