//! #286 / C28 phase 3: `aura run` on a measurement-shaped blueprint (declares a //! tap, exposes no `bias`) runs BARE — no wrap_r scaffold — emits a //! `MeasurementReport` and persists the tapped series. A taps-only blueprint is //! refused before this feature; this pins the new additive behaviour. 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-meas-{name}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("create temp cwd"); dir } /// The shipped `examples/r_sma.json`, turned MEASUREMENT-shaped: one declared tap /// on node 0 ("fast", SMA(2)) field 0, and its `bias` output removed. Same closed /// topology (`sma_signal`), so it runs on the built-in synthetic stream. fn measurement_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": "fast_tap", "from": {"node": 0, "field": 0}}]); v["blueprint"]["output"] = serde_json::json!([]); // no `bias` — a measurement, not a strategy serde_json::to_string(&v).expect("re-serialize measurement blueprint") } #[test] fn measurement_blueprint_runs_bare_and_emits_its_tap() { let cwd = temp_cwd("runs-bare"); let bp_path = cwd.join("measurement.json"); std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint"); let out = Command::new(BIN) .args(["run", bp_path.to_str().expect("utf-8 path")]) .current_dir(&cwd) .output() .expect("spawn aura run"); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); // stdout is a MeasurementReport: the declared tap name, no R `metrics`, the // measurement broker sentinel. let report: serde_json::Value = serde_json::from_slice(&out.stdout).expect("stdout parses as MeasurementReport JSON"); assert_eq!(report["taps"], serde_json::json!(["fast_tap"])); assert!(report.get("metrics").is_none(), "a measurement run has no R metrics"); assert_eq!(report["manifest"]["broker"], "measurement"); // The tapped series is persisted through the trace store. let trace_path = cwd.join("runs/traces/sma_signal/fast_tap.json"); let trace_text = std::fs::read_to_string(&trace_path) .unwrap_or_else(|e| panic!("expected a persisted tap trace at {}: {e}", trace_path.display())); let trace: serde_json::Value = serde_json::from_str(&trace_text).expect("parse tap trace json"); assert_eq!(trace["tap"], "fast_tap"); assert_eq!(trace["kinds"], serde_json::json!(["F64"])); } /// #310: the measurement arm honours the `--tap` selector — a `last` /// fold lands one summary row instead of the full series. #[test] fn measurement_run_honours_the_tap_selector() { let cwd = temp_cwd("selector-last"); let bp_path = cwd.join("measurement.json"); std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint"); let out = Command::new(BIN) .args(["run", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=last"]) .current_dir(&cwd) .output() .expect("spawn aura run"); assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr)); let trace_text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/fast_tap.json")) .expect("read fold trace"); let trace: serde_json::Value = serde_json::from_str(&trace_text).expect("parse fold trace"); let rows = trace["columns"][0].as_array().expect("columns[0] array"); assert_eq!(rows.len(), 1, "a fold lands exactly one summary row"); // last warm SMA(2) value over the synthetic stream = (1.0097 + 1.0092) / 2 let got = rows[0].as_f64().expect("f64 row"); let expected = (1.0097_f64 + 1.0092) / 2.0; assert!((got - expected).abs() < 1e-9, "last row: got {got}, expected {expected}"); }