reputation_core/
lib.rs

1//! # Reputation Core
2//! 
3//! This crate provides the core reputation calculation engine for the MCP agent
4//! reputation system. It implements a hybrid approach combining prior reputation
5//! scores with performance-based calculations.
6
7// Forbid unsafe code to ensure memory safety
8#![forbid(unsafe_code)] 
9//! ## Overview
10//! 
11//! The reputation system uses a weighted calculation that considers:
12//! - **Prior Score**: Initial reputation based on agent credentials (50-80 points)
13//! - **Performance Score**: Based on reviews and ratings (0-100 points)
14//! - **Confidence Factor**: How much we trust the performance data (0-1)
15//! 
16//! ## Features (Phase 2)
17//! 
18//! - **Enhanced Score Structure**: Detailed breakdowns with confidence levels
19//! - **Batch Processing**: Efficient parallel processing of multiple agents
20//! - **Builder Pattern**: Fluent API for calculator configuration
21//! - **Utility Methods**: Score analysis, predictions, and comparisons
22//! - **Property Testing**: Comprehensive test coverage with invariant verification
23//! 
24//! ## Algorithm
25//! 
26//! The final reputation score is calculated as:
27//! ```text
28//! final_score = (1 - confidence) * prior_score + confidence * empirical_score
29//! ```
30//! 
31//! Where confidence grows with the number of interactions:
32//! ```text
33//! confidence = interactions / (interactions + k)
34//! ```
35//! 
36//! ## Examples
37//! 
38//! ### Basic Usage
39//! ```no_run
40//! use reputation_core::Calculator;
41//! use reputation_types::{AgentData, AgentDataBuilder};
42//! 
43//! let agent = AgentDataBuilder::new("did:example:123")
44//!     .with_reviews(100, 4.3)
45//!     .mcp_level(2)
46//!     .identity_verified(true)
47//!     .build()
48//!     .unwrap();
49//! 
50//! let calculator = Calculator::default();
51//! let score = calculator.calculate(&agent).unwrap();
52//! 
53//! println!("Reputation Score: {:.1}", score.score);
54//! println!("Confidence: {:.2}", score.confidence);
55//! println!("Level: {:?}", score.level);
56//! println!("Is Provisional: {}", score.is_provisional);
57//! ```
58//! 
59//! ### Builder Pattern (Phase 2)
60//! ```no_run
61//! use reputation_core::{Calculator, CalculatorPreset};
62//! 
63//! let calculator = Calculator::builder()
64//!     .preset(CalculatorPreset::Conservative)
65//!     .prior_base(55.0)
66//!     .build()
67//!     .unwrap();
68//! ```
69//! 
70//! ### Batch Processing (Phase 2)
71//! ```no_run
72//! use reputation_core::{Calculator, BatchOptions};
73//! use reputation_types::AgentData;
74//! 
75//! # fn load_agents() -> Vec<AgentData> { vec![] }
76//! let agents = load_agents(); // Vec<AgentData>
77//! let calculator = Calculator::default();
78//! 
79//! // Simple batch processing
80//! let scores = calculator.calculate_batch(&agents);
81//! 
82//! // With progress tracking
83//! let options = BatchOptions {
84//!     chunk_size: Some(100),
85//!     fail_fast: false,
86//!     progress_callback: Some(Box::new(|completed, total| {
87//!         println!("Progress: {}/{}", completed, total);
88//!     })),
89//! };
90//! 
91//! let result = calculator.calculate_batch_with_options(&agents, options);
92//! println!("Processed {} agents in {:?}", 
93//!     result.successful_count, result.total_duration);
94//! ```
95//! 
96//! ### Utility Methods (Phase 2)
97//! ```no_run
98//! use reputation_core::Calculator;
99//! use reputation_types::AgentData;
100//! 
101//! # fn get_agent() -> AgentData { 
102//! #     reputation_types::AgentDataBuilder::new("did:test:1")
103//! #         .with_reviews(50, 4.0)
104//! #         .total_interactions(60)
105//! #         .build()
106//! #         .unwrap()
107//! # }
108//! let agent = get_agent();
109//! let calculator = Calculator::default();
110//! 
111//! // Get detailed explanation
112//! let explanation = calculator.explain_score(&agent).unwrap();
113//! println!("{}", explanation.explanation);
114//! 
115//! // Calculate needed interactions for target confidence
116//! let needed = calculator.interactions_for_confidence(
117//!     agent.total_interactions, 0.9
118//! ).unwrap();
119//! println!("Need {} more interactions for 90% confidence", needed);
120//! 
121//! // Predict score changes
122//! let prediction = calculator.predict_score_change(&agent, 50, 4.5).unwrap();
123//! println!("Score would change by {:+.1} points", prediction.score_change);
124//! ```
125//! 
126//! ## Performance Characteristics
127//! 
128//! - Single calculation: ~50-100μs
129//! - Batch 1000 agents: ~388μs (far exceeding <100ms target)
130//! - Memory usage: O(1) - no allocations during calculation
131//! - Thread-safe: Calculator can be shared across threads
132//! - Cache-friendly: Optimized for batch processing
133
134pub mod calculator;
135pub mod config;
136pub mod error;
137pub mod validation;
138pub mod performance;
139
140// Re-export main types
141pub use calculator::{Calculator, BatchOptions, BatchResult, BatchCalculation};
142pub use calculator::builder::{CalculatorBuilder, BonusConfig, CalculatorPreset};
143pub use calculator::utils::{ScoreExplanation, ScorePrediction, AgentComparison};
144pub use config::CalculatorConfig;
145pub use error::{ReputationError, ValidationError, BuilderError, CalculationError, Result};
146
147// Re-export new types from reputation-types for convenience
148pub use reputation_types::{ConfidenceLevel, ScoreComponents, PriorBreakdown};
149
150/// Version of the reputation calculation algorithm
151pub const ALGORITHM_VERSION: &str = "1.0.0";
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use chrono::{Duration, Utc};
157    use reputation_types::AgentData;
158
159    fn create_valid_agent() -> AgentData {
160        AgentData {
161            did: "did:test:123".to_string(),
162            created_at: Utc::now() - Duration::days(1),
163            mcp_level: None,
164            identity_verified: false,
165            security_audit_passed: false,
166            open_source: false,
167            total_interactions: 0,
168            total_reviews: 0,
169            average_rating: None,
170            positive_reviews: 0,
171            negative_reviews: 0,
172        }
173    }
174
175    #[test]
176    fn test_default_calculation() {
177        let agent = create_valid_agent();
178        let calc = Calculator::default();
179        let score = calc.calculate(&agent).unwrap();
180
181        assert_eq!(score.score, 50.0);
182        assert_eq!(score.confidence, 0.0);
183    }
184
185    #[test]
186    fn test_invalid_did_format() {
187        let mut agent = create_valid_agent();
188        agent.did = "invalid-did".to_string();
189
190        let calc = Calculator::default();
191        let result = calc.calculate(&agent);
192
193        assert!(result.is_err());
194        match result.unwrap_err() {
195            ReputationError::ValidationError(ValidationError::InvalidDid(msg)) => {
196                // The validation now returns a specific error message about DID format
197                assert!(msg.contains("DID must start with 'did:' prefix"));
198            }
199            _ => panic!("Expected InvalidDid error"),
200        }
201    }
202
203    #[test]
204    fn test_future_date_error() {
205        let mut agent = create_valid_agent();
206        agent.created_at = Utc::now() + Duration::days(1);
207
208        let calc = Calculator::default();
209        let result = calc.calculate(&agent);
210
211        assert!(result.is_err());
212        match result.unwrap_err() {
213            ReputationError::ValidationError(ValidationError::FutureDate(_)) => {}
214            _ => panic!("Expected FutureDate error"),
215        }
216    }
217
218    #[test]
219    fn test_invalid_rating_error() {
220        let mut agent = create_valid_agent();
221        agent.average_rating = Some(6.0);
222        agent.total_reviews = 10;
223        agent.positive_reviews = 6;
224        agent.negative_reviews = 4;
225        agent.total_interactions = 10;
226
227        let calc = Calculator::default();
228        let result = calc.calculate(&agent);
229
230        assert!(result.is_err());
231        match result.unwrap_err() {
232            ReputationError::ValidationError(ValidationError::InvalidRating(rating)) => {
233                assert_eq!(rating, 6.0);
234            }
235            _ => panic!("Expected InvalidRating error"),
236        }
237    }
238
239    #[test]
240    fn test_invalid_mcp_level_error() {
241        let mut agent = create_valid_agent();
242        agent.mcp_level = Some(5);
243
244        let calc = Calculator::default();
245        let result = calc.calculate(&agent);
246
247        assert!(result.is_err());
248        match result.unwrap_err() {
249            ReputationError::ValidationError(ValidationError::InvalidMcpLevel(level)) => {
250                assert_eq!(level, 5);
251            }
252            _ => panic!("Expected InvalidMcpLevel error"),
253        }
254    }
255
256    #[test]
257    fn test_inconsistent_reviews_error() {
258        let mut agent = create_valid_agent();
259        agent.positive_reviews = 10;
260        agent.negative_reviews = 5;
261        agent.total_reviews = 20; // Should be 15
262        agent.total_interactions = 20;
263        agent.average_rating = Some(4.0);
264
265        let calc = Calculator::default();
266        let result = calc.calculate(&agent);
267
268        assert!(result.is_err());
269        match result.unwrap_err() {
270            ReputationError::ValidationError(ValidationError::InconsistentReviews) => {
271                // The new validation module returns a simpler error without the specific counts
272            }
273            _ => panic!("Expected InconsistentReviews error"),
274        }
275    }
276
277    #[test]
278    fn test_reviews_exceed_interactions_error() {
279        let mut agent = create_valid_agent();
280        agent.total_reviews = 10;
281        agent.positive_reviews = 6;
282        agent.negative_reviews = 4;
283        agent.total_interactions = 5;
284        agent.average_rating = Some(4.0); // Add rating to avoid rating consistency error
285
286        let calc = Calculator::default();
287        let result = calc.calculate(&agent);
288
289        assert!(result.is_err());
290        match result.unwrap_err() {
291            ReputationError::ValidationError(ValidationError::InvalidField { field, .. }) => {
292                assert_eq!(field, "total_reviews");
293            }
294            _ => panic!("Expected InvalidField error"),
295        }
296    }
297
298    #[test]
299    fn test_calculator_new_invalid_confidence_k() {
300        let result = Calculator::new(-1.0, 50.0, 80.0);
301        assert!(result.is_err());
302        match result.unwrap_err() {
303            ReputationError::CalculationError(msg) => {
304                assert!(msg.contains("confidence_k must be positive"));
305            }
306            _ => panic!("Expected BuilderError for negative confidence_k"),
307        }
308    }
309
310    #[test]
311    fn test_calculator_new_invalid_prior_base() {
312        let result = Calculator::new(15.0, -10.0, 80.0);
313        assert!(result.is_err());
314
315        let result = Calculator::new(15.0, 110.0, 80.0);
316        assert!(result.is_err());
317    }
318
319    #[test]
320    fn test_calculator_new_invalid_prior_max() {
321        let result = Calculator::new(15.0, 50.0, 40.0); // max < base
322        assert!(result.is_err());
323
324        let result = Calculator::new(15.0, 50.0, 110.0); // max > 100
325        assert!(result.is_err());
326    }
327
328    #[test]
329    fn test_valid_calculation_with_reviews() {
330        let mut agent = create_valid_agent();
331        agent.total_interactions = 100;
332        agent.total_reviews = 50;
333        agent.positive_reviews = 40;
334        agent.negative_reviews = 10;
335        agent.average_rating = Some(4.2);
336        agent.mcp_level = Some(2);
337
338        let calc = Calculator::default();
339        let score = calc.calculate(&agent).unwrap();
340
341        assert!(score.score > 50.0); // Should be above base due to good rating
342        assert!(score.confidence > 0.0 && score.confidence < 1.0);
343    }
344
345    #[test]
346    fn test_edge_case_minimum_rating() {
347        let mut agent = create_valid_agent();
348        agent.total_interactions = 10;
349        agent.total_reviews = 10;
350        agent.positive_reviews = 0;
351        agent.negative_reviews = 10;
352        agent.average_rating = Some(1.0);
353
354        let calc = Calculator::default();
355        let score = calc.calculate(&agent).unwrap();
356
357        assert!(score.score >= 0.0);
358        assert!(score.score <= 100.0);
359    }
360
361    #[test]
362    fn test_edge_case_maximum_rating() {
363        let mut agent = create_valid_agent();
364        agent.total_interactions = 10;
365        agent.total_reviews = 10;
366        agent.positive_reviews = 10;
367        agent.negative_reviews = 0;
368        agent.average_rating = Some(5.0);
369
370        let calc = Calculator::default();
371        let score = calc.calculate(&agent).unwrap();
372
373        assert!(score.score >= 0.0);
374        assert!(score.score <= 100.0);
375    }
376
377    #[test]
378    fn test_mcp_level_bonuses() {
379        let base_agent = create_valid_agent();
380        let calc = Calculator::default();
381
382        // Test each MCP level
383        for level in 0..=3 {
384            let mut agent = base_agent.clone();
385            agent.mcp_level = Some(level);
386            let score = calc.calculate(&agent).unwrap();
387            
388            // With no reviews, score should be prior (base + mcp bonus)
389            let expected_bonus = match level {
390                1 => 5.0,
391                2 => 10.0,
392                3 => 15.0,
393                _ => 0.0,
394            };
395            assert_eq!(score.score, 50.0 + expected_bonus);
396        }
397    }
398
399    #[test]
400    fn test_confidence_calculation() {
401        let calc = Calculator::default();
402        let mut agent = create_valid_agent();
403        
404        // Test various interaction counts
405        let test_cases = vec![(0, 0.0), (15, 0.5), (30, 0.667), (150, 0.909)];
406        
407        for (interactions, expected_confidence) in test_cases {
408            agent.total_interactions = interactions;
409            let score = calc.calculate(&agent).unwrap();
410            assert!((score.confidence - expected_confidence).abs() < 0.01);
411        }
412    }
413
414    #[test]
415    fn test_error_propagation_chain() {
416        // Test that errors properly propagate through the calculation chain
417        let mut agent = create_valid_agent();
418        agent.did = "bad-did".to_string();
419        agent.average_rating = Some(10.0); // Also invalid
420        
421        let calc = Calculator::default();
422        let result = calc.calculate(&agent);
423        
424        assert!(result.is_err());
425        // Should get the first error (DID validation)
426        match result.unwrap_err() {
427            ReputationError::ValidationError(ValidationError::InvalidDid(_)) => {}
428            _ => panic!("Expected InvalidDid error to be caught first"),
429        }
430    }
431
432    #[test]
433    fn test_valid_edge_case_ratings() {
434        let calc = Calculator::default();
435        
436        // Test exact boundary values
437        let test_ratings = vec![1.0, 5.0, 3.0];
438        
439        for rating in test_ratings {
440            let mut agent = create_valid_agent();
441            agent.total_interactions = 50;
442            agent.total_reviews = 50;
443            agent.positive_reviews = 25;
444            agent.negative_reviews = 25;
445            agent.average_rating = Some(rating);
446            
447            let result = calc.calculate(&agent);
448            assert!(result.is_ok(), "Rating {} should be valid", rating);
449        }
450    }
451
452    #[test]
453    fn test_zero_confidence_k_prevention() {
454        let result = Calculator::new(0.0, 50.0, 80.0);
455        assert!(result.is_err());
456    }
457
458    #[test]
459    fn test_nan_prevention_in_calculation() {
460        // This test ensures our NaN checks work
461        // Even though with current logic it's hard to produce NaN,
462        // the check exists for robustness
463        let calc = Calculator::default();
464        let agent = create_valid_agent();
465        
466        // Normal calculation shouldn't produce NaN
467        let result = calc.calculate(&agent);
468        assert!(result.is_ok());
469        let score = result.unwrap();
470        assert!(!score.score.is_nan());
471        assert!(!score.confidence.is_nan());
472    }
473
474    #[test]
475    fn test_all_identity_flags() {
476        let calc = Calculator::default();
477        let mut agent = create_valid_agent();
478        
479        // Test with all identity flags set
480        agent.identity_verified = true;
481        agent.security_audit_passed = true;
482        agent.open_source = true;
483        agent.mcp_level = Some(3);
484        
485        let result = calc.calculate(&agent);
486        assert!(result.is_ok());
487        
488        // With all bonuses: 50 base + 15 (MCP3) + 5 (identity) + 7 (security) + 3 (open source) = 80
489        let score = result.unwrap();
490        assert_eq!(score.score, 80.0); // Hits the cap
491    }
492
493    #[test]
494    fn test_identity_verified_bonus() {
495        let calc = Calculator::default();
496        
497        // Test without identity verified
498        let mut agent = create_valid_agent();
499        let score_without = calc.calculate(&agent).unwrap();
500        
501        // Test with identity verified
502        agent.identity_verified = true;
503        let score_with = calc.calculate(&agent).unwrap();
504        
505        // Should add 5 points
506        assert_eq!(score_with.score - score_without.score, 5.0);
507        assert_eq!(score_with.score, 55.0); // 50 base + 5 identity
508    }
509
510    #[test]
511    fn test_security_audit_bonus() {
512        let calc = Calculator::default();
513        
514        // Test without security audit
515        let mut agent = create_valid_agent();
516        let score_without = calc.calculate(&agent).unwrap();
517        
518        // Test with security audit
519        agent.security_audit_passed = true;
520        let score_with = calc.calculate(&agent).unwrap();
521        
522        // Should add 7 points
523        assert_eq!(score_with.score - score_without.score, 7.0);
524        assert_eq!(score_with.score, 57.0); // 50 base + 7 security
525    }
526
527    #[test]
528    fn test_open_source_bonus() {
529        let calc = Calculator::default();
530        
531        // Test without open source
532        let mut agent = create_valid_agent();
533        let score_without = calc.calculate(&agent).unwrap();
534        
535        // Test with open source
536        agent.open_source = true;
537        let score_with = calc.calculate(&agent).unwrap();
538        
539        // Should add 3 points
540        assert_eq!(score_with.score - score_without.score, 3.0);
541        assert_eq!(score_with.score, 53.0); // 50 base + 3 open source
542    }
543
544    #[test]
545    fn test_age_bonus() {
546        let calc = Calculator::default();
547        
548        // Test with young agent (1 day old)
549        let mut agent = create_valid_agent();
550        agent.created_at = Utc::now() - Duration::days(1);
551        let score_young = calc.calculate(&agent).unwrap();
552        
553        // Test with old agent (400 days old)
554        agent.created_at = Utc::now() - Duration::days(400);
555        let score_old = calc.calculate(&agent).unwrap();
556        
557        // Should add 5 points for agents > 365 days
558        assert_eq!(score_old.score - score_young.score, 5.0);
559        assert_eq!(score_young.score, 50.0); // 50 base, no age bonus
560        assert_eq!(score_old.score, 55.0); // 50 base + 5 age bonus
561    }
562
563    #[test]
564    fn test_age_bonus_edge_cases() {
565        let calc = Calculator::default();
566        let mut agent = create_valid_agent();
567        
568        // Test exactly 365 days (no bonus)
569        agent.created_at = Utc::now() - Duration::days(365);
570        let score_365 = calc.calculate(&agent).unwrap();
571        assert_eq!(score_365.score, 50.0); // No age bonus
572        
573        // Test 366 days (gets bonus)
574        agent.created_at = Utc::now() - Duration::days(366);
575        let score_366 = calc.calculate(&agent).unwrap();
576        assert_eq!(score_366.score, 55.0); // Gets age bonus
577    }
578
579    #[test]
580    fn test_combined_bonuses() {
581        let calc = Calculator::default();
582        let mut agent = create_valid_agent();
583        
584        // Test various combinations
585        agent.mcp_level = Some(2); // +10
586        agent.identity_verified = true; // +5
587        let score = calc.calculate(&agent).unwrap();
588        assert_eq!(score.score, 65.0); // 50 + 10 + 5
589        
590        // Add more bonuses
591        agent.open_source = true; // +3
592        let score = calc.calculate(&agent).unwrap();
593        assert_eq!(score.score, 68.0); // 50 + 10 + 5 + 3
594        
595        // Add security audit
596        agent.security_audit_passed = true; // +7
597        let score = calc.calculate(&agent).unwrap();
598        assert_eq!(score.score, 75.0); // 50 + 10 + 5 + 3 + 7
599    }
600
601    #[test]
602    fn test_prior_score_cap() {
603        let calc = Calculator::default();
604        let mut agent = create_valid_agent();
605        
606        // Set all bonuses to exceed the cap
607        agent.mcp_level = Some(3); // +15
608        agent.identity_verified = true; // +5
609        agent.security_audit_passed = true; // +7
610        agent.open_source = true; // +3
611        agent.created_at = Utc::now() - Duration::days(400); // +5 age bonus
612        
613        // Total would be 50 + 15 + 5 + 7 + 3 + 5 = 85, but capped at 80
614        let score = calc.calculate(&agent).unwrap();
615        assert_eq!(score.score, 80.0);
616    }
617
618    #[test]
619    fn test_prior_score_cap_with_interactions() {
620        let calc = Calculator::default();
621        let mut agent = create_valid_agent();
622        
623        // Max out prior bonuses
624        agent.mcp_level = Some(3);
625        agent.identity_verified = true;
626        agent.security_audit_passed = true;
627        agent.open_source = true;
628        agent.created_at = Utc::now() - Duration::days(400);
629        
630        // Add some interactions with good rating
631        agent.total_interactions = 50;
632        agent.total_reviews = 50;
633        agent.positive_reviews = 45;
634        agent.negative_reviews = 5;
635        agent.average_rating = Some(4.5);
636        
637        let score = calc.calculate(&agent).unwrap();
638        
639        // Prior is capped at 80, empirical is 87.5 ((4.5-1)*25)
640        // With 50 interactions, confidence = 50/(50+15) ≈ 0.769
641        // Final score = 0.231 * 80 + 0.769 * 87.5 ≈ 85.76
642        assert!(score.score > 80.0); // Shows that prior cap doesn't limit final score
643        assert!(score.score < 90.0);
644    }
645
646    #[test]
647    fn test_enhanced_score_structure() {
648        let calc = Calculator::default();
649        let mut agent = create_valid_agent();
650        
651        // Set up an agent with some data
652        agent.total_interactions = 50;
653        agent.total_reviews = 30;
654        agent.positive_reviews = 25;
655        agent.negative_reviews = 5;
656        agent.average_rating = Some(4.2);
657        agent.mcp_level = Some(2);
658        agent.identity_verified = true;
659        
660        let score = calc.calculate(&agent).unwrap();
661        
662        // Test new fields
663        assert_eq!(score.level, ConfidenceLevel::High); // 50/(50+15) ≈ 0.77 which is > 0.7
664        assert!(!score.is_provisional); // confidence > 0.2
665        assert_eq!(score.data_points, 80); // 50 + 30
666        
667        // Test components
668        assert_eq!(score.components.prior_score, 65.0); // 50 + 10 + 5
669        assert_eq!(score.components.empirical_score, 80.0); // (4.2-1)*25
670        assert_eq!(score.components.confidence_level, ConfidenceLevel::High);
671        assert!(score.components.confidence_value > 0.7);
672        
673        // Test prior breakdown
674        assert_eq!(score.components.prior_breakdown.base_score, 50.0);
675        assert_eq!(score.components.prior_breakdown.mcp_bonus, 10.0);
676        assert_eq!(score.components.prior_breakdown.identity_bonus, 5.0);
677        assert_eq!(score.components.prior_breakdown.total, 65.0);
678    }
679
680    #[test]
681    fn test_provisional_score() {
682        let calc = Calculator::default();
683        let mut agent = create_valid_agent();
684        
685        // Very few interactions
686        agent.total_interactions = 2;
687        agent.total_reviews = 2;
688        agent.positive_reviews = 1;
689        agent.negative_reviews = 1;
690        agent.average_rating = Some(3.0);
691        
692        let score = calc.calculate(&agent).unwrap();
693        
694        // Should be provisional
695        assert!(score.is_provisional);
696        assert_eq!(score.level, ConfidenceLevel::Low);
697        assert!(score.confidence < 0.2);
698    }
699
700    #[test]
701    fn test_confidence_level_boundaries() {
702        let calc = Calculator::default();
703        let mut agent = create_valid_agent();
704        
705        // Test Low confidence (< 0.2)
706        agent.total_interactions = 3; // 3/(3+15) = 0.167
707        agent.total_reviews = 3;
708        agent.positive_reviews = 2;
709        agent.negative_reviews = 1;
710        agent.average_rating = Some(4.0);
711        let score = calc.calculate(&agent).unwrap();
712        assert_eq!(score.level, ConfidenceLevel::Low);
713        
714        // Test Medium confidence (0.2-0.7)
715        agent.total_interactions = 15; // 15/(15+15) = 0.5
716        agent.total_reviews = 15;
717        agent.positive_reviews = 12;
718        agent.negative_reviews = 3;
719        let score = calc.calculate(&agent).unwrap();
720        assert_eq!(score.level, ConfidenceLevel::Medium);
721        
722        // Test High confidence (>= 0.7)
723        agent.total_interactions = 50; // 50/(50+15) ≈ 0.77
724        agent.total_reviews = 40;
725        agent.positive_reviews = 32;
726        agent.negative_reviews = 8;
727        let score = calc.calculate(&agent).unwrap();
728        assert_eq!(score.level, ConfidenceLevel::High);
729    }
730
731    #[test]
732    fn test_individual_bonus_independence() {
733        let calc = Calculator::default();
734        
735        // Test that each bonus works independently
736        let base_agent = create_valid_agent();
737        let base_score = calc.calculate(&base_agent).unwrap().score;
738        
739        // Test identity verified bonus
740        let mut agent = base_agent.clone();
741        agent.identity_verified = true;
742        let score = calc.calculate(&agent).unwrap().score;
743        assert_eq!(score - base_score, 5.0, "Identity bonus should be 5");
744        
745        // Test security audit bonus
746        let mut agent = base_agent.clone();
747        agent.security_audit_passed = true;
748        let score = calc.calculate(&agent).unwrap().score;
749        assert_eq!(score - base_score, 7.0, "Security audit bonus should be 7");
750        
751        // Test open source bonus
752        let mut agent = base_agent.clone();
753        agent.open_source = true;
754        let score = calc.calculate(&agent).unwrap().score;
755        assert_eq!(score - base_score, 3.0, "Open source bonus should be 3");
756        
757        // Test MCP level 1 bonus
758        let mut agent = base_agent.clone();
759        agent.mcp_level = Some(1);
760        let score = calc.calculate(&agent).unwrap().score;
761        assert_eq!(score - base_score, 5.0, "MCP level 1 bonus should be 5");
762        
763        // Test age bonus
764        let mut agent = base_agent.clone();
765        agent.created_at = Utc::now() - Duration::days(400);
766        let score = calc.calculate(&agent).unwrap().score;
767        assert_eq!(score - base_score, 5.0, "Age bonus should be 5");
768    }
769}