//! Battery 3: Decay Math Precision. //! //! Tests confidence decay calculations across different source class tiers. //! //! # Test Coverage //! //! | Test | Feature | Validates | //! |------|---------|-----------| //! | `test_decay_tier0_never_decays` | Tier 0 | Regulatory never decays | //! | `test_decay_tier1_exact_halflife` | Tier 1 | Clinical at 730d = 0.5x | //! | `test_decay_tier1_two_halflives` | Tier 1 | Clinical at 1460d = 0.25x | //! | `test_decay_tier5_exact_halflife` | Tier 5 | Anecdotal at 30d = 0.5x | //! | `test_decay_tier5_three_halflives` | Tier 5 | Anecdotal at 90d = 0.125x | //! | `test_decay_zero_confidence_stays_zero` | Edge case | 0 * decay = 0 | //! | `test_decay_never_goes_negative` | Edge case | No negative confidence | //! | `test_decay_uses_as_of_for_age_calculation` | Time travel | Uses `now` param | #![allow(clippy::expect_used)] // Test code uses expect() for clear failure messages use super::helpers::*; /// Test 3.1: Regulatory assertions never decay. /// /// Tier 0 (Regulatory) has no decay halflife. /// A Regulatory assertion with confidence 0.95, timestamped 10 years ago, /// should maintain its original confidence after decay application. #[test] fn test_decay_tier0_never_decays() { let now: u64 = 1_000_000_000; let ten_years_ago = now - (10 * 365 * 86_400); let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Regulatory) .confidence(0.95) .timestamp(ten_years_ago) .build(); let fallback_halflife: u64 = 365 * 86_400; // 1 year let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); assert_eq!( decayed[0].confidence, 0.95, "Regulatory assertions should never decay, confidence should remain exactly 0.95" ); } /// Test 3.2: Clinical assertion decays to 0.5 at exactly one half-life. /// /// Tier 1 (Clinical) has 730-day half-life. /// An assertion with confidence 1.0, timestamped exactly 730 days ago, /// should decay to 0.5 (within tolerance of 0.02). #[test] fn test_decay_tier1_exact_halflife() { let now: u64 = 1_000_000_000; let days_730 = 730 * 86_400; let past = now - days_730; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Clinical) .confidence(1.0) .timestamp(past) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); let effective_conf = decayed[0].confidence; assert!( (effective_conf - 0.5).abs() < 0.02, "Clinical assertion at 730 days (1 half-life) should decay to ~0.5, got {}", effective_conf ); } /// Test 3.3: Clinical assertion decays to 0.25 at exactly two half-lives. /// /// Tier 1 (Clinical) has 730-day half-life. /// An assertion with confidence 1.0, timestamped exactly 1460 days ago, /// should decay to 0.25 (within tolerance of 0.02). #[test] fn test_decay_tier1_two_halflives() { let now: u64 = 1_000_000_000; let days_1460 = 1460 * 86_400; let past = now - days_1460; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Clinical) .confidence(1.0) .timestamp(past) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); let effective_conf = decayed[0].confidence; assert!( (effective_conf - 0.25).abs() < 0.02, "Clinical assertion at 1460 days (2 half-lives) should decay to ~0.25, got {}", effective_conf ); } /// Test 3.4: Anecdotal assertion decays to 0.5 at exactly one half-life. /// /// Tier 5 (Anecdotal) has 30-day half-life. /// An assertion with confidence 1.0, timestamped exactly 30 days ago, /// should decay to 0.5 (within tolerance of 0.02). #[test] fn test_decay_tier5_exact_halflife() { let now: u64 = 1_000_000_000; let days_30 = 30 * 86_400; let past = now - days_30; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Anecdotal) .confidence(1.0) .timestamp(past) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); let effective_conf = decayed[0].confidence; assert!( (effective_conf - 0.5).abs() < 0.02, "Anecdotal assertion at 30 days (1 half-life) should decay to ~0.5, got {}", effective_conf ); } /// Test 3.5: Anecdotal assertion decays to 0.125 at exactly three half-lives. /// /// Tier 5 (Anecdotal) has 30-day half-life. /// An assertion with confidence 1.0, timestamped exactly 90 days ago, /// should decay to 0.125 (within tolerance of 0.02). #[test] fn test_decay_tier5_three_halflives() { let now: u64 = 1_000_000_000; let days_90 = 90 * 86_400; let past = now - days_90; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Anecdotal) .confidence(1.0) .timestamp(past) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); let effective_conf = decayed[0].confidence; assert!( (effective_conf - 0.125).abs() < 0.02, "Anecdotal assertion at 90 days (3 half-lives) should decay to ~0.125, got {}", effective_conf ); } /// Test 3.6: Assertion with zero confidence stays zero after decay. /// /// Decay formula: confidence * 2^(-age/halflife) /// If confidence = 0.0, then 0 * anything = 0. #[test] fn test_decay_zero_confidence_stays_zero() { let now: u64 = 1_000_000_000; let days_365 = 365 * 86_400; let past = now - days_365; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Clinical) .confidence(0.0) .timestamp(past) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); assert_eq!( decayed[0].confidence, 0.0, "Zero confidence should remain zero after decay (0 * anything = 0)" ); } /// Test 3.7: Decay never produces negative confidence. /// /// Even with very low initial confidence and extreme age (12+ half-lives), /// the effective confidence should never go below 0.0. #[test] fn test_decay_never_goes_negative() { let now: u64 = 1_000_000_000; let days_365 = 365 * 86_400; let past = now - days_365; let assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Anecdotal) // 30-day half-life .confidence(0.01) .timestamp(past) // 365 days ago = 12+ half-lives .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay(&[assertion], fallback_halflife, now); assert_eq!(decayed.len(), 1); assert!( decayed[0].confidence >= 0.0, "Confidence should never go negative, got {}", decayed[0].confidence ); } /// Test 3.8: Decay uses `now` parameter for age calculation. /// /// Two assertions at T=1000, both with confidence 0.9: /// - Assertion A: Clinical (730-day half-life) /// - Assertion B: Anecdotal (30-day half-life) /// /// Apply decay with `now = T + 730*86400` (exactly 730 days later). /// Assert: /// - A's effective confidence ~ 0.45 (0.9 * 0.5, one half-life) /// - B's effective confidence ~ near zero (0.9 * 2^(-24), 24 half-lives) #[test] fn test_decay_uses_as_of_for_age_calculation() { let base_ts: u64 = 1000; let days_730 = 730 * 86_400; let now = base_ts + days_730; let clinical_assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(1.0) .source_class(SourceClass::Clinical) .confidence(0.9) .timestamp(base_ts) .build(); let anecdotal_assertion = AssertionBuilder::new() .subject("test") .predicate("decay_test") .object_number(2.0) .source_class(SourceClass::Anecdotal) .confidence(0.9) .timestamp(base_ts) .build(); let fallback_halflife: u64 = 365 * 86_400; let decayed = apply_source_class_decay( &[clinical_assertion, anecdotal_assertion], fallback_halflife, now, ); assert_eq!(decayed.len(), 2); // Clinical: 0.9 * 2^(-1) = 0.45 let clinical_conf = decayed[0].confidence; assert!( (clinical_conf - 0.45).abs() < 0.02, "Clinical assertion at 730 days (1 half-life) should decay to ~0.45, got {}", clinical_conf ); // Anecdotal: 0.9 * 2^(-24) ≈ 0.9 * 5.96e-8 ≈ near zero let anecdotal_conf = decayed[1].confidence; assert!( anecdotal_conf < 0.001, "Anecdotal assertion at 730 days (24 half-lives @ 30-day rate) should decay to near zero, got {}", anecdotal_conf ); }