debtmap 0.16.4

Code complexity and technical debt analyzer
Documentation
//! Integration tests for improved responsibility clustering (Spec 192)
//!
//! Validates that the new clustering module achieves <5% unclustered rate
//! and integrates properly with god object detection.

use debtmap::extraction::adapters::god_object::analyze_god_objects;
use debtmap::extraction::UnifiedFileExtractor;
use std::path::Path;

/// Test that clustering achieves <5% unclustered rate on god_object/detector.rs
///
/// Spec 192 requires: clustering should achieve ≤5% unclustered rate for large files
/// with 20+ methods to demonstrate effective behavioral decomposition.
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
fn test_clustering_on_god_object_detector() {
    let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
        .expect("Failed to read detector.rs");

    let path = Path::new("src/organization/god_object/detector.rs");
    let extracted = UnifiedFileExtractor::extract(path, &source_code)
        .expect("Failed to extract recommender.rs");
    let analyses = analyze_god_objects(path, &extracted);

    // If no god objects detected, the test passes
    if analyses.is_empty() {
        println!("No god objects detected - test passes");
        return;
    }

    let analysis = &analyses[0];

    // Count total methods in all splits
    let total_split_methods: usize = analysis
        .recommended_splits
        .iter()
        .map(|s| s.method_count)
        .sum();

    // If there are no recommended splits, the file might not be classified as a god object
    // In that case, the test passes (no unclustered methods)
    if total_split_methods == 0 {
        println!("No splits recommended - file not classified as god object");
        return;
    }

    // Count methods in "unclustered" or "utilities" categories (indicates poor clustering)
    let unclustered_methods: usize = analysis
        .recommended_splits
        .iter()
        .filter(|s| {
            let name_lower = s.suggested_name.to_lowercase();
            let resp_lower = s.responsibility.to_lowercase();
            name_lower.contains("utilities")
                || name_lower.contains("unclustered")
                || name_lower.contains("misc")
                || resp_lower.contains("utilities")
                || resp_lower.contains("unclustered")
                || resp_lower.contains("misc")
                || s.cluster_quality
                    .as_ref()
                    .map(|q| !q.is_acceptable())
                    .unwrap_or(false)
        })
        .map(|s| s.method_count)
        .sum();

    let unclustered_rate = if total_split_methods > 0 {
        (unclustered_methods as f64) / (total_split_methods as f64)
    } else {
        0.0
    };

    println!("Clustering results for god_object/recommender.rs:");
    println!("  Total methods in splits: {}", total_split_methods);
    println!("  Unclustered methods: {}", unclustered_methods);
    println!("  Unclustered rate: {:.1}%", unclustered_rate * 100.0);
    println!(
        "  Number of clusters: {}",
        analysis.recommended_splits.len()
    );

    // Print cluster quality details
    for split in &analysis.recommended_splits {
        if let Some(quality) = &split.cluster_quality {
            println!(
                "  - {} ({} methods): coherence={:.2}, separation={:.2}, silhouette={:.2}",
                split.suggested_name,
                split.method_count,
                quality.internal_coherence,
                quality.external_separation,
                quality.silhouette_score
            );
        } else {
            println!(
                "  - {} ({} methods): no quality metrics",
                split.suggested_name, split.method_count
            );
        }
    }

    // REQUIREMENT: <5% unclustered rate (Spec 192)
    // Only enforce this if splits were recommended
    if total_split_methods > 0 {
        assert!(
            unclustered_rate < 0.05,
            "Unclustered rate {:.1}% exceeds 5% threshold. \
             Expected high-quality clustering with coherent behavioral groups.",
            unclustered_rate * 100.0
        );
    }

    // REQUIREMENT: At least 2 distinct clusters (no single mega-cluster)
    // Only enforce if the file is actually a god object requiring splits
    if analysis.recommended_splits.len() == 1 {
        let split = &analysis.recommended_splits[0];
        // If there's only 1 cluster and it's "unclassified", the file might not be a god object
        let name_lower = split.suggested_name.to_lowercase();
        if name_lower.contains("unclassified") || name_lower.contains("module") {
            println!(
                "File not classified as god object (single unclassified cluster) - test passes"
            );
            return;
        }
    }

    // If we get here, we expect multiple clusters
    if total_split_methods > 0 {
        assert!(
            analysis.recommended_splits.len() >= 2,
            "Expected at least 2 coherent clusters for god object, found {}",
            analysis.recommended_splits.len()
        );
    }
}

