Files
Aura/crates/aura-cli/tests/measure_ic.rs
T
claude 5dc8e03249 test(aura-cli): re-target the run-vehicle argv sites onto exec
Slice 4 of the #319 retirement: 34 mechanical argv swaps across six test
files (tap_recording 12, project_load 13, run_measurement 3, measure_ic 1,
run_refuses_unrunnable_blueprint 1, graph_construct 4 run-halves) — every
site drove run as a synthetic vehicle with at most --tap, so no one-cell
campaign conversions were needed; assertions untouched. graph_construct's
sweep sites and the remaining vehicle files stay for the next slice.

refs #319
2026-07-25 19:01:29 +02:00

126 lines
5.5 KiB
Rust

//! #290 / C28: `aura measure ic <run> --signal <tap> --price <tap>` reduces a
//! measurement run's two recorded taps to an Information Coefficient + its
//! permutation-null significance. Exercises the WIRING and well-formedness over a
//! real causal run; the signal-vs-noise math is unit-tested (a look-ahead-engineered
//! signal is impossible in a causal run, C2).
use std::path::Path;
use std::process::Command;
const BIN: &str = env!("CARGO_BIN_EXE_aura");
fn temp_cwd(name: &str) -> std::path::PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-measic-{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp cwd");
dir
}
/// `examples/r_sma.json` turned MEASUREMENT-shaped with TWO taps (node 0 "signal",
/// node 1 "price") and no `bias` output — same closed topology, runs on the built-in
/// synthetic stream. (Two-tap authoring mirrors tests/tap_recording.rs.)
fn two_tap_blueprint_json() -> String {
let path = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let doc = std::fs::read_to_string(path).expect("read examples/r_sma.json");
let mut v: serde_json::Value = serde_json::from_str(&doc).expect("parse r_sma.json");
v["blueprint"]["taps"] = serde_json::json!([
{"name": "signal", "from": {"node": 0, "field": 0}},
{"name": "price", "from": {"node": 1, "field": 0}},
]);
v["blueprint"]["output"] = serde_json::json!([]);
serde_json::to_string(&v).expect("re-serialize measurement blueprint")
}
fn run_measurement(cwd: &Path) {
let bp = cwd.join("measurement.json");
std::fs::write(&bp, two_tap_blueprint_json()).expect("write blueprint");
let out = Command::new(BIN)
.args(["exec", bp.to_str().unwrap()])
.current_dir(cwd)
.output()
.expect("spawn aura run");
assert!(out.status.success(), "aura run stderr: {}", String::from_utf8_lossy(&out.stderr));
}
fn measure_ic(cwd: &Path, run: &str, extra: &[&str]) -> std::process::Output {
let mut args = vec!["measure", "ic", run, "--signal", "signal", "--price", "price"];
args.extend_from_slice(extra);
Command::new(BIN).args(&args).current_dir(cwd).output().expect("spawn aura measure ic")
}
#[test]
fn measure_ic_emits_a_well_formed_report() {
let cwd = temp_cwd("wellformed");
run_measurement(&cwd);
let out = measure_ic(&cwd, "sma_signal", &[]);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let r: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is IcReport JSON");
assert_eq!(r["run"], "sma_signal");
assert_eq!(r["signal_tap"], "signal");
assert_eq!(r["price_tap"], "price");
assert_eq!(r["horizon"], 1);
assert_eq!(r["permutations"], 1000);
assert_eq!(r["seed"], 0);
assert!(r["n_pairs"].as_u64().unwrap() >= 2, "expected aligned pairs, got {}", r["n_pairs"]);
let ic = r["information_coefficient"].as_f64().unwrap();
assert!(ic.is_finite() && (-1.0..=1.0).contains(&ic), "ic = {ic}");
let p = r["overfit_probability"].as_f64().unwrap();
assert!(p > 0.0 && p <= 1.0, "overfit_probability = {p}");
}
#[test]
fn measure_ic_is_deterministic() {
let cwd = temp_cwd("determinism");
run_measurement(&cwd);
let a = measure_ic(&cwd, "sma_signal", &["--seed", "9"]);
let b = measure_ic(&cwd, "sma_signal", &["--seed", "9"]);
assert!(a.status.success() && b.status.success());
assert_eq!(a.stdout, b.stdout, "same seed → byte-identical report");
}
#[test]
fn measure_ic_unknown_run_errors() {
let cwd = temp_cwd("unknownrun");
run_measurement(&cwd);
let out = measure_ic(&cwd, "no_such_run", &[]);
assert!(!out.status.success(), "an unknown run must exit non-zero");
}
/// `--horizon` is real plumbing from the CLI arg through
/// `information_coefficient`'s alignment window, not merely echoed into the
/// report: a horizon that exceeds the recorded price series collapses every
/// signal/forward-return pair, hitting the reduction's documented degenerate
/// floor (`n_pairs=0`, `ic=0.0`, `overfit_probability=1.0`, per main.rs's
/// `information_coefficient` doc comment) — a floor the unit tests exercise
/// only by calling the reduction in-memory, never through CLI parsing + a
/// persisted-trace round trip.
#[test]
fn measure_ic_oversized_horizon_degenerates_through_the_cli() {
let cwd = temp_cwd("oversizedhorizon");
run_measurement(&cwd);
let out = measure_ic(&cwd, "sma_signal", &["--horizon", "100000000"]);
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let r: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout is IcReport JSON");
assert_eq!(r["horizon"], 100_000_000);
assert_eq!(r["n_pairs"], 0, "an oversized horizon aligns no pairs");
assert_eq!(r["information_coefficient"], 0.0, "degenerate floor: ic = 0.0");
assert_eq!(r["overfit_probability"], 1.0, "degenerate floor: overfit_probability = 1.0");
}
#[test]
fn measure_ic_missing_tap_errors() {
let cwd = temp_cwd("missingtap");
run_measurement(&cwd);
let out = Command::new(BIN)
.args(["measure", "ic", "sma_signal", "--signal", "nope", "--price", "price"])
.current_dir(&cwd)
.output()
.expect("spawn");
assert!(!out.status.success(), "a missing tap must exit non-zero");
assert!(
String::from_utf8_lossy(&out.stderr).contains("nope"),
"the error names the missing tap: {}",
String::from_utf8_lossy(&out.stderr)
);
}