use parlov_core::{
ImpactClass, NormativeStrength, OracleClass, OracleResult, OracleVerdict, Severity,
StrategyMetaForStop, StrategyOutcome, Vector,
};
use super::*;
fn make_result(confidence: u8) -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::Confirmed,
severity: Some(Severity::High),
confidence,
impact_class: Some(ImpactClass::High),
reasons: vec![],
signals: vec![],
technique_id: None,
vector: None,
normative_strength: None,
label: None,
leaks: None,
rfc_basis: None,
}
}
fn meta(v: Vector) -> StrategyMetaForStop {
StrategyMetaForStop {
vector: v,
normative_strength: NormativeStrength::Must,
}
}
fn acc_with_log_odds(target: f64) -> EvidenceAccumulator {
let vectors = [
Vector::StatusCodeDiff,
Vector::CacheProbing,
Vector::ErrorMessageGranularity,
Vector::RedirectDiff,
];
let mut acc = EvidenceAccumulator::new();
let pos = StrategyOutcome::Positive(make_result(99));
let mut idx = 0usize;
while acc.log_odds_current() < target {
let vector = vectors[idx % vectors.len()];
acc.ingest(&pos, vector);
idx += 1;
if idx > vectors.len() * 2 + 2 {
break;
}
}
debug_assert!(
acc.log_odds_current() >= target,
"acc_with_log_odds: target {target} is unreachable with available vectors/families"
);
acc
}
fn acc_with_negative_log_odds(target: f64) -> EvidenceAccumulator {
assert!(target <= 0.0, "use acc_with_log_odds for positive targets");
let vectors = [
Vector::StatusCodeDiff,
Vector::CacheProbing,
Vector::ErrorMessageGranularity,
Vector::RedirectDiff,
];
let mut acc = EvidenceAccumulator::new();
let contra = StrategyOutcome::Contradictory(make_result(99), 1.0);
let mut idx = 0usize;
while acc.log_odds_current() > target {
let vector = vectors[idx % vectors.len()];
acc.ingest(&contra, vector);
idx += 1;
if idx > vectors.len() * 2 + 2 {
break;
}
}
debug_assert!(
acc.log_odds_current() <= target,
"acc_with_negative_log_odds: target {target} is unreachable with available vectors/families"
);
acc
}
#[test]
fn fresh_accumulator_no_remaining_is_early_reject() {
let acc = EvidenceAccumulator::new();
let rule = StopRule::new();
assert!(
matches!(rule.evaluate(&acc, &[]), StopDecision::EarlyReject { .. }),
"expected EarlyReject when no evidence and no remaining strategies"
);
}
#[test]
fn high_log_odds_with_small_max_neg_is_early_accept() {
let acc = acc_with_log_odds(2.0);
let rule = StopRule::new();
let decision = rule.evaluate(&acc, &[]);
assert!(
matches!(decision, StopDecision::EarlyAccept { .. }),
"expected EarlyAccept with log_odds={}, got {decision:?}",
acc.log_odds_current()
);
}
#[test]
fn neutral_accumulator_no_remaining_is_early_reject() {
let acc = EvidenceAccumulator::new();
let rule = StopRule::new();
let decision = rule.evaluate(&acc, &[]);
assert!(
matches!(decision, StopDecision::EarlyReject { .. }),
"expected EarlyReject with neutral log_odds and no remaining, got {decision:?}"
);
}
#[test]
fn log_odds_at_confirm_threshold_is_early_accept() {
let acc = acc_with_log_odds(CONFIRM_THRESHOLD);
let rule = StopRule::new();
let decision = rule.evaluate(&acc, &[]);
assert!(
matches!(decision, StopDecision::EarlyAccept { .. }),
"expected EarlyAccept at confirm boundary, log_odds={}, got {decision:?}",
acc.log_odds_current()
);
}
#[test]
fn large_remaining_potential_is_continue() {
let acc = EvidenceAccumulator::new();
let rule = StopRule::new();
let remaining: Vec<StrategyMetaForStop> = (0..10)
.flat_map(|_| {
vec![
meta(Vector::CacheProbing),
meta(Vector::StatusCodeDiff),
meta(Vector::ErrorMessageGranularity),
meta(Vector::RedirectDiff),
]
})
.collect();
assert_eq!(
rule.evaluate(&acc, &remaining),
StopDecision::Continue,
"many remaining strategies should keep the decision as Continue"
);
}
#[test]
fn early_accept_posterior_above_half() {
let acc = acc_with_log_odds(2.0);
let rule = StopRule::new();
let decision = rule.evaluate(&acc, &[]);
if let StopDecision::EarlyAccept { posterior } = decision {
assert!(
posterior > 0.5 && posterior <= 1.0,
"EarlyAccept posterior {posterior} not in (0.5, 1.0]"
);
} else {
panic!("expected EarlyAccept, got {decision:?}");
}
}
#[test]
fn early_reject_posterior_below_half() {
let acc = acc_with_negative_log_odds(-1.0);
let rule = StopRule::new();
let decision = rule.evaluate(&acc, &[]);
if let StopDecision::EarlyReject { posterior } = decision {
assert!(
(0.0..0.5).contains(&posterior),
"EarlyReject posterior {posterior} not in [0.0, 0.5)"
);
} else {
panic!("expected EarlyReject, got {decision:?}");
}
}
#[test]
fn early_reject_boundary_is_exclusive_at_likely_threshold() {
let acc = EvidenceAccumulator::new();
let remaining = vec![meta(Vector::StatusCodeDiff)];
let max_pos = acc.max_positive_remaining(&remaining);
let rule = StopRule {
confirm_threshold: CONFIRM_THRESHOLD,
likely_threshold: 0.0 + max_pos, };
let decision = rule.evaluate(&acc, &remaining);
assert!(
matches!(decision, StopDecision::Continue),
"EarlyReject must not fire at the exact boundary (boundary is exclusive, < not <=); got {decision:?}"
);
}