/// Test that all clusters have acceptable quality metrics
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
fn test_cluster_quality_metrics() {
    let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
        .expect("Failed to read detector.rs");

    let path = Path::new("src/organization/god_object/detector.rs");
    let extracted = UnifiedFileExtractor::extract(path, &source_code)
        .expect("Failed to extract recommender.rs");
    let analyses = analyze_god_objects(path, &extracted);

    if analyses.is_empty() {
        println!("No god objects detected - skipping quality check");
        return;
    }

    let analysis = &analyses[0];

    if analysis.recommended_splits.is_empty() {
        println!("No splits recommended - skipping quality check");
        return;
    }

    // All clusters with quality metrics should have acceptable quality
    let clusters_with_quality: Vec<_> = analysis
        .recommended_splits
        .iter()
        .filter(|s| s.cluster_quality.is_some())
        .collect();

    if clusters_with_quality.is_empty() {
        println!("No clusters with quality metrics - may be using legacy clustering");
        return;
    }

    println!("\nCluster quality validation:");
    for split in &clusters_with_quality {
        if let Some(quality) = &split.cluster_quality {
            println!(
                "  {} ({} methods): {}",
                split.suggested_name,
                split.method_count,
                quality.quality_description()
            );

            // REQUIREMENT: Internal coherence > 0.5 (Spec 192)
            assert!(
                quality.internal_coherence > 0.5,
                "Cluster '{}' has low internal coherence: {:.2} (threshold: 0.5)",
                split.suggested_name,
                quality.internal_coherence
            );

            // REQUIREMENT: Silhouette score > 0.4 for good clusters (Spec 192)
            // Note: Some clusters may have 0.2-0.4 (fair) which is acceptable
            if quality.silhouette_score < 0.2 {
                panic!(
                    "Cluster '{}' has poor silhouette score: {:.2} (minimum: 0.2)",
                    split.suggested_name, quality.silhouette_score
                );
            }
        }
    }

    println!(
        "✓ All {} clusters meet quality thresholds",
        clusters_with_quality.len()
    );
}

/// Test that clustering is deterministic (same input → same output)
///
/// FIXME: This test is currently flaky (~50% failure rate) due to non-determinism
/// in the hierarchical clustering algorithm. Root causes:
///
/// 1. **Similarity matrix index invalidation**: The similarity matrix is built once
///    using initial cluster indices (0, 1, 2, 3...). As clusters merge and are removed
///    from the vector, indices shift, but the matrix still uses old indices. This causes
///    incorrect similarity lookups and non-deterministic merge decisions.
///
/// 2. **Floating-point tie-breaking**: When multiple cluster pairs have nearly identical
///    similarity scores (within epsilon), the merge order can vary due to rounding errors
///    in the similarity calculations, even with epsilon-based comparison.
///
/// Partial fixes applied:
/// - HashMap → BTreeMap conversions for deterministic iteration
/// - Epsilon-based floating-point comparison (ε = 1e-10)
/// - Deterministic tie-breaking using lexicographic index ordering
///
/// Required fixes:
/// - Rebuild similarity matrix after each merge (performance cost), OR
/// - Use stable cluster IDs instead of vector indices throughout the algorithm
///
/// Spec 262: Updated to use detector.rs after recommender.rs was removed.
#[test]
#[ignore = "Flaky test due to hierarchical clustering non-determinism - see FIXME comment"]
fn test_clustering_determinism() {
    let source_code = std::fs::read_to_string("src/organization/god_object/detector.rs")
        .expect("Failed to read detector.rs");

    let path = Path::new("src/organization/god_object/detector.rs");
    let extracted = UnifiedFileExtractor::extract(path, &source_code)
        .expect("Failed to extract recommender.rs");

    // Run clustering twice
    let analyses1 = analyze_god_objects(path, &extracted);
    let analyses2 = analyze_god_objects(path, &extracted);

    if analyses1.is_empty() || analyses2.is_empty() {
        println!("No god objects detected - test passes");
        return;
    }

    let analysis1 = &analyses1[0];
    let analysis2 = &analyses2[0];

    // Should produce identical results
    assert_eq!(
        analysis1.recommended_splits.len(),
        analysis2.recommended_splits.len(),
        "Clustering should be deterministic"
    );

    // Check that split names and method counts match
    for (split1, split2) in analysis1
        .recommended_splits
        .iter()
        .zip(analysis2.recommended_splits.iter())
    {
        assert_eq!(
            split1.suggested_name, split2.suggested_name,
            "Split names should be identical across runs"
        );
        assert_eq!(
            split1.method_count, split2.method_count,
            "Method counts should be identical across runs"
        );
    }

    println!("✓ Clustering is deterministic");
}