use serde::{Deserialize, Serialize};
use std::fmt;
use super::plan::Plan;
use super::types::ConformanceLevel;
pub trait ConformanceAdapter {
fn parse_and_validate(&self, toml: &str) -> Result<Plan, Vec<String>>;
fn conformance_level(&self) -> ConformanceLevel;
fn host_name(&self) -> &str;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceReport {
pub suite_name: String,
pub host_name: String,
pub total: usize,
pub passed: usize,
pub failed: usize,
pub failures: Vec<TestFailure>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestFailure {
pub test_name: String,
pub error: String,
}
impl ConformanceReport {
pub fn all_passed(&self) -> bool {
self.failures.is_empty()
}
}
impl fmt::Display for ConformanceReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{} — {}: {}/{} passed",
self.suite_name, self.host_name, self.passed, self.total
)?;
for failure in &self.failures {
writeln!(f, " FAIL {}: {}", failure.test_name, failure.error)?;
}
Ok(())
}
}
pub struct PlanLoadingSuite {
cases: Vec<PlanLoadingCase>,
}
struct PlanLoadingCase {
name: &'static str,
toml: &'static str,
should_pass: bool,
#[allow(clippy::type_complexity)]
check_fields: Option<Box<dyn Fn(&Plan) -> Result<(), String>>>,
}
impl PlanLoadingSuite {
pub fn new() -> Self {
Self {
cases: vec![
PlanLoadingCase {
name: "valid_minimal_plan",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "test"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "a"
[[plan.steps]]
step_id = "s1"
description = "test"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = []
"#,
should_pass: true,
check_fields: None,
},
PlanLoadingCase {
name: "valid_multi_step_with_deps",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "multi-step"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 1.00
determinism = "strict"
content_sha256 = "b"
[[plan.steps]]
step_id = "build"
description = "build the project"
agent_hint = "generic"
phases = ["plan", "implement", "verify"]
verify_command = "cargo build"
depends_on = []
[[plan.steps]]
step_id = "test"
description = "run tests"
agent_hint = "code-review"
phases = ["test", "verify"]
verify_command = "cargo test"
depends_on = ["build"]
[[plan.steps]]
step_id = "deploy"
description = "deploy"
agent_hint = "generic"
phases = ["verify"]
verify_command = "curl localhost/health"
depends_on = ["test"]
"#,
should_pass: true,
check_fields: None,
},
PlanLoadingCase {
name: "optional_fields_default",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "test defaults"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "c"
[[plan.steps]]
step_id = "s1"
description = "test"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = []
"#,
should_pass: true,
check_fields: Some(Box::new(|plan: &Plan| {
if plan.plan.signature.is_some() {
return Err("signature should be None by default".into());
}
if plan.plan.steps[0].skill_ref.is_some() {
return Err("skill_ref should be None by default".into());
}
if plan.plan.max_escalations != 3 {
return Err(format!(
"max_escalations should default to 3, got {}",
plan.plan.max_escalations
));
}
Ok(())
})),
},
PlanLoadingCase {
name: "reject_unknown_phase",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "bad phase"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "d"
[[plan.steps]]
step_id = "s1"
description = "bad phase"
agent_hint = "generic"
phases = ["bogus_phase"]
verify_command = "true"
depends_on = []
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_empty_phases",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "no phases"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "e"
[[plan.steps]]
step_id = "s1"
description = "no phases"
agent_hint = "generic"
phases = []
verify_command = "true"
depends_on = []
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_empty_verify_command",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "no verify"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "f"
[[plan.steps]]
step_id = "s1"
description = "no verify"
agent_hint = "generic"
phases = ["verify"]
verify_command = ""
depends_on = []
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_unknown_dependency",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "bad dep"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "g"
[[plan.steps]]
step_id = "s1"
description = "bad dep"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = ["nonexistent"]
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_dependency_cycle",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "cycle"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "h"
[[plan.steps]]
step_id = "s1"
description = "step 1"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = ["s2"]
[[plan.steps]]
step_id = "s2"
description = "step 2"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = ["s1"]
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_phases_out_of_order",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "bad order"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = "i"
[[plan.steps]]
step_id = "s1"
description = "bad order"
agent_hint = "generic"
phases = ["verify", "plan", "implement"]
verify_command = "true"
depends_on = []
"#,
should_pass: false,
check_fields: None,
},
PlanLoadingCase {
name: "reject_empty_content_hash",
toml: r#"
[plan]
version = "0"
plan_id = "550e8400-e29b-41d4-a716-446655440000"
goal = "no hash"
created_at = "2026-05-24T12:00:00Z"
created_by = "agent:test"
budget_estimate_usd = 0.0
determinism = "best-effort"
content_sha256 = ""
[[plan.steps]]
step_id = "s1"
description = "test"
agent_hint = "generic"
phases = ["verify"]
verify_command = "true"
depends_on = []
"#,
should_pass: false,
check_fields: None,
},
],
}
}
pub fn run(&self, adapter: &dyn ConformanceAdapter) -> ConformanceReport {
let mut failures = Vec::new();
for case in &self.cases {
let result = adapter.parse_and_validate(case.toml);
match (case.should_pass, result) {
(true, Ok(ref plan)) => {
if let Some(ref check) = case.check_fields
&& let Err(err) = check(plan)
{
failures.push(TestFailure {
test_name: case.name.to_string(),
error: format!("field check failed: {}", err),
});
}
}
(true, Err(errs)) => {
failures.push(TestFailure {
test_name: case.name.to_string(),
error: format!("expected success, got errors: {:?}", errs),
});
}
(false, Ok(_)) => {
failures.push(TestFailure {
test_name: case.name.to_string(),
error: "expected validation failure, got success".to_string(),
});
}
(false, Err(_)) => {
}
}
}
let total = self.cases.len();
let failed_count = failures.len();
ConformanceReport {
suite_name: "PlanLoadingSuite".to_string(),
host_name: adapter.host_name().to_string(),
total,
passed: total - failed_count,
failed: failed_count,
failures,
}
}
}
impl Default for PlanLoadingSuite {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coordination::plan::Plan;
use crate::coordination::types::ConformanceLevel;
struct StaticAdapter {}
impl ConformanceAdapter for StaticAdapter {
fn parse_and_validate(&self, toml: &str) -> Result<Plan, Vec<String>> {
let plan: Plan = toml::from_str(toml).map_err(|e| vec![e.to_string()])?;
let errors = plan.validate();
if !errors.is_empty() {
return Err(errors.iter().map(|e| e.to_string()).collect());
}
Ok(plan)
}
fn conformance_level(&self) -> ConformanceLevel {
ConformanceLevel::Minimal
}
fn host_name(&self) -> &str {
"static-test-adapter"
}
}
#[test]
fn test_conformance_minimal_plan_loading_passes() {
let adapter = StaticAdapter {};
let suite = PlanLoadingSuite::new();
let report = suite.run(&adapter);
assert!(
report.failures.is_empty(),
"all plan-loading tests must pass: {:?}",
report.failures
);
}
}