parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Unit tests for signal family definitions and family-adjusted scoring.

use super::*;

#[test]
fn impact_saturates_at_255_without_overflow() {
    // 16 signals each contributing impact=20 would overflow u8 (16*20=320>255).
    // saturating_add must clamp to 255, not wrap or panic.
    let contributions: Vec<SignalContribution<'_>> = (0..16)
        .map(|_| SignalContribution {
            family: SignalFamily::General,
            confidence: 0.0,
            impact: 20,
            description: "overflow-probe",
        })
        .collect();
    let result = apply_family_adjustment(&contributions);
    assert_eq!(result.impact_total, 255, "expected saturation at 255");
}

#[test]
fn first_signal_gets_full_points() {
    let contributions = vec![SignalContribution {
        family: SignalFamily::Range,
        confidence: 12.0,
        impact: 8,
        description: "test",
    }];
    let result = apply_family_adjustment(&contributions);
    assert!((result.confidence_total - 12.0).abs() < 0.01);
    assert_eq!(result.impact_total, 8);
}

#[test]
fn second_signal_in_family_gets_half() {
    let contributions = vec![
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 12.0,
            impact: 8,
            description: "first",
        },
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 10.0,
            impact: 5,
            description: "second",
        },
    ];
    let result = apply_family_adjustment(&contributions);
    assert!((result.confidence_total - 17.0).abs() < 0.01);
    assert_eq!(result.impact_total, 13);
}

#[test]
fn third_signal_in_family_gets_zero_confidence() {
    let contributions = vec![
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 12.0,
            impact: 8,
            description: "first",
        },
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 10.0,
            impact: 5,
            description: "second",
        },
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 8.0,
            impact: 15,
            description: "third",
        },
    ];
    let result = apply_family_adjustment(&contributions);
    assert!((result.confidence_total - 17.0).abs() < 0.01);
    // Impact still counts: 8 + 5 + 15 = 28
    assert_eq!(result.impact_total, 28);
}

#[test]
fn different_families_count_independently() {
    let contributions = vec![
        SignalContribution {
            family: SignalFamily::Range,
            confidence: 12.0,
            impact: 8,
            description: "range",
        },
        SignalContribution {
            family: SignalFamily::Auth,
            confidence: 8.0,
            impact: 8,
            description: "auth",
        },
    ];
    let result = apply_family_adjustment(&contributions);
    assert!((result.confidence_total - 20.0).abs() < 0.01);
    assert_eq!(result.family_count, 2);
}

#[test]
fn corroboration_bonus_values() {
    assert_eq!(corroboration_bonus(0), 0);
    assert_eq!(corroboration_bonus(1), 0);
    assert_eq!(corroboration_bonus(2), 3);
    assert_eq!(corroboration_bonus(3), 6);
    assert_eq!(corroboration_bonus(5), 8);
}

#[test]
fn header_family_mappings() {
    assert_eq!(header_family("content-range"), SignalFamily::Range);
    assert_eq!(header_family("etag"), SignalFamily::CacheValidator);
    assert_eq!(header_family("www-authenticate"), SignalFamily::Auth);
    assert_eq!(header_family("x-custom"), SignalFamily::General);
}

#[test]
fn redirect_family_is_distinct() {
    assert_ne!(SignalFamily::Redirect, SignalFamily::Range);
    assert_ne!(SignalFamily::Redirect, SignalFamily::CacheValidator);
    assert_ne!(SignalFamily::Redirect, SignalFamily::Auth);
    assert_ne!(SignalFamily::Redirect, SignalFamily::Precondition);
    assert_ne!(SignalFamily::Redirect, SignalFamily::Negotiation);
    assert_ne!(SignalFamily::Redirect, SignalFamily::ErrorBody);
    assert_ne!(SignalFamily::Redirect, SignalFamily::General);
}

#[test]
fn status_code_family_redirect_301() {
    assert_eq!(status_code_family(301), SignalFamily::Redirect);
}

#[test]
fn status_code_family_redirect_302() {
    assert_eq!(status_code_family(302), SignalFamily::Redirect);
}

#[test]
fn status_code_family_redirect_303() {
    assert_eq!(status_code_family(303), SignalFamily::Redirect);
}

#[test]
fn status_code_family_redirect_307() {
    assert_eq!(status_code_family(307), SignalFamily::Redirect);
}

#[test]
fn status_code_family_redirect_308() {
    assert_eq!(status_code_family(308), SignalFamily::Redirect);
}

#[test]
fn status_code_family_redirect_300() {
    assert_eq!(status_code_family(300), SignalFamily::Redirect);
}

#[test]
fn header_family_location() {
    assert_eq!(header_family("location"), SignalFamily::Redirect);
}

#[test]
fn error_body_family_is_distinct() {
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::Range);
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::CacheValidator);
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::Auth);
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::Precondition);
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::Negotiation);
    assert_ne!(SignalFamily::ErrorBody, SignalFamily::General);
}