parlov-analysis 0.7.0

Analysis engine trait and signal detection for parlov.
Documentation
use super::*;

// --- 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, _outcome) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert!(result.severity.is_some());
}

#[test]
fn evaluate_complete_confirmed_on_403_vs_404() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result, _outcome) = 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)
        .expect("3-sample set has sufficient samples");
    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, _outcome) = 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, _outcome) = 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, _outcome) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert!(
        result
            .signals
            .iter()
            .any(|s| s.kind == parlov_core::SignalKind::StatusCodeDiff),
        "expected StatusCodeDiff signal"
    );
}

// --- Empty DifferentialSet guard ---

#[test]
fn evaluate_empty_baseline_returns_need_more() {
    let ds = DifferentialSet {
        baseline: vec![],
        probe: vec![fake_exchange(404)],
        canonical: None,
        technique: status_code_diff_technique(),
    };
    assert!(matches!(
        ExistenceAnalyzer.evaluate(&ds),
        SampleDecision::NeedMore
    ));
}

#[test]
fn evaluate_empty_probe_returns_need_more() {
    let ds = DifferentialSet {
        baseline: vec![fake_exchange(200)],
        probe: vec![],
        canonical: None,
        technique: status_code_diff_technique(),
    };
    assert!(matches!(
        ExistenceAnalyzer.evaluate(&ds),
        SampleDecision::NeedMore
    ));
}

// --- StrategyOutcome threading tests ---

/// A `Confirmed` result produces `StrategyOutcome::Positive`.
#[test]
fn confirmed_result_produces_positive_outcome() {
    let ds = diff_set_n(&[403, 403, 403], &[404, 404, 404]);
    let SampleDecision::Complete(result, outcome) = ExistenceAnalyzer.evaluate(&ds) else {
        panic!("expected Complete");
    };
    assert_eq!(result.verdict, OracleVerdict::Confirmed);
    assert!(
        matches!(outcome, StrategyOutcome::Positive(_)),
        "Confirmed verdict must produce Positive outcome"
    );
}

// --- analyze() error path ---

/// `analyze` returns `Err(InsufficientSamples)` when `evaluate` signals `NeedMore`.
///
/// Uses a test-only analyzer whose `required_samples` is 2 and whose `evaluate` always
/// returns `NeedMore` to exercise the default `analyze` error path without depending on
/// `ExistenceAnalyzer`'s sample-count logic.
#[test]
fn analyze_returns_err_when_evaluate_returns_need_more() {
    use crate::AnalyzerError;
    use parlov_core::OracleClass;

    struct AlwaysNeedsMore;

    impl crate::Analyzer for AlwaysNeedsMore {
        fn evaluate(&self, _data: &DifferentialSet) -> SampleDecision {
            SampleDecision::NeedMore
        }
        fn oracle_class(&self) -> OracleClass {
            OracleClass::Existence
        }
        fn required_samples(&self) -> usize {
            2
        }
    }

    let ds = diff_set_1(403, 404);
    let err = AlwaysNeedsMore.analyze(&ds).unwrap_err();
    assert!(
        matches!(err, AnalyzerError::InsufficientSamples { required: 2 }),
        "expected InsufficientSamples {{ required: 2 }}, got: {err:?}"
    );
}