use proptest::prelude::*;
use reputation_core::{Calculator, ConfidenceLevel};
use reputation_types::{AgentData, AgentDataBuilder};
use chrono::{Duration, Utc};
fn arb_agent_data() -> impl Strategy<Value = AgentData> {
(
any::<u32>(), any::<u32>(), 1.0..=5.0, any::<bool>(), any::<bool>(), any::<bool>(), 0u8..=3, 0..400i64, ).prop_map(|(interactions, reviews, rating, id, sec, os, mcp, days): (u32, u32, f64, bool, bool, bool, u8, i64)| {
let reviews = reviews.min(interactions);
let mut builder = AgentDataBuilder::new("did:test:prop")
.total_interactions(interactions);
if reviews > 0 {
let rating_normalized = (rating - 1.0) / 4.0;
let positive = (rating_normalized * reviews as f64).round() as u32;
let negative = reviews.saturating_sub(positive);
builder = builder
.total_reviews(reviews)
.positive_reviews(positive)
.negative_reviews(negative)
.average_rating(rating);
}
builder
.identity_verified(id)
.security_audit_passed(sec)
.open_source(os)
.mcp_level(mcp)
.created_at(Utc::now() - Duration::days(days))
.build()
.unwrap()
})
}
proptest! {
#[test]
fn prop_confidence_level_monotonic(interactions in 0u32..10000) {
let calc = Calculator::default();
let conf = interactions as f64 / (interactions as f64 + calc.confidence_k());
let level = ConfidenceLevel::from_confidence(conf);
if conf < 0.2 {
assert_eq!(level, ConfidenceLevel::Low);
} else if conf < 0.7 {
assert_eq!(level, ConfidenceLevel::Medium);
} else {
assert_eq!(level, ConfidenceLevel::High);
}
}
#[test]
fn prop_is_provisional_consistent_with_confidence(agent in arb_agent_data()) {
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
assert_eq!(score.is_provisional, score.confidence < 0.2);
assert_eq!(score.is_provisional, score.level == ConfidenceLevel::Low);
}
}
#[test]
fn prop_data_points_equals_sum(agent in arb_agent_data()) {
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
let expected = agent.total_interactions.saturating_add(agent.total_reviews);
assert_eq!(score.data_points, expected);
}
}
#[test]
fn prop_prior_breakdown_total_bounded(agent in arb_agent_data()) {
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
let breakdown = &score.components.prior_breakdown;
let sum = breakdown.base_score
+ breakdown.mcp_bonus
+ breakdown.identity_bonus
+ breakdown.security_audit_bonus
+ breakdown.open_source_bonus
+ breakdown.age_bonus;
assert_eq!(breakdown.total, sum.min(calc.prior_max()));
assert!(breakdown.total >= calc.prior_base());
assert!(breakdown.total <= calc.prior_max());
}
}
#[test]
fn prop_components_match_score_fields(agent in arb_agent_data()) {
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
assert_eq!(score.components.confidence_level, score.level);
assert_eq!(score.components.confidence_value, score.confidence);
assert_eq!(score.components.prior_score,
score.components.prior_breakdown.total);
}
}
#[test]
fn prop_score_calculation_from_components(agent in arb_agent_data()) {
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
let recalculated = (1.0 - score.confidence) * score.components.prior_score
+ score.confidence * score.components.empirical_score;
assert!((score.score - recalculated).abs() < 0.0001);
}
}
#[test]
fn prop_mcp_bonus_correct(mcp_level in 0u8..=3, agent in arb_agent_data()) {
let mut agent = agent;
agent.mcp_level = Some(mcp_level);
let calc = Calculator::default();
if let Ok(score) = calc.calculate(&agent) {
let expected_bonus = match mcp_level {
1 => 5.0,
2 => 10.0,
3 => 15.0,
_ => 0.0,
};
assert_eq!(score.components.prior_breakdown.mcp_bonus, expected_bonus);
}
}
#[test]
fn prop_confidence_affects_provisional_status(
reviews in 0u32..100,
rating in 1.0..=5.0
) {
let mut builder = AgentDataBuilder::new("did:test:provisional")
.total_interactions(reviews);
if reviews > 0 {
builder = builder.with_reviews(reviews, rating);
}
let agent = builder.build().unwrap();
let calc = Calculator::default();
let score = calc.calculate(&agent).unwrap();
let expected_conf = reviews as f64 / (reviews as f64 + 15.0);
let should_be_provisional = expected_conf < 0.2;
assert_eq!(score.is_provisional, should_be_provisional);
}
#[test]
fn prop_batch_preserves_component_details(agents in prop::collection::vec(arb_agent_data(), 1..50)) {
let calc = Calculator::default();
let individual: Vec<_> = agents.iter()
.map(|a| calc.calculate(a))
.collect();
let batch = calc.calculate_batch(&agents);
for (ind, bat) in individual.iter().zip(batch.iter()) {
match (ind, bat) {
(Ok(i), Ok(b)) => {
assert_eq!(i.components.prior_score, b.components.prior_score);
assert_eq!(i.components.empirical_score, b.components.empirical_score);
assert_eq!(i.components.confidence_level, b.components.confidence_level);
assert_eq!(i.components.prior_breakdown.total,
b.components.prior_breakdown.total);
assert_eq!(i.is_provisional, b.is_provisional);
assert_eq!(i.data_points, b.data_points);
}
_ => {} }
}
}
#[test]
fn prop_builder_produces_valid_components(
k in 0.1f64..100.0,
base in 0.0f64..100.0,
max_offset in 0.1f64..50.0
) {
let max = (base + max_offset).min(100.0);
if let Ok(calc) = Calculator::builder()
.confidence_k(k)
.prior_base(base)
.prior_max(max)
.build() {
let agent = AgentDataBuilder::new("did:test:builder")
.with_reviews(50, 4.0)
.total_interactions(50)
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
assert!(score.components.prior_breakdown.base_score >= base);
assert!(score.components.prior_score <= max);
assert!(score.components.empirical_score >= 0.0);
assert!(score.components.empirical_score <= 100.0);
}
}
}