flowrs-engine/crates/flowrs-core/tests/agent_edit_eval.rs
flowRS fd958c0547 flowRS engine, cut from flowRS 3451b065
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 19:04:39 -05:00

365 lines
13 KiB
Rust

//! The agent edit eval set, replayed from its recorded replies.
//!
//! No model and no key: each case's replies are replayed round by round through the real
//! loop — the real parser, applier, checker and `cargo check` — and the result is graded
//! against the case's properties. The pass rate is printed; every case must pass, because
//! the recorded replies are fixed and a regression in the loop is the only thing that can
//! change an outcome.
use flowrs_core::graph::edit::{eval, variants};
use flowrs_core::graph::verify::Native;
use flowrs_core::graph::widget::builtin_registry;
#[test]
fn the_recorded_eval_set_passes() {
if std::process::Command::new("cargo")
.arg("-V")
.output()
.is_err()
{
assert_eq!(
std::env::var("FLOWRS_ALLOW_MISSING_CARGO").as_deref(),
Ok("1"),
"no cargo on PATH"
);
return;
}
let cases = eval::cases().expect("cases.json parses");
assert!(cases.len() >= 12, "{}", cases.len());
let tc = Native::default();
let results: Vec<_> = cases
.iter()
.map(|c| eval::replay(c, builtin_registry(), &tc))
.collect();
let report = eval::report(&results);
println!("{report}");
assert!(results.iter().all(|r| r.passed), "\n{report}");
// The set exercises the repair loop and an honest failure, not only first-try passes.
assert!(
results
.iter()
.filter(|r| r.rounds >= 2 && r.outcome == "verified")
.count()
>= 4
);
assert!(results.iter().any(|r| r.outcome == "failed"));
assert!(results.iter().any(|r| r.outcome == "answered"));
}
/// Branch row 1's acceptance (work/variants), offline, on scripted replies:
///
/// - quality: every case that is not open passes in the single-answer loop (k = 1); every
/// alternative shown at k = 3 is verified (the grader's `alternatives_guard`); at least
/// 10 of the 12 open cases show two or more verified alternatives;
/// - diversity: the alternatives' mean pairwise edit distance is at least 1.5 times that of
/// three independent default-prompt samples of the same request; no two shown share a
/// `semantic_key`; at most one in five is within 0.1 of a retrieved example; every label
/// names a handle its alternative changed.
///
/// The scripted default samples are written the way default sampling behaves (mode
/// collapse: the same answer, or one parameter apart). The figure on a real model comes
/// from `scripts/agent-eval.sh --endpoint … --record`, which records both.
#[test]
fn open_cases_meet_the_variants_acceptance() {
if std::process::Command::new("cargo")
.arg("-V")
.output()
.is_err()
{
assert_eq!(
std::env::var("FLOWRS_ALLOW_MISSING_CARGO").as_deref(),
Ok("1"),
"no cargo on PATH"
);
return;
}
let cases = eval::cases().unwrap();
let tc = Native::default();
let (open, single): (Vec<_>, Vec<_>) = cases.iter().partition(|c| c.open);
assert_eq!(open.len(), 12, "the open-ended set");
assert!(single.len() >= 19);
// Quality at k = 1.
for c in &single {
let r = eval::replay(c, builtin_registry(), &tc);
assert!(r.passed && r.outcome != "alternatives", "{}: {r:?}", c.id);
}
let mut two_or_more = 0usize;
let mut shown = 0usize;
let mut near = 0usize;
let (mut alt_spread, mut base_spread, mut measured) = (0.0f64, 0.0f64, 0usize);
let (mut alt_calls, mut base_calls) = (0usize, 0usize);
let mut lines = Vec::new();
for c in &open {
let r = eval::replay(c, builtin_registry(), &tc);
assert!(r.passed, "{}: {:?}", c.id, r.failures);
assert_eq!(r.outcome, "alternatives", "{}", c.id);
// Re-derive the guard here rather than trust the grade: every one verified, no two
// sharing a semantic key, each label naming a handle it changed.
let mut s = eval::session(c, builtin_registry()).unwrap();
for rec in &c.recorded {
s.submit(&rec.reply, &tc);
}
let flowrs_core::graph::edit::Turn::Done(o) = s.next() else {
panic!("{}: the loop wanted more replies", c.id)
};
let flowrs_core::graph::edit::Outcome::Alternatives(ps) = *o else {
panic!("{}: not alternatives", c.id)
};
let mut keys = std::collections::HashSet::new();
for p in &ps {
assert!(p.verification.verified(), "{}", c.id);
assert!(
keys.insert(p.graph.semantic_key()),
"{}: a repeated key",
c.id
);
let v = p.variant.as_ref().unwrap();
assert!(
variants::names_changed_handle(&v.dimension, &p.changes),
"{}: {}",
c.id,
v.dimension.label()
);
}
assert!(ps.len() <= variants::MAX_IN_VIEW);
assert_eq!(ps.len(), r.alternatives);
if r.alternatives >= 2 {
two_or_more += 1;
}
shown += r.alternatives;
near += r
.example_distances
.iter()
.filter(|d| **d <= variants::NEAR_COPY)
.count();
alt_calls += r.rounds;
let b = eval::baseline(c, builtin_registry(), &tc).expect("every open case has a baseline");
assert_eq!(b.verified, 3, "{}: a baseline run did not verify", c.id);
base_calls += b.calls;
if r.alternatives >= 2 {
alt_spread += r.spread;
base_spread += b.spread;
measured += 1;
}
lines.push(format!(
" {:<34} {} shown · spread {:>5.2} vs default {:>5.2} · calls {} vs {} · nearest example {:?}",
c.id,
r.alternatives,
r.spread,
b.spread,
r.rounds,
b.calls,
r.example_distances
.iter()
.map(|d| (d * 100.0).round() / 100.0)
.collect::<Vec<_>>()
));
}
let alt_mean = alt_spread / measured as f64;
let base_mean = base_spread / measured as f64;
println!(
"open cases: {two_or_more} of {} show two or more verified alternatives; {shown} shown; \
{near} within {} of a retrieved example\n\
spread: alternatives {alt_mean:.2}, three default samples {base_mean:.2} ({:.2}x over {measured} cases)\n\
model calls: alternatives {alt_calls}, default samples {base_calls}\n{}",
open.len(),
variants::NEAR_COPY,
alt_mean / base_mean,
lines.join("\n")
);
assert!(
two_or_more >= 10,
"{two_or_more} of 12 open cases show two or more"
);
assert!(
alt_mean >= 1.5 * base_mean,
"alternatives {alt_mean:.2} apart, default samples {base_mean:.2}: under 1.5x"
);
assert!(
near * 5 <= shown,
"{near} of {shown} alternatives are near copies"
);
}
/// The set properties fail a set that misses them: too few alternatives, too close
/// together, or near copies of the examples.
#[test]
fn the_set_properties_fail_a_set_that_misses_them() {
let tc = flowrs_core::graph::verify::InBrowser;
let mut c = eval::cases()
.unwrap()
.into_iter()
.find(|c| c.id == "open-clean-temperature-log")
.unwrap();
let r = eval::replay(&c, builtin_registry(), &tc);
assert!(r.passed, "{r:?}");
assert_eq!(r.alternatives, 2);
c.properties = vec![
eval::Property::DistinctAlternatives(3),
eval::Property::DiversityAtLeast(r.spread + 1.0),
eval::Property::NotCopyOfExample(1.0),
];
let r = eval::replay(&c, builtin_registry(), &tc);
assert!(!r.passed);
let has = |s: &str| r.failures.iter().any(|f| f.contains(s));
assert!(has("2 distinct alternative(s) shown, at least 3"), "{r:?}");
assert!(has("apart on average"), "{r:?}");
assert!(has("near copy of an example"), "{r:?}");
// A single answer has no set to measure.
let mut one = eval::cases()
.unwrap()
.into_iter()
.find(|c| c.id == "iir-to-highpass")
.unwrap();
one.properties = vec![eval::Property::DistinctAlternatives(2)];
assert!(!eval::replay(&one, builtin_registry(), &tc).passed);
}
/// The grader is not a rubber stamp on alternatives either: a set with a repeat, an
/// unverified member or a label naming an untouched handle fails the guard.
#[test]
fn the_alternatives_guard_refuses_what_it_should() {
let c = eval::cases()
.unwrap()
.into_iter()
.find(|c| c.id == "open-filter-less-ringing")
.unwrap();
let tc = flowrs_core::graph::verify::InBrowser;
let mut s = eval::session(&c, builtin_registry()).unwrap();
for rec in &c.recorded {
s.submit(&rec.reply, &tc);
}
let flowrs_core::graph::edit::Turn::Done(o) = s.next() else {
panic!()
};
let flowrs_core::graph::edit::Outcome::Alternatives(ps) = *o else {
panic!()
};
let base = eval::base_graph(&c).unwrap();
assert!(eval::alternatives_guard(&ps, &base).is_empty());
let mut repeat = ps.clone();
repeat.push(ps[0].clone());
assert!(
eval::alternatives_guard(&repeat, &base)
.iter()
.any(|f| f.contains("are the same edit")),
);
let mut unverified = ps.clone();
unverified[1].verification.stages[0].status =
flowrs_core::graph::verify::Status::Failed("x".into());
assert!(
eval::alternatives_guard(&unverified, &base)
.iter()
.any(|f| f.contains("not verified"))
);
let mut mislabelled = ps.clone();
mislabelled[0].variant.as_mut().unwrap().dimension.handle = "n0".into();
assert!(
eval::alternatives_guard(&mislabelled, &base)
.iter()
.any(|f| f.contains("names no handle it changed"))
);
let mut swapped = ps.clone();
swapped[0].graph = ps[1].graph.clone();
assert!(
eval::alternatives_guard(&swapped, &base)
.iter()
.any(|f| f.contains("does not re-apply"))
);
let mut five = ps.clone();
five.extend(ps.iter().cloned());
assert!(
eval::alternatives_guard(&five, &base)
.iter()
.any(|f| f.contains("more than 4"))
);
}
/// A reply that is wrong is graded wrong: the grader is not a rubber stamp. Every case's
/// LAST recorded reply is dropped, so a case that needed it either asks for a reply that
/// is not there or ends on its earlier, failing round — and must not pass.
#[test]
fn the_grader_fails_a_case_whose_fix_is_missing() {
let cases = eval::cases().unwrap();
let tc = flowrs_core::graph::verify::InBrowser;
let mut graded = 0;
for mut c in cases
.into_iter()
.filter(|c| c.recorded.len() >= 2 && c.outcome == "verified")
{
c.recorded.pop();
let r = eval::replay(&c, builtin_registry(), &tc);
assert!(!r.passed, "{} passed without its fix: {r:?}", c.id);
graded += 1;
}
assert!(graded >= 4);
}
fn case(id: &str) -> eval::Case {
eval::cases()
.unwrap()
.into_iter()
.find(|c| c.id == id)
.unwrap_or_else(|| panic!("no case {id}"))
}
fn reply(text: &str) -> Vec<eval::Recorded> {
vec![eval::Recorded {
source: "hand-written".into(),
reply: text.into(),
}]
}
/// A graph that prints the string "5" says the right thing without adding anything;
/// `has_any` and `computes` are what refuse it.
#[test]
fn printing_the_answer_as_text_is_not_adding() {
let mut c = case("build-into-empty-graph");
c.recorded = reply(
"Prints the result.\n```patch\nlet s = StringLiteral { value: \"5\" };\nlet out = io.print(value: s.value);\n```",
);
let r = eval::replay(
&c,
builtin_registry(),
&flowrs_core::graph::verify::InBrowser,
);
assert_eq!(r.outcome, "verified", "{r:?}");
assert!(!r.passed, "{r:?}");
assert!(
r.failures.iter().any(|f| f.contains("no node of any of"))
&& r.failures.iter().any(|f| f.contains("computes 5")),
"{r:?}"
);
}
/// `no_edit` accepts the model saying it cannot (`answered`) or the checker refusing
/// every patch (`failed`) — never an edit that went through.
#[test]
fn no_edit_refuses_an_edit() {
let mut c = case("impossible-fft-fails-honestly");
c.recorded =
reply("Prints the message twice.\n```patch\nlet p = PrintLn(input: &n0.value);\n```");
let r = eval::replay(
&c,
builtin_registry(),
&flowrs_core::graph::verify::InBrowser,
);
assert_eq!(r.outcome, "verified", "{r:?}");
assert!(!r.passed, "{r:?}");
assert!(
r.failures
.iter()
.any(|f| f.contains("ended verified where the case expects no_edit")),
"{r:?}"
);
let live = eval::replay(
&case("impossible-fft-fails-honestly-live"),
builtin_registry(),
&flowrs_core::graph::verify::InBrowser,
);
assert!(live.passed && live.outcome == "answered", "{live:?}");
}