use parlov_core::{
NormativeStrength, OracleClass, OracleResult, OracleVerdict, Severity, StrategyOutcome, Vector,
};
use proptest::prelude::*;
use super::*;
fn make_contradictory_result(confidence: u8) -> OracleResult {
OracleResult {
class: OracleClass::Existence,
verdict: OracleVerdict::NotPresent,
severity: Some(Severity::Low),
confidence,
impact_class: None,
reasons: vec![],
signals: vec![],
technique_id: None,
vector: Some(Vector::StatusCodeDiff),
normative_strength: Some(NormativeStrength::Must),
label: None,
leaks: None,
rfc_basis: None,
}
}
#[test]
fn contradictory_low_confidence_produces_negative_contribution() {
let mut acc = EvidenceAccumulator::new();
let outcome = StrategyOutcome::Contradictory(make_contradictory_result(28), 1.0);
acc.ingest(&outcome, Vector::StatusCodeDiff);
assert!(
acc.log_odds_current() < 0.0,
"low-confidence Contradictory must lower log_odds; got {}",
acc.log_odds_current()
);
}
#[test]
fn contradictory_high_confidence_produces_negative_contribution() {
let mut acc = EvidenceAccumulator::new();
let outcome = StrategyOutcome::Contradictory(make_contradictory_result(85), 1.0);
acc.ingest(&outcome, Vector::StatusCodeDiff);
assert!(
acc.log_odds_current() < 0.0,
"high-confidence Contradictory must lower log_odds; got {}",
acc.log_odds_current()
);
}
#[test]
fn contradictory_magnitude_proportional_to_weight() {
let mut light = EvidenceAccumulator::new();
let mut heavy = EvidenceAccumulator::new();
let r = make_contradictory_result(30);
light.ingest(
&StrategyOutcome::Contradictory(r.clone(), 0.15),
Vector::StatusCodeDiff,
);
heavy.ingest(
&StrategyOutcome::Contradictory(r, 1.0),
Vector::StatusCodeDiff,
);
assert!(
heavy.log_odds_current() < light.log_odds_current(),
"weight=1.0 must produce a stronger negative shift than weight=0.15: \
heavy={}, light={}",
heavy.log_odds_current(),
light.log_odds_current()
);
}
#[test]
fn contradictory_magnitude_independent_of_confidence() {
let mut a = EvidenceAccumulator::new();
let mut b = EvidenceAccumulator::new();
a.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(10), 0.5),
Vector::StatusCodeDiff,
);
b.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(49), 0.5),
Vector::StatusCodeDiff,
);
assert!(
(a.log_odds_current() - b.log_odds_current()).abs() < 1e-9,
"confidence influenced magnitude: c=10 gave {}, c=49 gave {}",
a.log_odds_current(),
b.log_odds_current()
);
}
#[test]
fn juice_shop_two_contradictory_outcomes_drop_verdict() {
let mut acc = EvidenceAccumulator::new();
acc.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(28), 0.2),
Vector::StatusCodeDiff,
);
acc.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(39), 0.2),
Vector::StatusCodeDiff,
);
assert!(
acc.posterior_probability() < 0.5,
"two Contradictory outcomes must keep posterior < 0.5; got {}",
acc.posterior_probability()
);
}
#[test]
fn two_contradictory_at_new_max_weight_exceed_old_ceiling() {
let mut acc = EvidenceAccumulator::new();
acc.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(28), 0.25),
Vector::StatusCodeDiff,
);
acc.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(28), 0.25),
Vector::StatusCodeDiff,
);
let lo = acc.log_odds_current();
assert!(
lo < -0.35,
"two Contradictory at weight 0.25 must push log_odds below -0.35 (new ceiling -0.375); \
got {lo}"
);
}
proptest! {
#[test]
fn prop_contradictory_never_positive_contribution(
confidence in 0u8..=100,
weight in 0.01f32..=2.0,
) {
let mut acc = EvidenceAccumulator::new();
let outcome =
StrategyOutcome::Contradictory(make_contradictory_result(confidence), weight);
acc.ingest(&outcome, Vector::StatusCodeDiff);
prop_assert!(
acc.log_odds_current() <= 0.0,
"Contradictory contributed {} to log_odds (must be <= 0)",
acc.log_odds_current()
);
}
#[test]
fn prop_contradictory_magnitude_scales_with_weight(
weight_ratio in 0.5f32..=2.0,
) {
let mut a1 = EvidenceAccumulator::new();
let mut a2 = EvidenceAccumulator::new();
let r = make_contradictory_result(30);
let base: f32 = 0.3;
a1.ingest(
&StrategyOutcome::Contradictory(r.clone(), base),
Vector::StatusCodeDiff,
);
a2.ingest(
&StrategyOutcome::Contradictory(r, base * weight_ratio),
Vector::StatusCodeDiff,
);
let l1 = a1.log_odds_current().abs();
let l2 = a2.log_odds_current().abs();
prop_assert!(
(l2 - l1 * f64::from(weight_ratio)).abs() < 1e-6,
"expected ratio {weight_ratio}, got {} (l1={l1}, l2={l2})",
l2 / l1
);
}
#[test]
fn prop_contradictory_magnitude_independent_of_confidence(
c1 in 0u8..=100,
c2 in 0u8..=100,
) {
let mut a = EvidenceAccumulator::new();
let mut b = EvidenceAccumulator::new();
a.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(c1), 0.5),
Vector::StatusCodeDiff,
);
b.ingest(
&StrategyOutcome::Contradictory(make_contradictory_result(c2), 0.5),
Vector::StatusCodeDiff,
);
prop_assert!(
(a.log_odds_current() - b.log_odds_current()).abs() < 1e-9,
"confidence influenced magnitude: c1={c1}, c2={c2}, l1={}, l2={}",
a.log_odds_current(),
b.log_odds_current()
);
}
}