use warble_eval_runner::{rescore, Backend, CaseKey, GoldenCase, Trace, TraceStore};
fn golden_case(expected_rows: &str) -> GoldenCase {
let yaml = format!(
"id: q1\nquestion: \"how many orders?\"\ntags: [agg]\nmatch: scalar\nexpected: {{ columns: [n], rows: {expected_rows} }}\n"
);
serde_yaml::from_str(&yaml).expect("golden case parses")
}
fn trace_for(result: serde_json::Value) -> Trace {
Trace {
case_id: "q1".into(),
agent_sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into(),
model: "opus".into(),
context_version: Some("driftwood@c0ffee0".into()),
context_sha: "cccccccccccccccccccccccccccccccccccccccc".into(),
question: "how many orders?".into(),
sql_executed: None,
result,
cost: Some(0.1234),
latency_ms: 21_000,
turns: Some(6),
tool_calls: None,
backend: Backend::default(),
}
}
fn key_for<'a>(
question: &'a str,
agent_sha: &'a str,
model: &'a str,
context_sha: &'a str,
) -> CaseKey<'a> {
CaseKey {
case_id: "q1",
question,
agent_sha,
model,
context_sha,
sample: 0,
backend: Backend::default(),
max_turns: None,
tier_binding: None,
}
}
#[test]
fn changing_only_expected_rescores_from_cache_with_zero_llm() {
let dir = tempfile::tempdir().unwrap();
let store = TraceStore::new(dir.path().to_path_buf(), true);
let key = key_for(
"how many orders?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"opus",
"cccccccccccccccccccccccccccccccccccccccc",
);
store
.store(
&key,
&trace_for(serde_json::json!({"columns":["n"],"rows":[[42]]})),
)
.unwrap();
let later = TraceStore::new(dir.path().to_path_buf(), true);
let hit_v1 = later.load(&key).expect("cache hit for unchanged key");
let verdict_v1 = rescore(&hit_v1, &golden_case("[[42]]")).expect("re-score");
assert!(verdict_v1.pass, "cached 42 matches v1 expected 42");
assert_eq!(
hit_v1.turns,
Some(6),
"diagnostic turns carried through the cache"
);
let hit_v2 = later
.load(&key)
.expect("hit — the expected change does not move the key");
let verdict_v2 = rescore(&hit_v2, &golden_case("[[99]]")).expect("re-score");
assert!(
!verdict_v2.pass,
"cached 42 no longer matches v2 expected 99"
);
}
#[test]
fn no_cache_forces_a_miss() {
let dir = tempfile::tempdir().unwrap();
let key = key_for(
"how many orders?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"opus",
"cccccccccccccccccccccccccccccccccccccccc",
);
TraceStore::new(dir.path().to_path_buf(), true)
.store(
&key,
&trace_for(serde_json::json!({"columns":["n"],"rows":[[42]]})),
)
.unwrap();
let no_cache = TraceStore::new(dir.path().to_path_buf(), false);
assert!(no_cache.load(&key).is_none(), "disabled store never hits");
}
#[test]
fn key_isolates_result_inputs_from_the_expectation() {
let base = key_for(
"how many orders?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"opus",
"cccccccccccccccccccccccccccccccccccccccc",
);
let base_hash = base.hash().unwrap();
for k in [
key_for(
"how many customers?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"opus",
"cccccccccccccccccccccccccccccccccccccccc",
),
key_for(
"how many orders?",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"opus",
"cccccccccccccccccccccccccccccccccccccccc",
),
key_for(
"how many orders?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"haiku",
"cccccccccccccccccccccccccccccccccccccccc",
),
key_for(
"how many orders?",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"opus",
"dddddddddddddddddddddddddddddddddddddddd",
),
] {
assert_ne!(
base_hash,
k.hash().unwrap(),
"a key component change must move the hash"
);
}
assert_eq!(base_hash, base.hash().unwrap());
}
#[test]
fn rescoring_a_full_set_from_cache_is_fast_and_llm_free() {
let dir = tempfile::tempdir().unwrap();
let store = TraceStore::new(dir.path().to_path_buf(), true);
let n = 50;
let questions: Vec<String> = (0..n).map(|i| format!("q number {i}?")).collect();
let agent = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let ctx = "cccccccccccccccccccccccccccccccccccccccc";
for q in &questions {
store
.store(
&key_for(q, agent, "opus", ctx),
&trace_for(serde_json::json!({"columns":["n"],"rows":[[7]]})),
)
.unwrap();
}
let started = std::time::Instant::now();
let mut hits = 0usize;
for q in &questions {
let trace = store.load(&key_for(q, agent, "opus", ctx)).expect("hit");
let verdict = rescore(&trace, &golden_case("[[7]]")).expect("re-score");
assert!(verdict.pass);
hits += 1;
}
let elapsed = started.elapsed();
assert_eq!(hits, n, "every case re-scored from cache");
assert!(
elapsed < std::time::Duration::from_secs(5),
"re-scoring {n} cached cases took {elapsed:?} — exceeded the 5s no-LLM budget"
);
}