Files
Aura/crates/aura-cli/tests/run_measurement.rs
T
claude b18a695531 feat(aura-runner, aura-cli, aura-registry): a run's trace directory is keyed by its own identity
A single run recorded into `runs/traces/<render-name>/`, so a second run of the
same blueprint overwrote the first and could leave a stale tap file beside an
index that no longer listed it. The directory is now
`runs/traces/<render-name>-<id8>/`, where the id is a digest over the run's own
manifest: two runs differing in any identity-bearing input land in two
directories, two runs differing in nothing land in one.

The digest hashes the manifest with its two provenance fields removed and its
parameter vectors merged.

- `commit` (the aura binary's build sha) and `project.commit` (the project
  repo's HEAD plus a `-dirty` marker, re-derived on every invocation) record who
  built or checked out the code, not what the run was. The latter would have
  minted a fresh directory for any uncommitted file anywhere in the project
  worktree — the authoring loop's normal state.
- `params` and `defaults` are merged name-sorted before hashing: how a value was
  supplied is not what the run is, so a no-op `--override` no longer splits one
  run across two directories. The two vectors are disjoint by construction.

`project.dylib_sha256` stays in. It is what the C13 hot-reload comparison
varies — an unchanged blueprint against a rebuilt node crate — which the
campaign leg structurally cannot express, since its axes vary params,
instrument and window, never code.

The manifest is now assembled before the tap bind rather than after the run.
Every input it needs was already resolved at that point, and the one value both
keys the directory and enters the report, so a handle can never name one
identity while the record describes another.

`aura chart <render-name>` no longer names a directory; it now lists the trace
handles beginning with that name instead of refusing blankly.
`TraceStoreError::NameTaken` stops prescribing `--trace`, retired in #319.

C18, C22, C23 and C27 record the new identity, each superseded clause moved
verbatim to its history sidecar; C23 gains its first.

closes #311
2026-07-27 10:52:56 +02:00

185 lines
8.7 KiB
Rust

//! #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 `trace_name` handle a recording run printed on its stdout record line.
/// #311: the trace directory is `<render-name>-<id8>`, keyed by the run's own
/// identity, so the path is derived from this value rather than hard-coded.
fn trace_handle(stdout: &[u8]) -> String {
let line = String::from_utf8_lossy(stdout);
let v: serde_json::Value =
serde_json::from_str(line.trim()).expect("stdout is one JSON record line");
v["trace_name"]
.as_str()
.expect("a recording run prints its trace_name")
.to_string()
}
/// 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(["exec", 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").join(trace_handle(&out.stdout)).join("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(["exec", 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").join(trace_handle(&out.stdout)).join("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}");
}
/// #331 delta re-review, the third authored-file intake route: `dispatch_run`
/// loads a FRESH FILE via `blueprint_from_json` and, on this measurement arm,
/// feeds `signal.name()` straight into `bind_tap_plan` -> `TraceStore::begin_run`
/// (`trace_store.rs` joins the name unsanitized) — a hand-crafted envelope with
/// `"name":"../x"` used to reach `--tap fast_tap=record` and escape
/// `runs/traces/<name>/` for `runs/x/` (one level up, since `traces/../x`
/// normalizes there) instead of refusing at the same root-name choke point
/// `register`/`introspect --content-id <FILE>` already gate through. This pins
/// the closed route: exit nonzero (dispatch_run's own bad-input-file
/// convention, exit 2), the shared `name_gate_fault_prose` wording, and — the
/// property that actually matters — the escaped directory is never created.
#[test]
fn run_refuses_a_hand_crafted_envelope_with_a_bad_root_name_before_any_trace_write() {
let cwd = temp_cwd("bad-root-name");
let mut v: serde_json::Value =
serde_json::from_str(&measurement_blueprint_json()).expect("parse measurement blueprint");
v["blueprint"]["name"] = serde_json::json!("../x");
let bad = serde_json::to_string(&v).expect("re-serialize with a shape-violating root name");
let bp_path = cwd.join("bad-root-name.json");
std::fs::write(&bp_path, &bad).expect("write bad-root-name blueprint");
let out = Command::new(BIN)
.args(["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=record"])
.current_dir(&cwd)
.output()
.expect("spawn aura run");
assert_eq!(
out.status.code(),
Some(2),
"a shape-violating root name refuses at dispatch_run's own bad-file exit code: {:?} stderr={}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains(r#"blueprint name "../x" is invalid"#),
"the shared name_gate_fault_prose wording names the offending root name: {stderr}"
);
assert!(
out.stdout.is_empty(),
"a refused run must not print a report: {:?}",
String::from_utf8_lossy(&out.stdout)
);
// The property that actually matters: the escaped write target (one level
// up from `traces/`, since `trace_store.rs` joins the name unsanitized)
// was never created — the gate fires before `begin_run` ever touches disk.
assert!(
!cwd.join("runs/x").exists(),
"the escaped trace directory must never be created"
);
assert!(
!cwd.join("runs/traces").exists(),
"no trace directory of any shape is written on a refused run"
);
}
/// #309: the measurement leg reports its trace handle exactly as the strategy
/// leg does — both bind through one shared tap-plan pair, so a handle present
/// on one and absent on the other would mean the seam had drifted.
#[test]
fn measurement_run_reports_its_trace_handle_on_stdout() {
let cwd = temp_cwd("measurement-trace-handle");
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(["exec", bp_path.to_str().expect("utf-8 path")])
.current_dir(&cwd)
.output()
.expect("spawn exec");
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let line = String::from_utf8_lossy(&out.stdout);
let report: serde_json::Value =
serde_json::from_str(line.trim()).expect("stdout is one JSON object");
let handle = report["trace_name"].as_str().expect("trace_name is present and a string");
assert!(
cwd.join("runs/traces").join(handle).join("fast_tap.json").exists(),
"the reported handle must name the directory the tapped series landed in; got {handle}"
);
let _ = std::fs::remove_dir_all(&cwd);
}