use car_builder::{build_workflow, BuildRequest, BuildResult, ToolCatalog, ToolInfo};
use car_verify::{verify_workflow_graph, WorkflowEdge, WorkflowGraph};
use car_workflow::{StageStep, Workflow};
use serde::Deserialize;
use std::collections::HashSet;
use std::sync::Arc;
const PINNED_MODEL: &str = "anthropic/claude-sonnet-4-6:latest";
const MODEL_ENV: &str = "CAR_BUILDER_EVAL_MODEL";
const KEY_ENV: &str = "ANTHROPIC_API_KEY";
const RUNS: usize = 3;
const TEMPERATURE: f64 = 0.0;
const MAX_TOKENS: usize = 4096;
const MAX_ATTEMPTS: u32 = 3;
const REFUSAL_CAP: f64 = 0.40;
const MIN_REPAIR_RECOVERED: usize = 4;
const MAX_MEDIAN_REPAIR_ITERS: f64 = 1.0;
const BAND_TARGETS: &[(&str, f64)] = &[
("single_stage", 0.90),
("multi_stage", 0.80),
("adversarial", 0.70),
];
const FIXTURE: &str = include_str!("../eval/nl2workflow.jsonl");
#[derive(Debug, Clone, Deserialize)]
struct EvalCase {
id: String,
band: String,
nl_request: String,
must_have: MustHave,
#[serde(default)]
refusal_ok: Option<bool>,
#[serde(default)]
repair_forcing: Option<bool>,
#[serde(default)]
#[allow(dead_code)]
notes: String,
}
#[derive(Debug, Clone, Deserialize)]
struct MustHave {
stage_count: [i64; 2],
required_tools: Vec<String>,
required_edge_conditions: Vec<String>,
}
fn load_cases() -> Vec<EvalCase> {
FIXTURE
.lines()
.filter(|l| !l.trim().is_empty())
.enumerate()
.map(|(i, line)| {
serde_json::from_str::<EvalCase>(line)
.unwrap_or_else(|e| panic!("line {} is not a valid eval case: {e}", i + 1))
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
enum Outcome {
Pass { attempts: u32 },
Refusal { attempts: u32, parse_class: bool },
Fail {
attempts: u32,
violations: Vec<String>,
},
Panicked { message: String },
}
impl Outcome {
fn attempts(&self) -> u32 {
match self {
Outcome::Pass { attempts }
| Outcome::Refusal { attempts, .. }
| Outcome::Fail { attempts, .. } => *attempts,
Outcome::Panicked { .. } => 0,
}
}
fn short(&self) -> String {
match self {
Outcome::Pass { attempts } => format!("PASS (attempts={attempts})"),
Outcome::Refusal {
attempts,
parse_class,
} => format!("REFUSAL (attempts={attempts}, parse_class={parse_class})"),
Outcome::Fail {
attempts,
violations,
} => format!("FAIL (attempts={attempts}): {}", violations.join("; ")),
Outcome::Panicked { message } => format!("PANIC: {message}"),
}
}
}
#[derive(Debug, Clone)]
struct CaseRecord {
#[allow(dead_code)]
id: String,
band: String,
refusal_ok: bool,
repair_forcing: bool,
outcome: Outcome,
}
fn pre_refusal(case: &EvalCase) -> Option<Outcome> {
if case.nl_request.trim().is_empty() {
Some(Outcome::Refusal {
attempts: 0,
parse_class: false,
})
} else {
None
}
}
fn is_parse_class(issues: &[String]) -> bool {
issues
.iter()
.any(|i| i.contains("did not parse") || i.contains("no JSON workflow object"))
}
fn to_graph(wf: &Workflow) -> WorkflowGraph {
let stages: Vec<String> = wf.stages.iter().map(|s| s.id.clone()).collect();
let with_out: HashSet<&str> = wf.edges.iter().map(|e| e.from.as_str()).collect();
let terminals: Vec<String> = stages
.iter()
.filter(|s| !with_out.contains(s.as_str()))
.cloned()
.collect();
let edges: Vec<WorkflowEdge> = wf
.edges
.iter()
.map(|e| WorkflowEdge {
from: e.from.clone(),
to: e.to.clone(),
condition: e
.conditions
.first()
.map(|c| format!("{} {} {}", c.key, c.operator, c.value)),
})
.collect();
WorkflowGraph {
entry: wf.start.clone(),
terminals,
stages,
edges,
}
}
fn collect_tools_step(step: &StageStep, out: &mut HashSet<String>) {
match step {
StageStep::Proposal(ps) => {
for action in &ps.proposal.actions {
if let Some(tool) = &action.tool {
out.insert(tool.clone());
}
}
}
StageStep::Pattern(p) => {
for agent in &p.agents {
for tool in &agent.tools {
out.insert(tool.clone());
}
}
}
StageStep::Deliver(d) => {
for sink in &d.sinks {
for action in &sink.actions {
if let Some(tool) = &action.tool {
out.insert(tool.clone());
}
}
}
}
StageStep::SubWorkflow(sw) => {
for stage in &sw.workflow.stages {
collect_tools_step(&stage.step, out);
}
}
StageStep::LoopUntil(l) => collect_tools_step(&l.body, out),
StageStep::ForEach(f) => collect_tools_step(&f.body, out),
StageStep::Approval(_) | StageStep::Dedup(_) => {}
}
}
fn collect_tools(wf: &Workflow) -> HashSet<String> {
let mut out = HashSet::new();
for stage in &wf.stages {
collect_tools_step(&stage.step, &mut out);
}
out
}
fn must_have_violations(wf: &Workflow, mh: &MustHave) -> Vec<String> {
let mut violations = Vec::new();
let n = wf.stages.len() as i64;
let [min, max] = mh.stage_count;
if n < min || n > max {
violations.push(format!(
"stage count {n} outside required range [{min}, {max}]"
));
}
let tools = collect_tools(wf);
for t in &mh.required_tools {
if !tools.contains(t) {
violations.push(format!("required tool '{t}' not referenced by any stage"));
}
}
for needle in &mh.required_edge_conditions {
let hit = wf
.edges
.iter()
.flat_map(|e| e.conditions.iter())
.any(|c| c.key.contains(needle.as_str()) || c.operator.contains(needle.as_str()));
if !hit {
violations.push(format!(
"no edge condition key/operator contains required '{needle}'"
));
}
}
violations
}
fn classify(mh: &MustHave, result: &BuildResult) -> Outcome {
if !result.valid {
return Outcome::Refusal {
attempts: result.attempts,
parse_class: is_parse_class(&result.issues),
};
}
let Some(wf) = result.workflow.as_ref() else {
return Outcome::Fail {
attempts: result.attempts,
violations: vec!["builder contract violation: valid=true but no workflow".into()],
};
};
let mut violations: Vec<String> = verify_workflow_graph(&to_graph(wf))
.defects
.iter()
.map(|d| {
format!(
"graph defect {:?} at '{}': {}",
d.kind, d.subject, d.explanation
)
})
.collect();
violations.extend(must_have_violations(wf, mh));
if violations.is_empty() {
Outcome::Pass {
attempts: result.attempts,
}
} else {
Outcome::Fail {
attempts: result.attempts,
violations,
}
}
}
#[derive(Debug, Clone, PartialEq)]
struct BandStats {
band: String,
total: usize,
passed: usize,
direct_passes: usize,
refusal_passes: usize,
refusals_capped_out: usize,
refusals: usize,
}
impl BandStats {
fn rate(&self) -> f64 {
if self.total == 0 {
1.0
} else {
self.passed as f64 / self.total as f64
}
}
}
fn score_band(band: &str, records: &[CaseRecord]) -> BandStats {
let in_band: Vec<&CaseRecord> = records.iter().filter(|r| r.band == band).collect();
let total = in_band.len();
let allowance = (total as f64 * REFUSAL_CAP).floor() as usize;
let mut direct_passes = 0usize;
let mut refusal_passes = 0usize;
let mut refusals_capped_out = 0usize;
let mut refusals = 0usize;
for r in &in_band {
match &r.outcome {
Outcome::Pass { .. } => direct_passes += 1,
Outcome::Refusal { .. } => {
refusals += 1;
if r.refusal_ok {
if refusal_passes < allowance {
refusal_passes += 1;
} else {
refusals_capped_out += 1;
}
}
}
Outcome::Fail { .. } | Outcome::Panicked { .. } => {}
}
}
BandStats {
band: band.to_string(),
total,
passed: direct_passes + refusal_passes,
direct_passes,
refusal_passes,
refusals_capped_out,
refusals,
}
}
#[derive(Debug, Clone, PartialEq)]
struct RepairStats {
forcing_total: usize,
recovered: usize,
exercised: usize,
median_repair_iters: f64,
}
fn median(mut xs: Vec<u32>) -> f64 {
if xs.is_empty() {
return 0.0;
}
xs.sort_unstable();
let n = xs.len();
if n % 2 == 1 {
xs[n / 2] as f64
} else {
(xs[n / 2 - 1] as f64 + xs[n / 2] as f64) / 2.0
}
}
fn repair_stats(records: &[CaseRecord]) -> RepairStats {
let iters: Vec<u32> = records
.iter()
.map(|r| r.outcome.attempts().saturating_sub(1))
.collect();
let forcing: Vec<&CaseRecord> = records.iter().filter(|r| r.repair_forcing).collect();
RepairStats {
forcing_total: forcing.len(),
recovered: forcing
.iter()
.filter(|r| matches!(r.outcome, Outcome::Pass { .. }))
.count(),
exercised: forcing.iter().filter(|r| r.outcome.attempts() > 1).count(),
median_repair_iters: median(iters),
}
}
#[derive(Debug, Clone)]
struct RunReport {
bands: Vec<BandStats>,
repair: RepairStats,
panics: usize,
unparseable: usize,
}
fn score_run(records: &[CaseRecord]) -> RunReport {
let bands = BAND_TARGETS
.iter()
.map(|(band, _)| score_band(band, records))
.collect();
let panics = records
.iter()
.filter(|r| matches!(r.outcome, Outcome::Panicked { .. }))
.count();
let unparseable = records
.iter()
.filter(|r| {
!r.refusal_ok
&& matches!(
r.outcome,
Outcome::Refusal {
parse_class: true,
..
}
)
})
.count();
RunReport {
bands,
repair: repair_stats(records),
panics,
unparseable,
}
}
#[derive(Debug, Clone)]
struct WorstReport {
band_passed: Vec<(String, usize, usize)>,
worst_recovered: usize,
forcing_total: usize,
worst_median_repair_iters: f64,
total_panics: usize,
worst_unparseable: usize,
}
fn worst_of(runs: &[RunReport]) -> WorstReport {
assert!(!runs.is_empty(), "worst_of needs at least one run");
let band_passed = (0..runs[0].bands.len())
.map(|i| {
let band = runs[0].bands[i].band.clone();
let total = runs[0].bands[i].total;
let worst = runs.iter().map(|r| r.bands[i].passed).min().unwrap_or(0);
(band, worst, total)
})
.collect();
WorstReport {
band_passed,
worst_recovered: runs.iter().map(|r| r.repair.recovered).min().unwrap_or(0),
forcing_total: runs[0].repair.forcing_total,
worst_median_repair_iters: runs
.iter()
.map(|r| r.repair.median_repair_iters)
.fold(0.0, f64::max),
total_panics: runs.iter().map(|r| r.panics).sum(),
worst_unparseable: runs.iter().map(|r| r.unparseable).max().unwrap_or(0),
}
}
fn eval_catalog(models: Vec<String>) -> ToolCatalog {
let tools = [
car_ir::builtins::read_file(),
car_ir::builtins::list_dir(),
car_ir::builtins::find_files(),
car_ir::builtins::grep_files(),
car_ir::builtins::calculate(),
car_ir::builtins::write_file(),
car_ir::builtins::edit_file(),
]
.into_iter()
.map(|s| ToolInfo {
name: s.name,
description: s.description,
})
.collect();
ToolCatalog {
agents: Vec::new(),
tools,
models,
}
}
async fn run_case_live(
engine: Arc<car_inference::InferenceEngine>,
model: &str,
catalog: &ToolCatalog,
case: &EvalCase,
) -> Outcome {
if let Some(outcome) = pre_refusal(case) {
return outcome;
}
let goal = case.nl_request.clone();
let catalog = catalog.clone();
let model = model.to_string();
let must_have = case.must_have.clone();
let handle = tokio::spawn(async move {
let req = BuildRequest {
goal,
catalog,
existing: None,
feedback: None,
max_attempts: MAX_ATTEMPTS,
};
let result = build_workflow(
|prompt: String| {
let engine = engine.clone();
let model = model.clone();
async move {
let greq = car_inference::GenerateRequest {
prompt,
model: Some(model),
params: car_inference::GenerateParams {
temperature: TEMPERATURE,
max_tokens: MAX_TOKENS,
..Default::default()
},
context: None,
context_stable_prefix: None,
tools: None,
images: None,
messages: None,
cache_control: false,
response_format: None,
intent: None,
};
engine
.generate_tracked(greq)
.await
.map(|r| r.text)
.map_err(|e| e.to_string())
}
},
&req,
)
.await;
classify(&must_have, &result)
});
match handle.await {
Ok(outcome) => outcome,
Err(e) => Outcome::Panicked {
message: e.to_string(),
},
}
}
fn print_run_report(run_idx: usize, report: &RunReport) {
eprintln!("\n== run {run_idx}/{RUNS} ==");
for b in &report.bands {
eprintln!(
" band {:<13} passed {}/{} ({:.0}%) [direct={} refusal_passes={} refusals={} capped_out={}]",
b.band,
b.passed,
b.total,
b.rate() * 100.0,
b.direct_passes,
b.refusal_passes,
b.refusals,
b.refusals_capped_out,
);
}
eprintln!(
" repair: recovered {}/{} forcing cases (exercised repair on {}), median repair iterations {:.1}",
report.repair.recovered,
report.repair.forcing_total,
report.repair.exercised,
report.repair.median_repair_iters,
);
eprintln!(
" panics={} unparseable_final={}",
report.panics, report.unparseable
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "hits a real model API; set ANTHROPIC_API_KEY and run with --ignored --nocapture"]
async fn live_nl2workflow_eval() {
if std::env::var(KEY_ENV).is_err() {
eprintln!(
"[SKIP] {KEY_ENV} is not set — the live NL→workflow eval needs a real API key.\n\
Run it with:\n\
\n\
\t{KEY_ENV}=sk-... cargo test -p car-builder --test live_nl2workflow_eval -- --ignored --nocapture\n\
\n\
Optional: {MODEL_ENV}=<catalog id> overrides the pinned model\n\
(default: {PINNED_MODEL}; changing the pin is a spec-visible event —\n\
record it in docs/proposals/h2-builder-discovery-acceptance.md)."
);
return;
}
let model = std::env::var(MODEL_ENV).unwrap_or_else(|_| PINNED_MODEL.to_string());
let engine = Arc::new(car_inference::InferenceEngine::new(
car_inference::InferenceConfig::default(),
));
let models: Vec<String> = engine
.list_models_unified()
.into_iter()
.map(|m| m.id)
.collect();
assert!(
models.iter().any(|m| m == &model),
"eval model '{model}' is not in the engine's unified registry — \
the catalog moved out from under the pin; repin deliberately (spec-visible event)"
);
let catalog = eval_catalog(models);
let cases = load_cases();
eprintln!("== eval config (shipped defaults; no test-only knobs) ==");
eprintln!(
" resolved model id : {model} (pinned default: {PINNED_MODEL}; override env: {MODEL_ENV})"
);
eprintln!(" temperature : {TEMPERATURE}");
eprintln!(" max_tokens : {MAX_TOKENS}");
eprintln!(" max_attempts : {MAX_ATTEMPTS} (daemon builder.build default)");
eprintln!(" repeat runs (N) : {RUNS} — targets asserted on WORST-run counts");
eprintln!(
" refusal cap : {:.0}% of a band, refusal_ok-labeled cases only",
REFUSAL_CAP * 100.0
);
eprintln!(
" catalog tools : {}",
catalog
.tools
.iter()
.map(|t| t.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
eprintln!(" cases : {} (3 bands x 10)", cases.len());
for (band, target) in BAND_TARGETS {
eprintln!(" target {:<13}: >= {:.0}%", band, target * 100.0);
}
eprintln!(
" repair target : recovers >= {MIN_REPAIR_RECOVERED} repair-forcing cases; median repair iterations <= {MAX_MEDIAN_REPAIR_ITERS}"
);
let mut runs: Vec<RunReport> = Vec::with_capacity(RUNS);
for run_idx in 1..=RUNS {
let mut records: Vec<CaseRecord> = Vec::with_capacity(cases.len());
for case in &cases {
let outcome = run_case_live(engine.clone(), &model, &catalog, case).await;
eprintln!("[run {run_idx}] {:<24} {}", case.id, outcome.short());
records.push(CaseRecord {
id: case.id.clone(),
band: case.band.clone(),
refusal_ok: case.refusal_ok == Some(true),
repair_forcing: case.repair_forcing == Some(true),
outcome,
});
}
let report = score_run(&records);
print_run_report(run_idx, &report);
runs.push(report);
}
let worst = worst_of(&runs);
eprintln!("\n== WORST-OF-{RUNS} (the acceptance numbers) ==");
for (band, passed, total) in &worst.band_passed {
eprintln!(
" band {:<13} worst {}/{} ({:.0}%)",
band,
passed,
total,
*passed as f64 / *total as f64 * 100.0
);
}
eprintln!(
" repair recovered (worst) : {}/{}",
worst.worst_recovered, worst.forcing_total
);
eprintln!(
" median repair iters (worst): {:.1}",
worst.worst_median_repair_iters
);
eprintln!(
" panics={} unparseable_final(worst)={}",
worst.total_panics, worst.worst_unparseable
);
assert_eq!(
worst.total_panics, 0,
"zero panics required (typed failure only)"
);
assert_eq!(
worst.worst_unparseable, 0,
"zero unparseable final outputs required on non-refusal cases (typed failure only)"
);
for ((band, passed, total), (_, target)) in worst.band_passed.iter().zip(BAND_TARGETS) {
let rate = *passed as f64 / *total as f64;
assert!(
rate >= *target,
"band '{band}' worst-run pass rate {:.0}% below target {:.0}% ({passed}/{total})",
rate * 100.0,
target * 100.0
);
}
assert!(
worst.worst_recovered >= MIN_REPAIR_RECOVERED,
"repair loop recovered only {}/{} repair-forcing cases (need >= {MIN_REPAIR_RECOVERED})",
worst.worst_recovered,
worst.forcing_total
);
assert!(
worst.worst_median_repair_iters <= MAX_MEDIAN_REPAIR_ITERS,
"worst-run median repair iterations {:.1} exceeds {MAX_MEDIAN_REPAIR_ITERS}",
worst.worst_median_repair_iters
);
eprintln!("\n[OK] Part 1 acceptance targets met on worst-of-{RUNS} counts with model {model}.");
}
fn wf(json: &str) -> Workflow {
serde_json::from_str(json).expect("test workflow JSON parses")
}
const BRANCHING_WF: &str = r#"{
"id":"wf","name":"WF","start":"read",
"stages":[
{"id":"read","name":"Read","step":{"type":"proposal","proposal":{"id":"p1","source":"b","actions":[
{"id":"a1","type":"tool_call","tool":"read_file","parameters":{}}
],"context":{}}}},
{"id":"write","name":"Write","step":{"type":"proposal","proposal":{"id":"p2","source":"b","actions":[
{"id":"a2","type":"tool_call","tool":"write_file","parameters":{}}
],"context":{}}}},
{"id":"review","name":"Review","step":{"type":"approval","prompt":"ok?","fields":[],"output_key":"approval"}}
],
"edges":[
{"from":"read","to":"write","conditions":[{"key":"stage.read.total","operator":"gt","value":100}],"label":""},
{"from":"read","to":"review","conditions":[{"key":"stage.read.total","operator":"lte","value":100}],"label":""}
]
}"#;
const UNREACHABLE_WF: &str = r#"{
"id":"wf","name":"WF","start":"a",
"stages":[
{"id":"a","name":"A","step":{"type":"approval","prompt":"ok?","fields":[],"output_key":"k"}},
{"id":"orphan","name":"Orphan","step":{"type":"approval","prompt":"?","fields":[],"output_key":"k2"}}
],
"edges":[]
}"#;
const NESTED_TOOLS_WF: &str = r#"{
"id":"wf","name":"WF","start":"fan",
"stages":[
{"id":"fan","name":"Fan","step":{"type":"for_each","items_from":"items","body":
{"type":"proposal","proposal":{"id":"p","source":"b","actions":[
{"id":"a","type":"tool_call","tool":"calculate","parameters":{}}
],"context":{}}},
"max_concurrent":2}},
{"id":"team","name":"Team","step":{"type":"pattern","pattern":"pipeline","task":"t","agents":[
{"name":"g","system_prompt":"s","tools":["grep_files"],"max_turns":3,"metadata":{}}
],"config":{}}}
],
"edges":[{"from":"fan","to":"team","conditions":[],"label":""}]
}"#;
fn mh(stage_count: [i64; 2], tools: &[&str], conds: &[&str]) -> MustHave {
MustHave {
stage_count,
required_tools: tools.iter().map(|s| s.to_string()).collect(),
required_edge_conditions: conds.iter().map(|s| s.to_string()).collect(),
}
}
fn built(
workflow: Option<Workflow>,
valid: bool,
issues: Vec<String>,
attempts: u32,
) -> BuildResult {
BuildResult {
workflow,
valid,
issues,
warnings: Vec::new(),
attempts,
raw: None,
}
}
fn rec(id: &str, band: &str, refusal_ok: bool, forcing: bool, outcome: Outcome) -> CaseRecord {
CaseRecord {
id: id.into(),
band: band.into(),
refusal_ok,
repair_forcing: forcing,
outcome,
}
}
#[test]
fn must_have_holds_on_matching_workflow() {
let w = wf(BRANCHING_WF);
let violations = must_have_violations(
&w,
&mh([3, 5], &["read_file", "write_file"], &["gt", "stage.read"]),
);
assert!(
violations.is_empty(),
"unexpected violations: {violations:?}"
);
}
#[test]
fn must_have_flags_stage_count_out_of_range() {
let w = wf(BRANCHING_WF); let violations = must_have_violations(&w, &mh([4, 6], &[], &[]));
assert_eq!(violations.len(), 1);
assert!(violations[0].contains("stage count 3"), "{violations:?}");
}
#[test]
fn must_have_flags_missing_tool_and_condition() {
let w = wf(BRANCHING_WF);
let violations = must_have_violations(&w, &mh([1, 9], &["grep_files"], &["approval"]));
assert_eq!(violations.len(), 2, "{violations:?}");
assert!(violations.iter().any(|v| v.contains("grep_files")));
assert!(violations.iter().any(|v| v.contains("approval")));
}
#[test]
fn must_have_matches_operator_substring() {
let w = wf(BRANCHING_WF);
assert!(must_have_violations(&w, &mh([1, 9], &[], &["lte"])).is_empty());
}
#[test]
fn collect_tools_recurses_into_bodies_and_agents() {
let w = wf(NESTED_TOOLS_WF);
let tools = collect_tools(&w);
assert!(tools.contains("calculate"), "for_each body tool");
assert!(tools.contains("grep_files"), "pattern agent tool");
assert!(must_have_violations(&w, &mh([1, 9], &["calculate", "grep_files"], &[])).is_empty());
}
#[test]
fn to_graph_derives_terminals_and_verifies_clean() {
let w = wf(BRANCHING_WF);
let g = to_graph(&w);
assert_eq!(g.entry, "read");
let mut terminals = g.terminals.clone();
terminals.sort();
assert_eq!(terminals, vec!["review".to_string(), "write".to_string()]);
let report = verify_workflow_graph(&g);
assert!(
report.sound,
"expected clean graph, got {:?}",
report.defects
);
}
#[test]
fn classify_flags_structural_graph_defect() {
let w = wf(UNREACHABLE_WF);
let outcome = classify(&mh([1, 9], &[], &[]), &built(Some(w), true, vec![], 1));
match outcome {
Outcome::Fail { violations, .. } => {
assert!(
violations.iter().any(|v| v.contains("orphan")),
"{violations:?}"
);
}
other => panic!("expected Fail, got {other:?}"),
}
}
#[test]
fn classify_pass_refusal_and_must_have_fail() {
let w = wf(BRANCHING_WF);
assert_eq!(
classify(
&mh([1, 9], &["read_file"], &["gt"]),
&built(Some(w.clone()), true, vec![], 2)
),
Outcome::Pass { attempts: 2 }
);
assert_eq!(
classify(
&mh([1, 9], &[], &[]),
&built(
None,
false,
vec!["Your output did not parse as a workflow JSON object: x".into()],
3
)
),
Outcome::Refusal {
attempts: 3,
parse_class: true
}
);
assert_eq!(
classify(
&mh([1, 9], &[], &[]),
&built(
None,
false,
vec!["gate: edge to 'ghost' references unknown stage".into()],
3
)
),
Outcome::Refusal {
attempts: 3,
parse_class: false
}
);
assert!(matches!(
classify(
&mh([1, 9], &["quantum_teleport"], &[]),
&built(Some(w), true, vec![], 1)
),
Outcome::Fail { .. }
));
}
#[test]
fn refusal_cap_limits_refusal_passes_to_forty_percent() {
let records: Vec<CaseRecord> = (0..10)
.map(|i| {
rec(
&format!("c{i}"),
"adversarial",
true,
false,
Outcome::Refusal {
attempts: 1,
parse_class: false,
},
)
})
.collect();
let stats = score_band("adversarial", &records);
assert_eq!(stats.total, 10);
assert_eq!(stats.passed, 4, "cap must hold passes at 40%");
assert_eq!(stats.refusal_passes, 4);
assert_eq!(stats.refusals_capped_out, 6);
assert_eq!(stats.refusals, 10);
}
#[test]
fn refusal_never_passes_where_not_labeled() {
let records = vec![
rec(
"a",
"adversarial",
false,
false,
Outcome::Refusal {
attempts: 3,
parse_class: false,
},
),
rec(
"b",
"adversarial",
true,
false,
Outcome::Refusal {
attempts: 1,
parse_class: false,
},
),
rec(
"c",
"adversarial",
false,
false,
Outcome::Pass { attempts: 1 },
),
];
let stats = score_band("adversarial", &records);
assert_eq!(stats.passed, 2, "one direct pass + one labeled refusal");
assert_eq!(stats.direct_passes, 1);
assert_eq!(stats.refusal_passes, 1);
}
#[test]
fn direct_passes_are_never_capped() {
let records: Vec<CaseRecord> = (0..10)
.map(|i| {
rec(
&format!("c{i}"),
"adversarial",
true,
false,
Outcome::Pass { attempts: 1 },
)
})
.collect();
let stats = score_band("adversarial", &records);
assert_eq!(stats.passed, 10);
assert_eq!(stats.refusals_capped_out, 0);
}
#[test]
fn repair_stats_count_recovery_and_median() {
let records = vec![
rec(
"f1",
"multi_stage",
false,
true,
Outcome::Pass { attempts: 2 },
),
rec(
"f2",
"multi_stage",
false,
true,
Outcome::Pass { attempts: 3 },
),
rec(
"f3",
"multi_stage",
false,
true,
Outcome::Pass { attempts: 1 },
),
rec(
"f4",
"multi_stage",
false,
true,
Outcome::Fail {
attempts: 3,
violations: vec![],
},
),
rec(
"f5",
"multi_stage",
false,
true,
Outcome::Refusal {
attempts: 3,
parse_class: false,
},
),
rec(
"p1",
"single_stage",
false,
false,
Outcome::Pass { attempts: 1 },
),
];
let stats = repair_stats(&records);
assert_eq!(stats.forcing_total, 5);
assert_eq!(stats.recovered, 3, "only full passes count as recovered");
assert_eq!(stats.exercised, 4, "attempts > 1 among forcing cases");
assert_eq!(stats.median_repair_iters, 1.5);
}
#[test]
fn median_handles_odd_even_and_empty() {
assert_eq!(median(vec![]), 0.0);
assert_eq!(median(vec![2]), 2.0);
assert_eq!(median(vec![0, 1, 2]), 1.0);
assert_eq!(median(vec![0, 0, 1, 1]), 0.5);
}
#[test]
fn worst_of_takes_per_band_minima_and_median_maximum() {
let run = |b1_pass: bool, attempts: u32| {
let records = vec![
rec(
"s1",
"single_stage",
false,
false,
if b1_pass {
Outcome::Pass { attempts }
} else {
Outcome::Fail {
attempts,
violations: vec![],
}
},
),
rec("m1", "multi_stage", false, true, Outcome::Pass { attempts }),
rec(
"a1",
"adversarial",
true,
false,
Outcome::Refusal {
attempts: 1,
parse_class: false,
},
),
];
score_run(&records)
};
let runs = vec![run(true, 1), run(false, 3), run(true, 2)];
let worst = worst_of(&runs);
assert_eq!(worst.band_passed[0], ("single_stage".into(), 0, 1));
assert_eq!(worst.band_passed[1], ("multi_stage".into(), 1, 1));
assert_eq!(worst.band_passed[2], ("adversarial".into(), 0, 1));
assert_eq!(worst.worst_median_repair_iters, 2.0);
assert_eq!(worst.worst_recovered, 1);
assert_eq!(worst.total_panics, 0);
}
#[test]
fn score_run_counts_panics_and_unparseable() {
let records = vec![
rec(
"a",
"single_stage",
false,
false,
Outcome::Panicked {
message: "boom".into(),
},
),
rec(
"b",
"multi_stage",
false,
false,
Outcome::Refusal {
attempts: 3,
parse_class: true,
},
),
rec(
"c",
"adversarial",
true,
false,
Outcome::Refusal {
attempts: 3,
parse_class: true,
},
),
];
let report = score_run(&records);
assert_eq!(report.panics, 1);
assert_eq!(report.unparseable, 1);
}
#[test]
fn empty_goal_is_pre_refused_without_a_model_call() {
let cases = load_cases();
let empty = cases.iter().find(|c| c.id == "b3-empty-request").unwrap();
assert_eq!(
pre_refusal(empty),
Some(Outcome::Refusal {
attempts: 0,
parse_class: false
})
);
let nonempty = cases.iter().find(|c| c.id == "b1-read-file").unwrap();
assert_eq!(pre_refusal(nonempty), None);
}
#[test]
fn eval_catalog_lists_the_seven_commodity_tools() {
let catalog = eval_catalog(vec!["m1".into()]);
let names: HashSet<&str> = catalog.tools.iter().map(|t| t.name.as_str()).collect();
for tool in [
"read_file",
"list_dir",
"find_files",
"grep_files",
"calculate",
"write_file",
"edit_file",
] {
assert!(names.contains(tool), "missing commodity tool {tool}");
}
assert_eq!(catalog.tools.len(), 7);
assert_eq!(catalog.models, vec!["m1".to_string()]);
}
#[test]
fn fixture_repair_forcing_cases_cover_the_recovery_assertion() {
let cases = load_cases();
let forcing = cases
.iter()
.filter(|c| c.repair_forcing == Some(true))
.count();
assert!(
forcing > MIN_REPAIR_RECOVERED,
"need at least {} repair-forcing cases for a >= {MIN_REPAIR_RECOVERED} recovery target, found {forcing}",
MIN_REPAIR_RECOVERED + 1
);
}