reputation-core 0.1.0

Core calculation engine for the KnowThat Reputation System with advanced scoring algorithms
Documentation
use reputation_core::{Calculator, BatchOptions};
use reputation_types::{AgentDataBuilder, AgentData};
use std::time::Instant;

fn create_test_agents(count: usize) -> Vec<AgentData> {
    (0..count)
        .map(|i| {
            // For i=0, ensure we don't create invalid data
            let reviews = if i == 0 { 0 } else { i as u32 % 100 };
            let rating = if reviews == 0 { None } else { Some(3.0 + (i % 3) as f64 * 0.5) };
            let interactions = reviews + (i as u32 % 50); // Ensure interactions >= reviews
            
            let mut builder = AgentDataBuilder::new(&format!("did:test:agent{}", i))
                .total_interactions(interactions)
                .mcp_level((i % 4) as u8)
                .identity_verified(i % 2 == 0);
            
            if let Some(r) = rating {
                builder = builder.with_reviews(reviews, r);
            } else {
                // No reviews case
                builder = builder
                    .total_reviews(0)
                    .positive_reviews(0)
                    .negative_reviews(0);
            }
            
            builder.build().unwrap()
        })
        .collect()
}

#[test]
fn test_basic_batch_processing() {
    let calculator = Calculator::default();
    let agents = create_test_agents(10);
    
    let results = calculator.calculate_batch(&agents);
    
    assert_eq!(results.len(), agents.len());
    for result in &results {
        assert!(result.is_ok());
    }
}

#[test]
fn test_batch_maintains_order() {
    let calculator = Calculator::default();
    let agents = create_test_agents(20);
    
    let results = calculator.calculate_batch(&agents);
    
    // Verify order is maintained
    for (_i, result) in results.iter().enumerate() {
        assert!(result.is_ok());
        let score = result.as_ref().unwrap();
        // The score should be calculated for the corresponding agent
        assert!(score.score >= 0.0 && score.score <= 100.0);
    }
}

#[test]
fn test_batch_with_errors() {
    let calculator = Calculator::default();
    let mut agents = create_test_agents(5);
    
    // Introduce an invalid agent
    agents[2].did = "invalid-did".to_string();
    
    let results = calculator.calculate_batch(&agents);
    
    assert_eq!(results.len(), 5);
    assert!(results[0].is_ok());
    assert!(results[1].is_ok());
    assert!(results[2].is_err()); // The invalid one
    assert!(results[3].is_ok());
    assert!(results[4].is_ok());
}

#[test]
fn test_batch_performance() {
    let calculator = Calculator::default();
    let agents = create_test_agents(1000);
    
    let start = Instant::now();
    let results = calculator.calculate_batch(&agents);
    let duration = start.elapsed();
    
    assert_eq!(results.len(), 1000);
    let successful = results.iter().filter(|r| r.is_ok()).count();
    assert_eq!(successful, 1000);
    
    // Should process 1000 agents in under 100ms
    assert!(
        duration.as_millis() < 100,
        "Batch processing took {:?}, expected < 100ms",
        duration
    );
    
    println!("Processed 1000 agents in {:?}", duration);
}

#[test]
fn test_batch_with_options() {
    let calculator = Calculator::default();
    let agents = create_test_agents(100);
    
    let options = BatchOptions {
        chunk_size: Some(10),
        fail_fast: false,
        progress_callback: None,
    };
    
    let result = calculator.calculate_batch_with_options(&agents, options);
    
    assert_eq!(result.calculations.len(), 100);
    assert_eq!(result.successful_count, 100);
    assert_eq!(result.failed_count, 0);
    assert!(result.total_duration.as_millis() < 50);
}

#[test]
fn test_batch_with_progress_callback() {
    use std::sync::{Arc, Mutex};
    
    let calculator = Calculator::default();
    let agents = create_test_agents(50);
    
    let progress_calls = Arc::new(Mutex::new(Vec::new()));
    let progress_calls_clone = progress_calls.clone();
    
    let options = BatchOptions {
        chunk_size: Some(5),
        fail_fast: false,
        progress_callback: Some(Box::new(move |current, total| {
            progress_calls_clone.lock().unwrap().push((current, total));
        })),
    };
    
    let result = calculator.calculate_batch_with_options(&agents, options);
    
    assert_eq!(result.calculations.len(), 50);
    
    // Check that progress was reported
    let calls = progress_calls.lock().unwrap();
    assert!(!calls.is_empty());
    
    // With parallel processing, we should see all 50 items processed
    let max_progress = calls.iter().map(|(current, _)| *current).max().unwrap();
    assert_eq!(max_progress, 50); // All 50 items should be processed
    
    // All calls should report total of 50
    for (_, total) in calls.iter() {
        assert_eq!(*total, 50);
    }
}

#[test]
fn test_batch_result_metadata() {
    let calculator = Calculator::default();
    let mut agents = create_test_agents(10);
    
    // Make one agent invalid
    agents[5].did = "bad-did".to_string();
    
    let options = BatchOptions::default();
    let result = calculator.calculate_batch_with_options(&agents, options);
    
    assert_eq!(result.calculations.len(), 10);
    assert_eq!(result.successful_count, 9);
    assert_eq!(result.failed_count, 1);
    
    // Check individual calculation metadata
    for (i, calc) in result.calculations.iter().enumerate() {
        assert_eq!(calc.agent_id, agents[i].did);
        assert!(calc.duration.as_nanos() > 0);
        
        if i == 5 {
            assert!(calc.result.is_err());
        } else {
            assert!(calc.result.is_ok());
        }
    }
}

#[test]
fn test_empty_batch() {
    let calculator = Calculator::default();
    let agents: Vec<AgentData> = vec![];
    
    let results = calculator.calculate_batch(&agents);
    assert!(results.is_empty());
    
    let options = BatchOptions::default();
    let batch_result = calculator.calculate_batch_with_options(&agents, options);
    assert!(batch_result.calculations.is_empty());
    assert_eq!(batch_result.successful_count, 0);
    assert_eq!(batch_result.failed_count, 0);
}

#[test]
fn test_single_agent_batch() {
    let calculator = Calculator::default();
    let agents = create_test_agents(1);
    
    let results = calculator.calculate_batch(&agents);
    assert_eq!(results.len(), 1);
    assert!(results[0].is_ok());
    
    // Compare with single calculation
    let single_result = calculator.calculate(&agents[0]).unwrap();
    let batch_result = results[0].as_ref().unwrap();
    
    assert_eq!(single_result.score, batch_result.score);
    assert_eq!(single_result.confidence, batch_result.confidence);
}

#[test]
fn test_large_batch_memory_efficiency() {
    let calculator = Calculator::default();
    
    // Test with increasingly large batches
    for size in [100, 500, 1000, 5000] {
        let agents = create_test_agents(size);
        
        let start = Instant::now();
        let results = calculator.calculate_batch(&agents);
        let duration = start.elapsed();
        
        assert_eq!(results.len(), size);
        
        // Time should scale roughly linearly
        let ms_per_agent = duration.as_micros() as f64 / size as f64;
        println!("Batch size {}: {:.2}μs per agent", size, ms_per_agent);
        
        // Should be under 100 microseconds per agent
        assert!(ms_per_agent < 100.0);
    }
}