#[path = "../../cortiq-engine/tests/common/embryo_synth.rs"]
mod embryo_synth;
#[path = "../../cortiq-engine/tests/common/knowledge_synth.rs"]
mod knowledge_synth;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn cortiq(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cortiq"))
.args(args)
.env("CMF_GPU", "0")
.env("RUST_LOG", "warn")
.env_remove("CMF_EMBRYO_RESIDENT")
.env_remove("CMF_GROWTH")
.env_remove("CMF_GROWTH_SHELL")
.output()
.expect("spawn cortiq")
}
fn json(args: &[&str]) -> serde_json::Value {
let out = cortiq(args);
let (so, se) = (
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
);
assert!(
out.status.success(),
"cortiq {args:?} failed\nstdout:\n{so}\nstderr:\n{se}"
);
serde_json::from_str(&so).unwrap_or_else(|e| panic!("cortiq {args:?}: not JSON ({e}):\n{so}"))
}
fn s(p: &Path) -> &str {
p.to_str().unwrap()
}
fn f(v: &serde_json::Value) -> f64 {
v.as_f64().unwrap_or_else(|| panic!("not a number: {v}"))
}
fn strings(v: &serde_json::Value) -> Vec<String> {
v.as_array()
.unwrap_or_else(|| panic!("not an array: {v}"))
.iter()
.map(|x| x.as_str().unwrap().to_string())
.collect()
}
const ROWS: &[(&str, &str, &str, &[&str])] = &[
("ru", "Achillea", "К какому семейству относится тысячелистник?", &["Asteraceae", "Астровые"]),
("en", "Salvia", "Which plant family does sage belong to?", &["Lamiaceae", "Яснотковые"]),
("ru", "Rosa", "К какому семейству относится шиповник?", &["Rosaceae", "Розовые"]),
("en", "Arnica", "Which plant family does arnica belong to?", &["asteraceae"]),
("ru", "Aconitum", "К какому семейству относится борец?", &["Ranunculaceae", "Лютиковые"]),
("en", "Tanacetum", "Which plant family does tansy belong to?", &["Asteraceae", "Астровые"]),
];
const DEFAULT_CANDIDATES: [&str; 4] = ["Asteraceae", "Lamiaceae", "Rosaceae", "Ranunculaceae"];
struct Fixture {
dir: PathBuf,
f0: PathBuf,
prompts: PathBuf,
}
fn fixture(tag: &str) -> Fixture {
unsafe { std::env::set_var("CMF_GPU", "0") };
let dir = std::env::temp_dir().join(format!("cmf-choice-cli-{tag}-{}", std::process::id()));
let files = knowledge_synth::write_knowledge_pair(
&dir,
&embryo_synth::SynthGeom::tiny_gdn_bounded(),
"active",
);
let prompts = dir.join("prompts.jsonl");
let text: String = ROWS
.iter()
.map(|(lang, src, prompt, expect)| {
serde_json::json!({"lang": lang, "src": src, "prompt": prompt, "expect": expect})
.to_string()
+ "\n"
})
.collect();
std::fs::write(&prompts, text).unwrap();
Fixture {
dir,
f0: files.f0,
prompts,
}
}
fn check_rows(v: &serde_json::Value, candidates: &[String]) {
let rows = v["per_row"].as_array().unwrap();
assert_eq!(rows.len(), ROWS.len(), "{v}");
let mut top1 = 0usize;
let mut top5 = 0usize;
for (i, r) in rows.iter().enumerate() {
assert_eq!(r["index"], i);
assert_eq!(r["lang"], ROWS[i].0);
assert_eq!(r["src"], ROWS[i].1);
assert_eq!(strings(&r["expect"]), ROWS[i].3);
let predicted = r["predicted"].as_str().unwrap();
assert!(candidates.iter().any(|c| c == predicted), "{r}");
let lp_best = f(&r["lp_best"]);
assert!(lp_best.is_finite() && lp_best < 0.0, "{r}");
let in_set = candidates
.iter()
.any(|c| c.to_lowercase() == ROWS[i].3[0].to_lowercase());
if in_set {
let rank = r["rank_of_correct"].as_u64().unwrap() as usize;
assert!((1..=candidates.len()).contains(&rank), "{r}");
let lp_correct = f(&r["lp_correct"]);
assert!(lp_correct <= lp_best + 1e-12, "{r}");
assert_eq!(r["correct"], rank == 1, "{r}");
if rank == 1 {
assert_eq!(predicted.to_lowercase(), ROWS[i].3[0].to_lowercase());
assert_eq!(lp_correct, lp_best);
assert!(f(&r["lp_best_wrong"]) <= lp_best, "{r}");
} else {
assert_eq!(f(&r["lp_best_wrong"]), lp_best, "{r}");
}
assert_eq!(r["tokens_correct"], ROWS[i].3[0].len() as u64, "{r}");
top1 += (rank == 1) as usize;
top5 += (rank <= 5) as usize;
} else {
assert_eq!(r["rank_of_correct"], serde_json::Value::Null, "{r}");
assert_eq!(r["lp_correct"], serde_json::Value::Null);
assert_eq!(r["tokens_correct"], serde_json::Value::Null);
assert_eq!(r["correct"], false);
assert_eq!(f(&r["lp_best_wrong"]), lp_best);
}
assert_eq!(r["tokens_predicted"], predicted.len() as u64, "{r}");
assert_eq!(r["route"]["target"], "backbone", "{r}");
}
assert_eq!(v["top1"], top1 as u64, "{v}");
assert_eq!(v["top5"], top5 as u64, "{v}");
assert_eq!(f(&v["top1_acc"]), top1 as f64 / ROWS.len() as f64);
assert_eq!(f(&v["top5_acc"]), top5 as f64 / ROWS.len() as f64);
let per_lang = v["per_lang"].as_object().unwrap();
assert_eq!(per_lang.len(), 2);
assert_eq!(per_lang["ru"]["n"], 3);
assert_eq!(per_lang["en"]["n"], 3);
let lang_top1: u64 = per_lang.values().map(|b| b["top1"].as_u64().unwrap()).sum();
assert_eq!(lang_top1, top1 as u64);
}
#[test]
fn default_candidates_ranking_and_summary() {
let fx = fixture("default");
let v = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--json",
]);
assert_eq!(v["n"], 6, "{v}");
let list = strings(&v["candidate_list"]);
assert_eq!(list, DEFAULT_CANDIDATES);
assert_eq!(v["candidates"], 4);
assert!((f(&v["chance"]) - 0.25).abs() < 1e-12);
assert_eq!(v["majority"]["label"], "Asteraceae");
assert_eq!(v["majority"]["n"], 3);
assert_eq!(f(&v["majority"]["acc"]), 0.5);
assert_eq!(v["expect_outside_candidates"], 0);
assert_eq!(v["top5"], 6);
assert_eq!(v["candidates_source"], "expect[0] of the prompt set");
assert_eq!(v["norm"], "sum");
assert_eq!(v["scorer"], "shared");
assert_eq!(v["bos"], false);
assert_eq!(v["route_mode"], "none");
assert_eq!(v["route_counts"]["backbone"], 6);
assert_eq!(v["growth"], "active");
assert_eq!(v["shell"], "on");
let mc = f(&v["mean_lp_correct"]);
let mw = f(&v["mean_lp_best_wrong"]);
assert!(mc.is_finite() && mw.is_finite());
assert!((f(&v["mean_margin"]) - (mc - mw)).abs() < 1e-9, "{v}");
check_rows(&v, &list);
let again = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--json",
]);
assert_eq!(again["per_row"], v["per_row"]);
let _ = std::fs::remove_dir_all(&fx.dir);
}
#[test]
fn explicit_candidates_and_mean_norm() {
let fx = fixture("cands");
let cands = fx.dir.join("cands.jsonl");
std::fs::write(
&cands,
"{\"text\":\"Asteraceae\"}\n{\"text\":\"Rosaceae\"}\n{\"text\":\" asteraceae\"}\n{\"text\":\"Apiaceae\"}\n",
)
.unwrap();
let sum = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--candidates-jsonl",
s(&cands),
"--json",
]);
let list = strings(&sum["candidate_list"]);
assert_eq!(list, ["Asteraceae", "Rosaceae", "Apiaceae"]);
assert_eq!(sum["candidates"], 3);
assert!((f(&sum["chance"]) - 1.0 / 3.0).abs() < 1e-12);
assert_eq!(sum["candidates_source"], s(&cands));
assert_eq!(sum["expect_outside_candidates"], 2, "{sum}");
let rows = sum["per_row"].as_array().unwrap();
assert_eq!(rows[1]["rank_of_correct"], serde_json::Value::Null);
assert_eq!(rows[4]["rank_of_correct"], serde_json::Value::Null);
assert!(rows[0]["rank_of_correct"].is_u64());
assert!(rows[3]["rank_of_correct"].is_u64(), "lower-case answer folds: {}", rows[3]);
assert_eq!(sum["majority"]["label"], "Asteraceae");
assert_eq!(sum["majority"]["n"], 3);
check_rows(&sum, &list);
let mean = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--candidates-jsonl",
s(&cands),
"--norm",
"mean",
"--json",
]);
assert_eq!(mean["norm"], "mean");
check_rows(&mean, &list);
for (a, b) in sum["per_row"]
.as_array()
.unwrap()
.iter()
.zip(mean["per_row"].as_array().unwrap())
{
if let Some(tok) = a["tokens_correct"].as_f64() {
let want = f(&a["lp_correct"]) / tok;
assert!((f(&b["lp_correct"]) - want).abs() < 1e-9, "{a}\n{b}");
}
}
let bad = cortiq(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--norm",
"median",
"--json",
]);
assert!(!bad.status.success());
let _ = std::fs::remove_dir_all(&fx.dir);
}
#[test]
fn shared_walk_matches_full_reforward() {
let fx = fixture("scorer");
let shared = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--json",
]);
let full = json(&[
"probe-choice",
s(&fx.f0),
"--prompts-jsonl",
s(&fx.prompts),
"--scorer",
"full",
"--json",
]);
assert_eq!(full["scorer"], "full");
let (a, b) = (
shared["per_row"].as_array().unwrap(),
full["per_row"].as_array().unwrap(),
);
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b) {
for key in ["lp_correct", "lp_best", "lp_best_wrong"] {
let (p, q) = (f(&x[key]), f(&y[key]));
assert!(
(p - q).abs() < 1e-3,
"{key}: shared {p} vs full {q}\n{x}\n{y}"
);
}
assert_eq!(x["predicted"], y["predicted"], "{x}\n{y}");
assert_eq!(x["rank_of_correct"], y["rank_of_correct"]);
}
assert_eq!(shared["top1"], full["top1"]);
let _ = std::fs::remove_dir_all(&fx.dir);
}