78b80ec0fd
The #319 retirement's constructive half, slice 1: a new top-level `exec <target>` executes a campaign document (file, register-then-run, or 64-hex content id — byte-behaviour of the retired `campaign run`, which no longer parses) or a fully-bound signal blueprint (synthetic single run, record line byte-identical to `aura run`'s, root-name gate migrated). Flag discipline is per-leg: --parallel-instruments on campaign targets, --tap on blueprint targets (refused on campaign targets with prose). File routing peeks the document's top-level "kind" key: is_blueprint_file alone cannot tell two .json classes apart. --override (the #246 residue) and the measurement leg follow in later slices; run/sweep/walkforward/mc/generalize stay alive until the removal slice. refs #319
398 lines
18 KiB
Rust
398 lines
18 KiB
Rust
//! exec — the one executor verb (#319): campaign file, campaign id,
|
|
//! blueprint single run. Flag discipline is per-leg.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
mod common;
|
|
use common::{fresh_project, fresh_project_with_data, ScratchGuard, ScratchPath};
|
|
|
|
/// A fresh, unique working directory for an exec test that persists
|
|
/// content-addressed documents under `./runs/` (so nothing dirties the
|
|
/// repo). Unique per test + per process (mirrors `research_docs.rs`).
|
|
fn temp_cwd(name: &str) -> PathBuf {
|
|
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-exec-{name}"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).expect("create temp cwd");
|
|
dir
|
|
}
|
|
|
|
fn run_code_in(dir: &Path, args: &[&str]) -> (String, Option<i32>) {
|
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(args)
|
|
.current_dir(dir)
|
|
.output()
|
|
.expect("binary runs");
|
|
let text = format!(
|
|
"{}{}",
|
|
String::from_utf8_lossy(&out.stdout),
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
(text, out.status.code())
|
|
}
|
|
|
|
fn write_doc(dir: &Path, name: &str, text: &str) -> PathBuf {
|
|
let p = dir.join(name);
|
|
std::fs::write(&p, text).expect("write doc");
|
|
p
|
|
}
|
|
|
|
/// The minimal executable pipeline (one sweep stage) — copied verbatim from
|
|
/// `research_docs.rs`'s constant of the same name.
|
|
const SWEEP_ONLY_PROCESS_DOC: &str = r#"{
|
|
"format_version": 1,
|
|
"kind": "process",
|
|
"name": "sweep-only",
|
|
"pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ]
|
|
}"#;
|
|
|
|
/// A campaign document over an unresolved blueprint/process pair — copied
|
|
/// verbatim from `research_docs.rs::CAMPAIGN_DOC` (the outside-project-gate
|
|
/// fixture; referential resolution never runs since the project gate fires
|
|
/// first).
|
|
const CAMPAIGN_DOC: &str = r#"{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "screen",
|
|
"data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] },
|
|
"strategies": [ { "ref": { "content_id": "9f3a" },
|
|
"axes": { "fast": { "kind": "I64", "values": [8] } } } ],
|
|
"process": { "ref": { "content_id": "4e2d" } },
|
|
"seed": 1,
|
|
"presentation": { "persist_taps": [], "emit": ["family_table"] }
|
|
}"#;
|
|
|
|
/// Seed one blueprint (its params bound, reopened by the sweep axes per
|
|
/// #246) into the built demo project's store via a real sweep and return
|
|
/// its content id — copied verbatim from `research_docs.rs`'s recipe of the
|
|
/// same name.
|
|
fn seed_blueprint(dir: &Path, name: &str) -> String {
|
|
let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
|
let (out, code) = run_code_in(
|
|
dir,
|
|
&[
|
|
"sweep",
|
|
&closed_bp,
|
|
"--axis",
|
|
"fast.length=2,4",
|
|
"--axis",
|
|
"slow.length=8,16",
|
|
"--name",
|
|
name,
|
|
],
|
|
);
|
|
assert_eq!(code, Some(0), "seed sweep failed: {out}");
|
|
std::fs::read_dir(dir.join("runs").join("blueprints"))
|
|
.expect("blueprints dir")
|
|
.next()
|
|
.expect("one stored blueprint")
|
|
.expect("dir entry")
|
|
.path()
|
|
.file_stem()
|
|
.expect("stem")
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
}
|
|
|
|
/// Register `doc` as a process document in the project store; returns its
|
|
/// id — copied verbatim from `research_docs.rs`'s recipe of the same name.
|
|
fn register_process_doc(dir: &Path, file: &str, doc: &str) -> String {
|
|
write_doc(dir, file, doc);
|
|
let (out, code) = run_code_in(dir, &["process", "register", file]);
|
|
assert_eq!(code, Some(0), "process register failed: {out}");
|
|
out.lines()
|
|
.find(|l| l.starts_with("registered process "))
|
|
.expect("register line")
|
|
.trim_start_matches("registered process ")
|
|
.split(' ')
|
|
.next()
|
|
.expect("id")
|
|
.trim_start_matches("content:")
|
|
.to_string()
|
|
}
|
|
|
|
/// Register `doc` as a campaign document in the project store; returns its id.
|
|
fn register_campaign_doc(dir: &Path, file: &str, doc: &str) -> String {
|
|
write_doc(dir, file, doc);
|
|
let (out, code) = run_code_in(dir, &["campaign", "register", file]);
|
|
assert_eq!(code, Some(0), "campaign register failed: {out}");
|
|
out.lines()
|
|
.find(|l| l.starts_with("registered campaign "))
|
|
.expect("register line")
|
|
.trim_start_matches("registered campaign ")
|
|
.split(' ')
|
|
.next()
|
|
.expect("id")
|
|
.to_string()
|
|
}
|
|
|
|
/// [`campaign_doc_json_for`]'s one-instrument shape — copied verbatim from
|
|
/// `research_docs.rs`'s recipe of the same name.
|
|
fn campaign_doc_json_for(
|
|
instrument: &str,
|
|
bp_id: &str,
|
|
proc_id: &str,
|
|
window: (i64, i64),
|
|
) -> String {
|
|
format!(
|
|
r#"{{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "run-seam",
|
|
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
|
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
|
"axes": {{ "fast.length": {{ "kind": "I64", "values": [2, 4] }},
|
|
"slow.length": {{ "kind": "I64", "values": [8, 16] }} }} }} ],
|
|
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
|
"seed": 7,
|
|
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
|
}}"#,
|
|
from = window.0,
|
|
to = window.1,
|
|
)
|
|
}
|
|
|
|
/// Campaign file target: register-then-run, member lines, exit 0 — byte
|
|
/// behaviour of the retired `campaign run <file>`. Fixture built exactly as
|
|
/// `research_docs.rs`'s campaign-run e2es do (`fresh_project_with_data`,
|
|
/// `seed_blueprint`, `register_process_doc`), over a window fully inside
|
|
/// SYMA's synthetic span (2024-01..08) so the run completes clean (exit 0,
|
|
/// not the parallel-instruments-fault sibling's exit 3).
|
|
#[test]
|
|
fn exec_campaign_file_registers_and_runs() {
|
|
let (dir, _fixture) = fresh_project_with_data();
|
|
let runs_dir = dir.join("runs");
|
|
std::fs::remove_dir_all(&runs_dir).ok();
|
|
let _cleanup = ScratchGuard(vec![
|
|
ScratchPath::Dir(runs_dir.clone()),
|
|
ScratchPath::File(dir.join("execfile.process.json")),
|
|
ScratchPath::File(dir.join("execfile.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-file-seed");
|
|
let proc_id = register_process_doc(&dir, "execfile.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "execfile.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", "execfile.campaign.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.lines().any(|l| l.starts_with(r#"{"family_id":"#)),
|
|
"the emit-gated family member line must appear: {out}"
|
|
);
|
|
let line = out
|
|
.lines()
|
|
.find(|l| l.starts_with("{\"campaign_run\":"))
|
|
.expect("the always-on final campaign_run line");
|
|
let v: serde_json::Value = serde_json::from_str(line).expect("record parses");
|
|
assert_eq!(v["campaign_run"]["cells"].as_array().expect("cells").len(), 1);
|
|
}
|
|
|
|
/// Campaign id target with `--parallel-instruments`.
|
|
#[test]
|
|
fn exec_campaign_id_runs_with_parallel_instruments_bound() {
|
|
let (dir, _fixture) = fresh_project_with_data();
|
|
let runs_dir = dir.join("runs");
|
|
std::fs::remove_dir_all(&runs_dir).ok();
|
|
let _cleanup = ScratchGuard(vec![
|
|
ScratchPath::Dir(runs_dir.clone()),
|
|
ScratchPath::File(dir.join("execid.process.json")),
|
|
ScratchPath::File(dir.join("execid.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-id-seed");
|
|
let proc_id = register_process_doc(&dir, "execid.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
let id = register_campaign_doc(&dir, "execid.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", &id, "--parallel-instruments", "2"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// The retired executor spelling no longer parses.
|
|
#[test]
|
|
fn campaign_run_no_longer_parses() {
|
|
let dir = temp_cwd("campaign-run-gone");
|
|
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
|
|
let (out, code) = run_code_in(&dir, &["campaign", "run", "c.campaign.json"]);
|
|
assert_eq!(code, Some(2), "clap unknown-subcommand is a usage error: {out}");
|
|
assert!(
|
|
out.contains("unrecognized subcommand") || out.contains("Usage"),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
}
|
|
|
|
/// `--tap` is blueprint-leg-only: a campaign FILE target with `--tap` refuses
|
|
/// with prose, never silently ignoring the flag and running anyway.
|
|
#[test]
|
|
fn exec_tap_on_a_campaign_target_refuses_with_prose() {
|
|
let (dir, _fixture) = fresh_project();
|
|
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
|
|
let (out, code) = run_code_in(&dir, &["exec", "c.campaign.json", "--tap", "signal=mean"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("--tap applies only to a blueprint target"), "stdout/stderr: {out}");
|
|
assert!(out.contains("declares its persisted taps itself"), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// exec's campaign leg refuses outside a project exactly as `campaign run`
|
|
/// always has (no store touched).
|
|
#[test]
|
|
fn exec_campaign_target_outside_project_refuses() {
|
|
let dir = temp_cwd("exec-campaign-outside-project");
|
|
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
|
|
let (out, code) = run_code_in(&dir, &["exec", "c.campaign.json"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(out.contains("campaign run needs a project"), "stdout/stderr: {out}");
|
|
assert!(!dir.join("runs").exists(), "a refused run must not create a store outside a project");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 2: the exec blueprint leg (synthetic single run).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn run_code(args: &[&str]) -> (String, Option<i32>) {
|
|
run_code_in(Path::new("."), args)
|
|
}
|
|
|
|
/// The exact synthetic price array `r_sma_prices()` (main.rs) feeds the
|
|
/// `RunData::Synthetic` path — duplicated here (as `tap_recording.rs` does)
|
|
/// so the tapped SMA(2) column can be pinned against an
|
|
/// independently-derived expectation, not a blind capture.
|
|
const R_SMA_PRICES: [f64; 18] = [
|
|
1.0000, 1.0008, 1.0021, 1.0039, 1.0062, 1.0090, 1.0083, 1.0061, 1.0034, 1.0012, 0.9998,
|
|
1.0006, 1.0024, 1.0047, 1.0069, 1.0086, 1.0097, 1.0092,
|
|
];
|
|
|
|
/// The shipped `examples/r_sma.json` blueprint, with one additional declared
|
|
/// tap on node 0 ("fast", `SMA(length=2)`) field 0 — copied verbatim from
|
|
/// `tap_recording.rs::tap_blueprint_json`.
|
|
fn 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": "fast_tap", "from": {"node": 0, "field": 0}}]);
|
|
serde_json::to_string(&v).expect("re-serialize tapped blueprint")
|
|
}
|
|
|
|
/// The `examples/r_sma.json` blueprint turned MEASUREMENT-shaped: one
|
|
/// declared tap, `bias` output removed — copied verbatim from
|
|
/// `run_measurement.rs::measurement_blueprint_json`. The root-name gate
|
|
/// fires before the has-bias/has-tap split, so this fixture works to pin the
|
|
/// gate regardless of which leg the exec blueprint path implements.
|
|
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!([]);
|
|
serde_json::to_string(&v).expect("re-serialize measurement blueprint")
|
|
}
|
|
|
|
/// Blank the one volatile manifest field (`manifest.commit`, the compile-time
|
|
/// build sha) before a byte comparison — the same blanking `aura-bench`'s
|
|
/// `run_line_fingerprint` applies before hashing (`fixed_cost.rs:21-31`).
|
|
fn strip_volatile(line: &str) -> String {
|
|
let mut v: serde_json::Value = serde_json::from_str(line).expect("record line parses");
|
|
if let Some(commit) = v.get_mut("manifest").and_then(|m| m.get_mut("commit")) {
|
|
*commit = serde_json::Value::String(String::new());
|
|
}
|
|
serde_json::to_string(&v).expect("re-serializing a parsed Value cannot fail")
|
|
}
|
|
|
|
/// Single run of a fully-bound blueprint on the synthetic stream: the record
|
|
/// line is byte-shape-identical to `aura run`'s (manifest-first, one line) —
|
|
/// mirrors `cli_run.rs::aura_run_loads_and_runs_a_blueprint_file`.
|
|
#[test]
|
|
fn exec_blueprint_file_emits_the_single_run_record_line() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
let line = out.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let v: serde_json::Value = serde_json::from_str(line).expect("record parses");
|
|
assert!(v.get("manifest").is_some() && v.get("metrics").is_some());
|
|
assert!(v["manifest"].get("topology_hash").is_some());
|
|
assert!(v["manifest"].get("commit").is_some());
|
|
}
|
|
|
|
/// Parity: `exec` and `run` emit the identical record line for the same
|
|
/// file (both verbs alive until Task 8; the pin survives `run`'s removal by
|
|
/// keeping only the exec half and its literal expectations).
|
|
#[test]
|
|
fn exec_blueprint_record_line_matches_runs_line_bytes() {
|
|
let (run_out, run_code_) = run_code(&["run", "examples/r_sma.json"]);
|
|
let (exec_out, exec_code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(run_code_, Some(0), "stdout/stderr: {run_out}");
|
|
assert_eq!(exec_code, Some(0), "stdout/stderr: {exec_out}");
|
|
let run_line = run_out.lines().find(|l| l.starts_with('{')).expect("run record line");
|
|
let exec_line = exec_out.lines().find(|l| l.starts_with('{')).expect("exec record line");
|
|
assert_eq!(strip_volatile(run_line), strip_volatile(exec_line));
|
|
}
|
|
|
|
/// The migrated root-name gate (retirement inventory): exec's blueprint
|
|
/// intake refuses a bad root name with `run`'s exact prose, before any
|
|
/// trace write — mirrors
|
|
/// `run_measurement.rs::run_refuses_a_hand_crafted_envelope_with_a_bad_root_name_before_any_trace_write`.
|
|
#[test]
|
|
fn exec_blueprint_refuses_a_hand_crafted_envelope_with_a_bad_root_name() {
|
|
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, code) = run_code_in(
|
|
&cwd,
|
|
&["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=record"],
|
|
);
|
|
assert_eq!(code, Some(2), "a shape-violating root name refuses at exec's own bad-file exit code: {out}");
|
|
assert!(
|
|
out.contains(r#"blueprint name "../x" is invalid"#),
|
|
"the shared name_gate_fault_prose wording names the offending root name: {out}"
|
|
);
|
|
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");
|
|
}
|
|
|
|
/// Open blueprint refuses (dispatch-boundary refusal, unchanged prose) —
|
|
/// mirrors `cli_run.rs::aura_run_rejects_an_open_blueprint_without_panicking`.
|
|
#[test]
|
|
fn exec_rejects_an_open_blueprint_without_panicking() {
|
|
let cwd = temp_cwd("exec-blueprint-open-reject");
|
|
let fixture = format!("{}/tests/fixtures/r_sma_open.json", env!("CARGO_MANIFEST_DIR"));
|
|
let (out, code) = run_code_in(&cwd, &["exec", &fixture]);
|
|
assert_eq!(code, Some(2), "an open blueprint fails clean (exit 2, not a panic/exit 101): {out}");
|
|
assert!(out.contains("closed blueprint"), "names the closed-blueprint requirement: {out}");
|
|
assert!(!out.contains("panicked"), "must refuse at the dispatch boundary, never panic: {out}");
|
|
}
|
|
|
|
/// `--tap` works on the blueprint leg (fold selector, #310 semantics) —
|
|
/// mirrors `tap_recording.rs::run_tap_selector_persists_a_fold_summary_row`.
|
|
#[test]
|
|
fn exec_blueprint_tap_selector_persists_a_fold_summary_row() {
|
|
let cwd = temp_cwd("exec-tap-selector-mean");
|
|
let bp_path = cwd.join("tapped_r_sma.json");
|
|
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
|
|
|
let (out, code) = run_code_in(
|
|
&cwd,
|
|
&["exec", bp_path.to_str().expect("utf-8 path"), "--tap", "fast_tap=mean"],
|
|
);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
|
|
let index_text = std::fs::read_to_string(cwd.join("runs/traces/sma_signal/index.json"))
|
|
.expect("read index.json");
|
|
let index: serde_json::Value = serde_json::from_str(&index_text).expect("parse index.json");
|
|
assert_eq!(index["taps"], serde_json::json!(["fast_tap"]));
|
|
|
|
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");
|
|
assert_eq!(trace["tap"], "fast_tap");
|
|
let rows = trace["columns"][0].as_array().expect("columns[0] array");
|
|
assert_eq!(rows.len(), 1, "a fold lands exactly one summary row");
|
|
let sma: Vec<f64> = (1..R_SMA_PRICES.len())
|
|
.map(|i| (R_SMA_PRICES[i - 1] + R_SMA_PRICES[i]) / 2.0)
|
|
.collect();
|
|
let expected = sma.iter().sum::<f64>() / sma.len() as f64;
|
|
let got = rows[0].as_f64().expect("f64 row");
|
|
assert!((got - expected).abs() < 1e-9, "mean row: got {got}, expected {expected}");
|
|
}
|