diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index f87b2dd..8c7c677 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -3582,6 +3582,61 @@ fn generalize_grades_a_candidate_across_two_instruments() { assert!(stdout.contains("\"family_id\":\"generalize-0\""), "family handle: {stdout}"); } +/// Characterization pin (byte-identity anchor for the generalize dissolution, +/// #210). The current `aura generalize` binds its protective stop as a grid axis +/// (`r_sma_sweep_family` with `stop_open=true`); the dissolution rebinds the same +/// stop through the risk-regime seam (`RiskRegime::Vol -> StopRule::Vol`). The +/// sibling `generalize_grades_a_candidate_across_two_instruments` asserts shape +/// only — `worst_case` and the per-instrument R floats are never checked — so a +/// stop-mechanism divergence between the two bindings would ship green. This pins +/// the EXACT current R grade of the identical invocation; after the dissolution +/// the same command must reproduce these bytes (the acceptance gate), and any +/// drift fails here loudly. Gated on the shared GER40/USDJPY Sept-2024 archive; +/// skips cleanly on a data refusal. +#[test] +fn generalize_real_e2e_pins_the_exact_current_grade() { + const FROM_MS: &str = "1725148800000"; + const TO_MS: &str = "1727740799999"; + let cwd = temp_cwd("generalize-exact-grade"); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args([ + "generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY", + "--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0", + "--from", FROM_MS, "--to", TO_MS, + ]) + .current_dir(&cwd) + .output() + .expect("spawn aura"); + if out.status.code() == Some(1) { + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("no local data") || stderr.contains("no recorded geometry"), + "exit 1 must be a data refusal, got: {stderr}" + ); + eprintln!("skip: no local GER40/USDJPY data"); + return; + } + assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status); + let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout"); + let grade_line = stdout + .lines() + .find(|l| l.starts_with("{\"generalize\":")) + .unwrap_or_else(|| panic!("the generalize grade line: {stdout}")); + let v: serde_json::Value = serde_json::from_str(grade_line).expect("grade line parses as JSON"); + let g = &v["generalize"]; + assert_eq!(g["metric"].as_str(), Some("expectancy_r"), "metric: {grade_line}"); + assert_eq!(g["n_instruments"].as_u64(), Some(2), "two instruments: {grade_line}"); + assert_eq!(g["sign_agreement"].as_u64(), Some(2), "both agree in sign: {grade_line}"); + // EXACT R floats — the byte-identity anchor the dissolution must preserve. + assert_eq!(g["worst_case"].as_f64(), Some(0.005795903617609842), "worst-case R floor: {grade_line}"); + let per = g["per_instrument"].as_array().expect("per_instrument is an array"); + assert_eq!(per.len(), 2, "two per-instrument grades: {grade_line}"); + assert_eq!(per[0][0].as_str(), Some("GER40"), "first instrument: {grade_line}"); + assert_eq!(per[0][1].as_f64(), Some(0.01056371324510624), "GER40 expectancy R: {grade_line}"); + assert_eq!(per[1][0].as_str(), Some("USDJPY"), "second instrument: {grade_line}"); + assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {grade_line}"); +} + /// Property: a single-instrument generalize is refused at parse time (exit 2), /// before any data access — so it asserts the arity refusal on any machine. #[test]