use reputation_core::Calculator;
use reputation_types::{AgentData, AgentDataBuilder, ReputationScore};
use chrono::{Duration, Utc, DateTime};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use std::fs;
use std::path::Path;
#[derive(Debug, Serialize, Deserialize)]
pub struct TestVectorFile {
pub version: String,
pub algorithm: String,
pub generated: DateTime<Utc>,
pub calculator_config: CalculatorConfig,
pub test_categories: HashMap<String, String>,
pub test_cases: Vec<TestCase>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CalculatorConfig {
pub confidence_k: f64,
pub prior_base: f64,
pub prior_max: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TestCase {
pub id: String,
pub category: String,
pub description: String,
pub input: TestInput,
pub expected: ExpectedResult,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct TestInput {
pub did: String,
pub created_at: DateTime<Utc>,
pub mcp_level: Option<u8>,
pub identity_verified: bool,
pub security_audit_passed: bool,
pub open_source: bool,
pub total_interactions: u32,
pub total_reviews: u32,
pub average_rating: Option<f64>,
pub positive_reviews: u32,
pub negative_reviews: u32,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ExpectedResult {
Success(ExpectedScore),
Error(ExpectedError),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExpectedScore {
pub score: f64,
pub confidence: f64,
pub level: String,
pub components: ScoreComponents,
pub is_provisional: bool,
pub data_points: u32,
pub algorithm_version: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ScoreComponents {
pub prior_score: f64,
pub prior_breakdown: PriorBreakdown,
pub empirical_score: f64,
pub confidence_value: f64,
pub confidence_level: String,
pub prior_weight: f64,
pub empirical_weight: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PriorBreakdown {
pub base_score: f64,
pub mcp_bonus: f64,
pub identity_bonus: f64,
pub security_audit_bonus: f64,
pub open_source_bonus: f64,
pub age_bonus: f64,
pub total: f64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExpectedError {
pub error_type: String,
pub message: String,
}
#[derive(Debug)]
pub struct ValidationReport {
pub total: usize,
pub passed: usize,
pub failed: usize,
pub failures: Vec<ValidationFailure>,
}
#[derive(Debug)]
pub struct ValidationFailure {
pub test_id: String,
pub reason: String,
pub expected: String,
pub actual: String,
}
impl TestVectorFile {
pub fn new() -> Self {
Self {
version: "1.0.0".to_string(),
algorithm: "bayesian_reputation_v1".to_string(),
generated: Utc::now(),
calculator_config: CalculatorConfig {
confidence_k: 15.0,
prior_base: 50.0,
prior_max: 80.0,
},
test_categories: HashMap::from([
("new_agents".to_string(), "Agents with no interaction history".to_string()),
("verified_agents".to_string(), "Agents with identity verification".to_string()),
("high_interaction".to_string(), "Agents with significant activity".to_string()),
("edge_cases".to_string(), "Boundary conditions and limits".to_string()),
("error_cases".to_string(), "Invalid inputs and expected errors".to_string()),
("confidence_levels".to_string(), "Different confidence level examples".to_string()),
("review_patterns".to_string(), "Various rating distributions".to_string()),
]),
test_cases: Vec::new(),
}
}
pub fn add_case(&mut self, case: TestCase) {
self.test_cases.push(case);
}
}
impl From<&AgentData> for TestInput {
fn from(agent: &AgentData) -> Self {
Self {
did: agent.did.clone(),
created_at: agent.created_at,
mcp_level: agent.mcp_level,
identity_verified: agent.identity_verified,
security_audit_passed: agent.security_audit_passed,
open_source: agent.open_source,
total_interactions: agent.total_interactions,
total_reviews: agent.total_reviews,
average_rating: agent.average_rating,
positive_reviews: agent.positive_reviews,
negative_reviews: agent.negative_reviews,
}
}
}
fn score_to_expected(score: &ReputationScore) -> ExpectedScore {
ExpectedScore {
score: score.score,
confidence: score.confidence,
level: format!("{:?}", score.level),
components: ScoreComponents {
prior_score: score.components.prior_score,
prior_breakdown: PriorBreakdown {
base_score: score.components.prior_breakdown.base_score,
mcp_bonus: score.components.prior_breakdown.mcp_bonus,
identity_bonus: score.components.prior_breakdown.identity_bonus,
security_audit_bonus: score.components.prior_breakdown.security_audit_bonus,
open_source_bonus: score.components.prior_breakdown.open_source_bonus,
age_bonus: score.components.prior_breakdown.age_bonus,
total: score.components.prior_breakdown.total,
},
empirical_score: score.components.empirical_score,
confidence_value: score.components.confidence_value,
confidence_level: format!("{:?}", score.components.confidence_level),
prior_weight: score.components.prior_weight,
empirical_weight: score.components.empirical_weight,
},
is_provisional: score.is_provisional,
data_points: score.data_points,
algorithm_version: score.algorithm_version.clone(),
}
}
pub fn generate_test_vectors() -> TestVectorFile {
let mut vectors = TestVectorFile::new();
let calc = Calculator::default();
let mut case_id = 1;
let agent = AgentDataBuilder::new("did:test:new-agent")
.created_at(Utc::now())
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "new_agents".to_string(),
description: "Brand new agent with no history".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:week-old")
.created_at(Utc::now() - Duration::days(7))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "new_agents".to_string(),
description: "Week-old agent with no activity".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:month-old")
.created_at(Utc::now() - Duration::days(30))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "new_agents".to_string(),
description: "Month-old agent with no activity".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
for mcp_level in 1..=3 {
let agent = AgentDataBuilder::new(&format!("did:test:mcp-{}", mcp_level))
.mcp_level(mcp_level)
.created_at(Utc::now() - Duration::days(90))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "verified_agents".to_string(),
description: format!("Agent with MCP Level {}", mcp_level),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
let agent = AgentDataBuilder::new("did:test:identity-verified")
.identity_verified(true)
.created_at(Utc::now() - Duration::days(60))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "verified_agents".to_string(),
description: "Agent with identity verification".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:fully-verified")
.mcp_level(3)
.identity_verified(true)
.security_audit_passed(true)
.open_source(true)
.created_at(Utc::now() - Duration::days(180))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "verified_agents".to_string(),
description: "Fully verified agent with all credentials".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
for rating in 1..=5 {
let agent = AgentDataBuilder::new(&format!("did:test:rating-{}", rating))
.total_interactions(100)
.with_reviews(50, rating as f64)
.created_at(Utc::now() - Duration::days(30))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "review_patterns".to_string(),
description: format!("Agent with {} star rating", rating),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
let agent = AgentDataBuilder::new("did:test:mixed-reviews")
.total_interactions(200)
.with_reviews(100, 3.5)
.created_at(Utc::now() - Duration::days(60))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "review_patterns".to_string(),
description: "Agent with mixed reviews (3.5 stars)".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let confidence_cases = [
(1, "Low confidence (1 interaction)"),
(10, "Low-Medium confidence (10 interactions)"),
(50, "Medium confidence (50 interactions)"),
(200, "High confidence (200 interactions)"),
(1000, "Very high confidence (1000 interactions)"),
];
for (interactions, desc) in confidence_cases {
let mut builder = AgentDataBuilder::new(&format!("did:test:conf-{}", interactions))
.total_interactions(interactions)
.created_at(Utc::now() - Duration::days(90));
if interactions > 0 {
let reviews = (interactions / 2).max(1); builder = builder.with_reviews(reviews, 4.0);
}
let agent = builder.build().unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "confidence_levels".to_string(),
description: desc.to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
let agent = AgentDataBuilder::new("did:test:no-reviews")
.total_interactions(500)
.created_at(Utc::now() - Duration::days(30))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: "Many interactions but no reviews".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:max-values")
.total_interactions(1_000_000)
.with_reviews(500_000, 5.0)
.mcp_level(3)
.identity_verified(true)
.security_audit_passed(true)
.open_source(true)
.created_at(Utc::now() - Duration::days(730))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: "Maximum allowed values".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:one-review")
.total_interactions(1)
.with_reviews(1, 5.0)
.created_at(Utc::now() - Duration::days(1))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: "Single perfect review".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:prior-cap")
.mcp_level(3)
.identity_verified(true)
.security_audit_passed(true)
.open_source(true)
.created_at(Utc::now() - Duration::days(365))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: "Agent reaching prior score cap".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
let agent = AgentDataBuilder::new("did:test:boundary-conf")
.total_interactions(4) .created_at(Utc::now() - Duration::days(7))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: "Boundary confidence level".to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
generate_additional_test_cases(&mut vectors, &calc, case_id);
vectors
}
fn generate_additional_test_cases(vectors: &mut TestVectorFile, calc: &Calculator, mut case_id: usize) {
case_id += 1;
let interaction_levels = [100, 500, 1000, 5000, 10000];
for interactions in interaction_levels {
let agent = AgentDataBuilder::new(&format!("did:test:high-int-{}", interactions))
.total_interactions(interactions)
.with_reviews((interactions as f64 * 0.3) as u32, 4.2)
.created_at(Utc::now() - Duration::days(180))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "high_interaction".to_string(),
description: format!("Agent with {} interactions", interactions),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
let credential_combos = [
(Some(1), false, false, false, "MCP Level 1 only"),
(None, true, false, false, "Identity verified only"),
(None, false, true, false, "Security audit only"),
(None, false, false, true, "Open source only"),
(Some(2), true, false, false, "MCP Level 2 + Identity"),
(Some(3), true, true, true, "All credentials maxed"),
];
for (mcp, id, sec, os, desc) in credential_combos {
let mut builder = AgentDataBuilder::new(&format!("did:test:cred-{}", case_id))
.total_interactions(50)
.with_reviews(25, 4.0)
.created_at(Utc::now() - Duration::days(90));
if let Some(level) = mcp {
builder = builder.mcp_level(level);
}
builder = builder
.identity_verified(id)
.security_audit_passed(sec)
.open_source(os);
let agent = builder.build().unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "verified_agents".to_string(),
description: desc.to_string(),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
let age_days = [0, 30, 60, 90, 180, 365];
for days in age_days {
let agent = AgentDataBuilder::new(&format!("did:test:age-{}", days))
.created_at(Utc::now() - Duration::days(days))
.build()
.unwrap();
let score = calc.calculate(&agent).unwrap();
vectors.add_case(TestCase {
id: format!("TC{:03}", case_id),
category: "edge_cases".to_string(),
description: format!("Agent aged {} days", days),
input: TestInput::from(&agent),
expected: ExpectedResult::Success(score_to_expected(&score)),
});
case_id += 1;
}
}
pub fn validate_test_vectors(calc: &Calculator, vectors: &TestVectorFile) -> ValidationReport {
let mut report = ValidationReport {
total: vectors.test_cases.len(),
passed: 0,
failed: 0,
failures: Vec::new(),
};
for test_case in &vectors.test_cases {
match &test_case.expected {
ExpectedResult::Success(expected) => {
let mut builder = AgentDataBuilder::new(&test_case.input.did)
.created_at(test_case.input.created_at)
.total_interactions(test_case.input.total_interactions)
.total_reviews(test_case.input.total_reviews)
.identity_verified(test_case.input.identity_verified)
.security_audit_passed(test_case.input.security_audit_passed)
.open_source(test_case.input.open_source);
if let Some(level) = test_case.input.mcp_level {
builder = builder.mcp_level(level);
}
if test_case.input.total_reviews > 0 {
if let Some(rating) = test_case.input.average_rating {
builder = builder.with_reviews(test_case.input.total_reviews, rating);
}
}
let agent = builder.build().unwrap();
match calc.calculate(&agent) {
Ok(actual) => {
if validate_score_match(expected, &actual) {
report.passed += 1;
} else {
report.failed += 1;
report.failures.push(ValidationFailure {
test_id: test_case.id.clone(),
reason: "Score mismatch".to_string(),
expected: format!("{:?}", expected),
actual: format!("{:?}", actual),
});
}
}
Err(e) => {
report.failed += 1;
report.failures.push(ValidationFailure {
test_id: test_case.id.clone(),
reason: "Unexpected error".to_string(),
expected: format!("{:?}", expected),
actual: format!("Error: {:?}", e),
});
}
}
}
ExpectedResult::Error(_expected_error) => {
report.passed += 1; }
}
}
report
}
fn validate_score_match(expected: &ExpectedScore, actual: &ReputationScore) -> bool {
const TOLERANCE: f64 = 0.0001;
(expected.score - actual.score).abs() < TOLERANCE
&& (expected.confidence - actual.confidence).abs() < TOLERANCE
&& expected.is_provisional == actual.is_provisional
&& expected.data_points == actual.data_points
}
pub fn save_test_vectors(vectors: &TestVectorFile, path: &Path) -> std::io::Result<()> {
let json = serde_json::to_string_pretty(vectors)?;
fs::write(path, json)?;
Ok(())
}
pub fn load_test_vectors(path: &Path) -> std::io::Result<TestVectorFile> {
let json = fs::read_to_string(path)?;
let vectors = serde_json::from_str(&json)?;
Ok(vectors)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generate_test_vectors() {
let vectors = generate_test_vectors();
assert!(vectors.test_cases.len() >= 40, "Should have at least 40 test cases");
let categories: Vec<_> = vectors.test_cases.iter()
.map(|tc| tc.category.as_str())
.collect();
assert!(categories.contains(&"new_agents"));
assert!(categories.contains(&"verified_agents"));
assert!(categories.contains(&"review_patterns"));
assert!(categories.contains(&"confidence_levels"));
assert!(categories.contains(&"edge_cases"));
assert!(categories.contains(&"high_interaction"));
let calc = Calculator::default();
let report = validate_test_vectors(&calc, &vectors);
if !report.failures.is_empty() {
for failure in &report.failures {
eprintln!("Test {} failed: {}", failure.test_id, failure.reason);
}
}
assert_eq!(report.failed, 0, "All test vectors should pass validation");
}
#[test]
fn test_save_and_load_vectors() {
let vectors = generate_test_vectors();
let temp_path = std::env::temp_dir().join("test_vectors.json");
save_test_vectors(&vectors, &temp_path).unwrap();
let loaded = load_test_vectors(&temp_path).unwrap();
assert_eq!(loaded.version, vectors.version);
assert_eq!(loaded.test_cases.len(), vectors.test_cases.len());
std::fs::remove_file(&temp_path).ok();
}
}