parlov-analysis 0.6.0

Analysis engine trait and signal detection for parlov.
Documentation
//! Unit tests for `ExistenceAnalyzer` — stability, short-circuit, scoring.

use super::*;
use parlov_core::OracleVerdict;

use crate::signals::tests::{diff_set_with_statuses, fake_exchange, status_code_diff_technique};

fn diff_set_1(baseline_status: u16, probe_status: u16) -> DifferentialSet {
    DifferentialSet {
        baseline: vec![fake_exchange(baseline_status)],
        probe: vec![fake_exchange(probe_status)],
        technique: status_code_diff_technique(),
    }
}

fn diff_set_n(baseline_statuses: &[u16], probe_statuses: &[u16]) -> DifferentialSet {
    diff_set_with_statuses(baseline_statuses, probe_statuses)
}

// --- Stability / NeedMore tests ---

#[test]
fn evaluate_1_sample_diff_returns_need_more() {
    let ds = diff_set_1(403, 404);
    assert!(matches!(
        ExistenceAnalyzer.evaluate(&ds),
        SampleDecision::NeedMore
    ));
}

#[test]
fn evaluate_2_samples_diff_returns_need_more() {
    let ds = diff_set_n(&[403, 403], &[404, 404]);
    assert!(matches!(
        ExistenceAnalyzer.evaluate(&ds),
        SampleDecision::NeedMore
    ));
}

#[test]
fn evaluate_3_samples_stable_diff_confirmed() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert!(result.severity.is_some());
}

#[test]
fn evaluate_3_samples_unstable_probe_not_present() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 200, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::NotPresent);
    assert_eq!(result.severity, None);
}

#[test]
fn evaluate_3_samples_unstable_baseline_not_present() {
    let ds = diff_set_n(&[403, 200, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::NotPresent);
    assert_eq!(result.severity, None);
}

// --- Same-status short-circuit ---

#[test]
fn evaluate_complete_not_present_on_same_status() {
    let ds = diff_set_1(404, 404);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::NotPresent);
    assert_eq!(result.severity, None);
}

#[test]
fn evaluate_complete_confirmed_on_403_vs_404() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert!(result.severity.is_some());
}

#[test]
fn analyze_provided_method_delegates_to_evaluate() {
    let ds = diff_set_n(&[200, 200, 200], &[404, 404, 404]);
    let result = ExistenceAnalyzer.analyze(&ds);
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert!(result.severity.is_some());
}

#[test]
fn need_more_variant_is_constructible() {
    assert!(matches!(SampleDecision::NeedMore, SampleDecision::NeedMore));
}

// --- Technique context propagation ---

#[test]
fn result_carries_technique_metadata() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert!(result.technique_id.is_some());
    assert!(result.vector.is_some());
    assert!(result.normative_strength.is_some());
}

// --- Scoring fields populated ---

#[test]
fn result_carries_confidence_and_impact() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert!(result.confidence > 0);
    assert!(result.impact_class.is_some());
    assert!(!result.reasons.is_empty());
}

// --- Signal extraction ---

#[test]
fn result_carries_status_code_diff_signal() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert!(
        result
            .signals
            .iter()
            .any(|s| s.kind == parlov_core::SignalKind::StatusCodeDiff),
        "expected StatusCodeDiff signal"
    );
}

#[test]
fn unstable_result_carries_signal() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 200, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::NotPresent);
    assert!(
        result.primary_evidence().contains("unstable"),
        "unstable result should carry unstable signal"
    );
}

// --- RedirectDiff relevance guard ---

fn redirect_diff_technique() -> parlov_core::Technique {
    use parlov_core::{NormativeStrength, OracleClass, Technique, Vector};
    Technique {
        id: "test-redirect-diff",
        name: "Test redirect diff",
        oracle_class: OracleClass::Existence,
        vector: Vector::RedirectDiff,
        strength: NormativeStrength::Should,
    }
}

fn redirect_diff_set_n(baseline_statuses: &[u16], probe_statuses: &[u16]) -> DifferentialSet {
    use crate::signals::tests::fake_exchange;
    DifferentialSet {
        baseline: baseline_statuses
            .iter()
            .map(|&s| fake_exchange(s))
            .collect(),
        probe: probe_statuses
            .iter()
            .map(|&s| fake_exchange(s))
            .collect(),
        technique: redirect_diff_technique(),
    }
}

/// When a `RedirectDiff` probe produces a stable differential but neither side is 3xx,
/// the technique did not fire — `evaluate` must return `NotPresent`, not `Likely`/`Confirmed`.
#[test]
fn redirect_diff_no_3xx_on_either_side_is_not_present() {
    // 200 vs 412: URL manipulation triggered a precondition failure, not redirect behavior.
    let ds = redirect_diff_set_n(&[200, 200, 200], &[412, 412, 412]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(
        result.verdict,
        OracleVerdict::NotPresent,
        "stable 200 vs 412 under RedirectDiff must be NotPresent: technique did not fire"
    );
    assert_eq!(result.severity, None);
    assert!(
        result.primary_evidence().contains("did not fire"),
        "signal evidence must explain dismissal; got: {:?}",
        result.primary_evidence()
    );
}

/// When the baseline is 3xx the technique fired — the differential proceeds to normal scoring.
#[test]
fn redirect_diff_baseline_3xx_proceeds_to_scoring() {
    // 301 vs 404: classic redirect-diff existence oracle.
    let ds = redirect_diff_set_n(&[301, 301, 301], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_ne!(
        result.verdict,
        OracleVerdict::NotPresent,
        "301 vs 404 under RedirectDiff is a real differential; must not be dismissed"
    );
}

/// When the probe is 3xx the technique fired — the differential proceeds to normal scoring.
#[test]
fn redirect_diff_probe_3xx_proceeds_to_scoring() {
    // 302 vs 404: probe redirected, baseline got not-found.
    // The (FOUND, NOT_FOUND) pattern has base_confidence 80 → Confirmed.
    let ds = redirect_diff_set_n(&[302, 302, 302], &[404, 404, 404]);
    let SampleDecision::Complete(result) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_ne!(
        result.verdict,
        OracleVerdict::NotPresent,
        "302 vs 404 under RedirectDiff is a real differential; must not be dismissed"
    );
}