567f98b4e5
Harvest sweep, review re-check fix. The prior fix corrected only the stdout record line: run_signal_r persists the manifest to runs/traces/<name>/index.json (bound.finish) BEFORE the post-hoc overwrite ran, so an overridden run's trace store still carried the reopened hash — diverging from stdout in exactly the case the revised C24/C18 clause describes. run_signal_r now takes topo: Option<&str> (None = inline computation as before; Some = the caller's reference-semantics hash, the run_blueprint_member precedent) so record line and trace index read the one hash built before any persistence; exec's override branch passes the base document's id and the post-run mutation is gone. RED-first: the new trace-store pin failed against the divergent state. refs #343
1705 lines
81 KiB
Rust
1705 lines
81 KiB
Rust
//! exec — the one executor verb (#319): campaign file, campaign id,
|
|
//! blueprint single run. Flag discipline is per-leg.
|
|
|
|
use std::collections::HashSet;
|
|
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 into the built demo project's store via `aura graph
|
|
/// register` and return its content id — copied verbatim from
|
|
/// `research_docs.rs`'s recipe of the same name (#319 — the retired `sweep`
|
|
/// side-effect seeding this used to ride played no role in the returned
|
|
/// content id, a pure function of the loaded blueprint's canonical bytes;
|
|
/// `name` labels the registered id, mirroring the old per-call sweep family
|
|
/// 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, &["graph", "register", &closed_bp, "--name", name]);
|
|
assert_eq!(code, Some(0), "seed register failed: {out}");
|
|
out.lines()
|
|
.find(|l| l.starts_with("registered blueprint "))
|
|
.expect("register line")
|
|
.trim_start_matches("registered blueprint ")
|
|
.split(' ')
|
|
.next()
|
|
.expect("id")
|
|
.trim_start_matches("content:")
|
|
.to_string()
|
|
}
|
|
|
|
/// 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");
|
|
}
|
|
|
|
/// A malformed FILE target (a trailing comma — not valid JSON at all) must
|
|
/// refuse NEUTRALLY at the routing seam, before either leg's own
|
|
/// document-shape validation ever runs — not silently fall through to the
|
|
/// blueprint leg's "blueprint document is not valid JSON" prose (review
|
|
/// Minor-3). The file's shape (blueprint vs. campaign) was never
|
|
/// determined, so neither leg's own wording is the right attribution.
|
|
#[test]
|
|
fn exec_malformed_json_file_refuses_neutrally_not_misrouted() {
|
|
let (dir, _fixture) = fresh_project();
|
|
let malformed = r#"{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "broken",
|
|
}"#;
|
|
write_doc(&dir, "broken.campaign.json", malformed);
|
|
let (out, code) = run_code_in(&dir, &["exec", "broken.campaign.json"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("target file is not valid JSON"), "stdout/stderr: {out}");
|
|
assert!(
|
|
!out.contains("blueprint document is not valid JSON"),
|
|
"must not misattribute to the blueprint leg's own prose: {out}"
|
|
);
|
|
}
|
|
|
|
/// #342 item 3: a valid-JSON op-script (a top-level JSON array of
|
|
/// construction ops) handed to `exec` must not fall through to the
|
|
/// blueprint leg's "blueprint document is not valid JSON" refusal — that
|
|
/// claim is false, the document IS valid JSON, just the wrong shape. It
|
|
/// refuses right at the routing seam with prose that names the op-script
|
|
/// diagnosis and points at the build step that turns it into a runnable
|
|
/// blueprint (`aura graph build`).
|
|
#[test]
|
|
fn exec_op_script_file_refuses_with_build_hint_not_invalid_json() {
|
|
let dir = temp_cwd("exec-op-script");
|
|
let op_script = r#"[
|
|
{"op":"source","role":"price","kind":"F64"},
|
|
{"op":"add","type":"SMA","name":"fast","bind":{"length":{"I64":2}}},
|
|
{"op":"feed","role":"price","into":["fast.series"]},
|
|
{"op":"expose","from":"fast.value","as":"bias"}
|
|
]"#;
|
|
write_doc(&dir, "script.json", op_script);
|
|
let (out, code) = run_code_in(&dir, &["exec", "script.json"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("op-script"), "must name the op-script diagnosis: {out}");
|
|
assert!(out.contains("graph build"), "must point at the build step: {out}");
|
|
assert!(
|
|
!out.contains("not valid JSON"),
|
|
"the document IS valid JSON — must not claim otherwise: {out}"
|
|
);
|
|
}
|
|
|
|
/// #342 item 4/item 5: a kind-bearing document that is not a campaign (here,
|
|
/// a process document) refuses naming the found kind and exec's two
|
|
/// executable classes, instead of the campaign leg's generic "the \"kind\"
|
|
/// key must be \"campaign\"" — which never says WHAT was found, nor what
|
|
/// exec accepts at all. Exit 2, not 1 (C14 exit-partition realignment,
|
|
/// #342 item 5): the target IS a real, existing, valid-JSON file, so its
|
|
/// content is itself part of the argument (C14's usage-class rule) — the
|
|
/// same class as the op-script and not-valid-JSON file-content faults
|
|
/// (`exec_op_script_file_refuses_with_build_hint_not_invalid_json`,
|
|
/// `exec_malformed_json_file_refuses_neutrally_not_misrouted`), not the
|
|
/// runtime class reserved for a target that resolves to no real state at
|
|
/// all (missing file / unregistered content id).
|
|
#[test]
|
|
fn exec_process_document_names_found_kind_and_executable_classes() {
|
|
let dir = temp_cwd("exec-wrong-kind-process");
|
|
write_doc(&dir, "p.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let (out, code) = run_code_in(&dir, &["exec", "p.process.json"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("\"process\""), "must name the found kind: {out}");
|
|
assert!(
|
|
out.contains("campaign document") && out.contains("fully-bound blueprint"),
|
|
"must name both of exec's executable classes: {out}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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")
|
|
}
|
|
|
|
/// 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());
|
|
}
|
|
|
|
/// #324 (comment 4501): the built-in synthetic stream `exec`'s bias leg runs
|
|
/// on is a small (18-cycle) smoke fixture — a degenerate override (fast and
|
|
/// slow SMA lengths coinciding, the same trick the campaign zero-trade
|
|
/// fixtures use) makes the bias-difference constantly zero, so the run
|
|
/// trades zero times over it. The note names the fixture and its cycle
|
|
/// count instead of leaving the all-zero metrics reading as a broken
|
|
/// strategy; the record line and exit code are unaffected.
|
|
#[test]
|
|
fn exec_blueprint_zero_trades_on_synthetic_emits_smoke_fixture_note() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=4"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("aura: note: the built-in synthetic stream (18 cycles) is a small smoke \
|
|
fixture"),
|
|
"the smoke-fixture note must fire, naming the actual cycle count: {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_eq!(
|
|
v["metrics"]["r"]["n_trades"],
|
|
serde_json::json!(0),
|
|
"the degenerate equal-length override must trade zero times: {out}"
|
|
);
|
|
}
|
|
|
|
/// Negative twin: the default (non-degenerate, fast=2/slow=4) run trades at
|
|
/// least once over the same synthetic stream, so no smoke-fixture note
|
|
/// fires — `exec_blueprint_file_emits_the_single_run_record_line`'s fixture,
|
|
/// re-asserted here for the note's absence.
|
|
#[test]
|
|
fn exec_blueprint_with_trades_on_synthetic_emits_no_smoke_fixture_note() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(
|
|
!out.contains("small smoke fixture"),
|
|
"a traded synthetic run emits no smoke-fixture note: {out}"
|
|
);
|
|
}
|
|
|
|
/// 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}");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 2b: the measurement leg of exec's blueprint target.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A no-bias (measurement) blueprint runs through `exec` exactly as through
|
|
/// `run`: same record shape (`manifest` + `taps`, no R `metrics` — a
|
|
/// measurement run has no broker) — mirrors
|
|
/// `run_measurement.rs::measurement_blueprint_runs_bare_and_emits_its_tap`
|
|
/// (a `MeasurementReport` carries no `metrics` field at all, unlike a
|
|
/// strategy `RunReport` — `aura_engine::report::MeasurementReport`'s doc
|
|
/// comment states this explicitly).
|
|
#[test]
|
|
fn exec_measurement_blueprint_emits_the_record_line() {
|
|
let cwd = temp_cwd("exec-measurement-blueprint");
|
|
let bp_path = cwd.join("m.json");
|
|
std::fs::write(&bp_path, measurement_blueprint_json()).expect("write measurement blueprint");
|
|
|
|
let (out, code) = run_code_in(&cwd, &["exec", bp_path.to_str().expect("utf-8 path")]);
|
|
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(), "record: {out}");
|
|
assert_eq!(v["taps"], serde_json::json!(["fast_tap"]));
|
|
assert!(v.get("metrics").is_none(), "a measurement run has no R metrics: {out}");
|
|
assert_eq!(v["manifest"]["broker"], "measurement");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 3: `--override` on the blueprint leg.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// The one deliberate sugar residue: a bound-param override, recorded raw in
|
|
/// `manifest.params` ("what varied") and dropped from `manifest.defaults`
|
|
/// ("what was held") — the #246 reopen mechanism, now reachable through
|
|
/// `exec`. `examples/r_sma.json` binds `fast.length` to 2 (its "fast" `SMA`
|
|
/// node) — the same bound param
|
|
/// `cli_run.rs::sweep_dissolved_accepts_an_axis_over_a_bound_param` reopens
|
|
/// via `--axis` on the sweep-dissolved campaign path.
|
|
#[test]
|
|
fn exec_blueprint_override_reopens_and_records_the_value_in_params() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=8"]);
|
|
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");
|
|
let params = v["manifest"]["params"].as_array().expect("params array");
|
|
assert!(
|
|
params.iter().any(|e| e[0] == "fast.length" && e[1] == serde_json::json!({"I64": 8})),
|
|
"the overridden value must land in params: {out}"
|
|
);
|
|
let defaults = v["manifest"]["defaults"].as_array().expect("defaults array");
|
|
assert!(
|
|
!defaults.iter().any(|e| e[0] == "fast.length"),
|
|
"an overridden bound param drops out of defaults: {out}"
|
|
);
|
|
}
|
|
|
|
/// Without `--override`, the same blueprint's bound values stay untouched:
|
|
/// `fast.length` is a held default, not a varied param.
|
|
#[test]
|
|
fn exec_blueprint_without_override_runs_bound_values_untouched() {
|
|
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");
|
|
let params = v["manifest"]["params"].as_array().expect("params array");
|
|
assert!(
|
|
!params.iter().any(|e| e[0] == "fast.length"),
|
|
"no override means fast.length stays out of params: {out}"
|
|
);
|
|
let defaults = v["manifest"]["defaults"].as_array().expect("defaults array");
|
|
assert!(
|
|
defaults.iter().any(|e| e[0] == "fast.length" && e[1] == serde_json::json!({"I64": 2})),
|
|
"fast.length is a held default at its bound value 2: {out}"
|
|
);
|
|
}
|
|
|
|
/// Ratification pin (#343, revised): `topology_hash` carries REFERENCE
|
|
/// semantics on every leg — it names the (stored/loaded) base document,
|
|
/// never a transient reopened variant, mirroring the campaign leg's own
|
|
/// precedent (`runner.rs` passes `&cell.strategy_id`, the stored base
|
|
/// document's id, as `topo` even for reopened members). `examples/r_sma.json`
|
|
/// already binds `fast.length` to 2, so `--override fast.length=2` is a
|
|
/// no-op override value-wise; it still moves the param from
|
|
/// `manifest.defaults` to `manifest.params` (the #246 reopen mechanism,
|
|
/// unconditionally applied) — the manifest's `params` carry the variation,
|
|
/// while `topology_hash` stamps the base document's own content id (computed
|
|
/// before the reopen) on both legs, so the bare run's and the
|
|
/// no-op-override run's `topology_hash` are EQUAL, and the overridden run's
|
|
/// own hash stays stable across repeated runs (determinism, C1). The
|
|
/// manifest's `params` plus the base document deterministically reconstruct
|
|
/// the executed run (reproduction identity) — see the corrected ratification
|
|
/// prose in C12/C24.
|
|
#[test]
|
|
fn exec_blueprint_override_keeps_the_base_documents_topology_hash() {
|
|
let (base_out, base_code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(base_code, Some(0), "stdout/stderr: {base_out}");
|
|
let base_line = base_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let base_v: serde_json::Value = serde_json::from_str(base_line).expect("record parses");
|
|
let base_hash =
|
|
base_v["manifest"]["topology_hash"].as_str().expect("base topology_hash").to_string();
|
|
|
|
let (ov_out, ov_code) =
|
|
run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=2"]);
|
|
assert_eq!(ov_code, Some(0), "stdout/stderr: {ov_out}");
|
|
let ov_line = ov_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let ov_v: serde_json::Value = serde_json::from_str(ov_line).expect("record parses");
|
|
let ov_hash =
|
|
ov_v["manifest"]["topology_hash"].as_str().expect("overridden topology_hash").to_string();
|
|
|
|
assert_eq!(
|
|
base_hash, ov_hash,
|
|
"an override stamps the base document's own content id — reference semantics (#343): \
|
|
base {base_out} / overridden {ov_out}"
|
|
);
|
|
|
|
let ov_params = ov_v["manifest"]["params"].as_array().expect("params array");
|
|
assert!(
|
|
ov_params.iter().any(|e| e[0] == "fast.length"),
|
|
"the variation lives in the manifest's params, not in the hash: {ov_out}"
|
|
);
|
|
|
|
let (ov_out_2, ov_code_2) =
|
|
run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=2"]);
|
|
assert_eq!(ov_code_2, Some(0), "stdout/stderr: {ov_out_2}");
|
|
let ov_line_2 = ov_out_2.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let ov_v_2: serde_json::Value = serde_json::from_str(ov_line_2).expect("record parses");
|
|
let ov_hash_2 =
|
|
ov_v_2["manifest"]["topology_hash"].as_str().expect("overridden topology_hash (2)");
|
|
assert_eq!(
|
|
ov_hash, ov_hash_2,
|
|
"the overridden run's topology_hash is stable across repeated runs (determinism, C1): \
|
|
{ov_out} / {ov_out_2}"
|
|
);
|
|
}
|
|
|
|
/// Follow-up to the #343 revised pin above: `run_signal_r`'s in-graph tap
|
|
/// persistence (`bound.finish(&manifest)`, `aura-registry::trace_store`)
|
|
/// writes `traces/<name>/index.json` INSIDE `run_signal_r`, before
|
|
/// `exec_blueprint_leg` ever sees the returned `RunReport` — so the
|
|
/// persisted manifest must ALSO carry the base document's reference-semantics
|
|
/// hash, not just the stdout record line. A tap-bearing blueprint (so
|
|
/// `index.json` exists to inspect) with a no-op `--override` pins that the
|
|
/// override run's persisted `topology_hash` equals both its own stdout hash
|
|
/// and the un-overridden base run's hash.
|
|
#[test]
|
|
fn exec_blueprint_override_persists_the_base_documents_hash_in_the_trace_store() {
|
|
let cwd = temp_cwd("override-persisted-hash");
|
|
let bp_path = cwd.join("tapped_r_sma.json");
|
|
std::fs::write(&bp_path, tap_blueprint_json()).expect("write tapped blueprint");
|
|
|
|
let (base_out, base_code) =
|
|
run_code_in(&cwd, &["exec", bp_path.to_str().expect("utf-8 path")]);
|
|
assert_eq!(base_code, Some(0), "stdout/stderr: {base_out}");
|
|
let base_line = base_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let base_v: serde_json::Value = serde_json::from_str(base_line).expect("record parses");
|
|
let base_hash =
|
|
base_v["manifest"]["topology_hash"].as_str().expect("base topology_hash").to_string();
|
|
|
|
let (ov_out, ov_code) = run_code_in(
|
|
&cwd,
|
|
&["exec", bp_path.to_str().expect("utf-8 path"), "--override", "fast.length=2"],
|
|
);
|
|
assert_eq!(ov_code, Some(0), "stdout/stderr: {ov_out}");
|
|
let ov_line = ov_out.lines().find(|l| l.starts_with('{')).expect("record line");
|
|
let ov_v: serde_json::Value = serde_json::from_str(ov_line).expect("record parses");
|
|
let ov_stdout_hash =
|
|
ov_v["manifest"]["topology_hash"].as_str().expect("overridden topology_hash").to_string();
|
|
assert_eq!(ov_stdout_hash, base_hash, "stdout already carries the base document's hash");
|
|
|
|
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");
|
|
let persisted_hash =
|
|
index["manifest"]["topology_hash"].as_str().expect("persisted topology_hash").to_string();
|
|
assert_eq!(
|
|
persisted_hash, base_hash,
|
|
"the trace store's persisted manifest must carry the SAME reference-semantics hash \
|
|
as stdout, not the reopened topology's own — index.json: {index_text}"
|
|
);
|
|
}
|
|
|
|
/// A malformed override token (no `NODE.PARAM=VALUE` shape) refuses as usage.
|
|
#[test]
|
|
fn exec_override_malformed_token_refuses_with_usage() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("--override"), "stdout/stderr: {out}");
|
|
assert!(out.contains("NODE.PARAM=VALUE"), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// An override path naming no param of the blueprint (open or bound) refuses
|
|
/// with `override_paths`' own prose (`member.rs:768-771`), pointing at the
|
|
/// live axis-discovery verb (`aura graph introspect --params`) — the retired
|
|
/// `aura sweep --list-axes` no longer exists.
|
|
#[test]
|
|
fn exec_override_unknown_path_refuses_with_the_override_prose() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "nope.knob=1"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("names no param of this blueprint (open or bound)"),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
assert!(
|
|
out.contains("aura graph introspect --params"),
|
|
"must point at the live discovery verb, not the retired --list-axes: {out}"
|
|
);
|
|
}
|
|
|
|
/// A repeated `--override` path (the same NODE.PARAM named twice) refuses as
|
|
/// a usage error naming the duplicated path — it must NOT fall through to
|
|
/// `reopen_all`'s `NoSuchBoundParam` panic (the second reopen of an
|
|
/// already-reopened bound param, exit 101): the first occurrence reopens
|
|
/// `fast.length`, so a second `override_paths` resolution against the SAME
|
|
/// un-reopened bound-name set still succeeds, and only `reopen_all`'s fold
|
|
/// discovers the collision, too late to refuse cleanly. Mirrors `--tap`'s own
|
|
/// duplicate refusal (`tap_plan_from_args`).
|
|
#[test]
|
|
fn exec_override_duplicate_path_refuses_with_usage_not_a_panic() {
|
|
let (out, code) = run_code(&[
|
|
"exec",
|
|
"examples/r_sma.json",
|
|
"--override",
|
|
"fast.length=5",
|
|
"--override",
|
|
"fast.length=9",
|
|
]);
|
|
assert_ne!(code, Some(101), "must not panic: {out}");
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("fast.length"), "the duplicated path must be named: {out}");
|
|
assert!(out.contains("--override"), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// BUG B1 (fieldtest, #319 exec --override leg): an out-of-domain override
|
|
/// value on a bound param panics deep in the node's own constructor
|
|
/// (`Sma::new`'s `assert!`) instead of refusing — the campaign leg contains
|
|
/// the identical value as a #272 per-cell fault (exit 3, see
|
|
/// `aura_sweep_synthetic_member_panic_is_contained`); the single-run leg has
|
|
/// no cell containment, so the panic must be caught at the dispatch boundary
|
|
/// and rendered as a runtime-class refusal (exit 1, C14 partition) instead of
|
|
/// an uncaught exit-101 panic.
|
|
#[test]
|
|
fn exec_override_out_of_domain_value_refuses_not_panics() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=0"]);
|
|
assert_ne!(code, Some(101), "must not panic: {out}");
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
|
assert!(
|
|
out.contains("SMA length must be >= 1"),
|
|
"the refusal must name the node's own domain rule: {out}"
|
|
);
|
|
}
|
|
|
|
/// BUG B1 twin: a negative override value overflows the SMA's ring-buffer
|
|
/// allocation (`length as usize` on a negative `i64` wraps to a huge
|
|
/// capacity) — same fault class, different panic message, same required
|
|
/// treatment (refuse, exit 1, never 101).
|
|
#[test]
|
|
fn exec_override_negative_value_refuses_not_panics() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=-3"]);
|
|
assert_ne!(code, Some(101), "must not panic: {out}");
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
|
assert!(out.contains("capacity overflow"), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// BUG 2 (#319 fieldtest cycle 2): a negative override value still exits 1
|
|
/// (the earlier fix caught the panic), but the message rendered the raw
|
|
/// allocator string verbatim (`aura: capacity overflow`) — a library panic
|
|
/// payload, not a user-facing diagnostic naming any node's domain rule. The
|
|
/// known non-diagnostic payload must be wrapped in domain-refusal prose (the
|
|
/// raw string kept as parenthetical detail, never the entire message), while
|
|
/// a real constructor message (`SMA length must be >= 1`,
|
|
/// `exec_override_out_of_domain_value_refuses_not_panics` above) stays
|
|
/// byte-identical.
|
|
#[test]
|
|
fn exec_override_negative_value_wraps_the_raw_panic_in_domain_prose() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=-1"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("out of domain"),
|
|
"the raw allocator string must be wrapped in a domain refusal: {out}"
|
|
);
|
|
assert_ne!(
|
|
out.trim(),
|
|
"aura: capacity overflow",
|
|
"the bare allocator string must not be the whole message: {out}"
|
|
);
|
|
}
|
|
|
|
/// BUG 3 (#319 fieldtest cycle 2): a kind-mismatched override (`bias.scale`
|
|
/// is F64, `2` lexes as I64) reaches the exec leg's compile boundary as a
|
|
/// raw `CompileError::ParamKindMismatch { .. }` Debug struct. It must render
|
|
/// as prose naming the param path and both kinds, mirroring the campaign
|
|
/// leg's `ref_fault_prose` treatment of the same fault class.
|
|
#[test]
|
|
fn exec_override_kind_mismatch_refuses_with_prose_not_the_debug_struct() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "bias.scale=2"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
!out.contains("ParamKindMismatch"),
|
|
"the Debug struct must not leak: {out}"
|
|
);
|
|
assert!(out.contains("bias.scale"), "the param path must be named: {out}");
|
|
assert!(out.contains("F64"), "the expected kind must be named: {out}");
|
|
assert!(out.contains("I64"), "the got kind must be named: {out}");
|
|
}
|
|
|
|
/// BUG B2 (fieldtest, #319 exec --override leg): the retired pre-#328
|
|
/// WRAPPED override form (`<rootname>.<node>.<param>`) panics at the
|
|
/// params-zip `.expect` in `exec_blueprint_leg` instead of refusing:
|
|
/// `override_paths` accepts an exact wrapped name too (by design, for
|
|
/// resolving an already-validated set) so the wrapped token "resolves" there,
|
|
/// but the zip below looks the ORIGINAL token up by its raw-translated key,
|
|
/// missing it. Refuse before the zip, mirroring the campaign leg's own
|
|
/// did-you-mean refusal for the same retired form
|
|
/// (`RefFault::AxisNotInParamSpace`).
|
|
#[test]
|
|
fn exec_override_wrapped_form_path_refuses_not_panics() {
|
|
let (out, code) =
|
|
run_code(&["exec", "examples/r_sma.json", "--override", "sma_signal.fast.length=1"]);
|
|
assert_ne!(code, Some(101), "must not panic: {out}");
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
|
assert!(
|
|
out.contains("did you mean") && out.contains("fast.length"),
|
|
"the refusal must name the raw candidate: {out}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 4: `--override` on the campaign leg.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A bound param not covered by the document's own axes (`bias.scale`,
|
|
/// `examples/r_sma.json`'s `Bias` node — `seed_blueprint` only reopens
|
|
/// `fast.length`/`slow.length`, so `bias.scale` stays bound at 0.5 in the
|
|
/// stored blueprint): `--override bias.scale=0.75` reaches every realized
|
|
/// member's manifest raw (recorded in `params`, dropped from `defaults`) —
|
|
/// mirrors `exec_blueprint_override_reopens_and_records_the_value_in_params`
|
|
/// on the campaign leg.
|
|
#[test]
|
|
fn exec_campaign_override_reaches_the_member_manifests_raw() {
|
|
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("ovreach.process.json")),
|
|
ScratchPath::File(dir.join("ovreach.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-override-reach-seed");
|
|
let proc_id = register_process_doc(&dir, "ovreach.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "ovreach.campaign.json", &doc);
|
|
|
|
let (out, code) =
|
|
run_code_in(&dir, &["exec", "ovreach.campaign.json", "--override", "bias.scale=0.75"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
let members: Vec<serde_json::Value> = out
|
|
.lines()
|
|
.filter(|l| l.starts_with(r#"{"family_id":"#))
|
|
.map(|l| serde_json::from_str(l).expect("member line parses"))
|
|
.collect();
|
|
assert!(!members.is_empty(), "at least one member line: {out}");
|
|
for m in &members {
|
|
let params = m["report"]["manifest"]["params"].as_array().expect("params array");
|
|
assert!(
|
|
params
|
|
.iter()
|
|
.any(|e| e[0] == "bias.scale" && e[1] == serde_json::json!({"F64": 0.75})),
|
|
"the overridden value must land in every member's params: {m}"
|
|
);
|
|
let defaults = m["report"]["manifest"]["defaults"].as_array().expect("defaults array");
|
|
assert!(
|
|
!defaults.iter().any(|e| e[0] == "bias.scale"),
|
|
"an overridden bound param drops out of defaults: {m}"
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Cross-check (fieldtest B1, #319): the campaign leg's existing #272
|
|
/// containment already covers an out-of-domain OVERRIDE value, not just a
|
|
/// document-declared axis value. `bias.scale` is bound-only (not one of
|
|
/// `campaign_doc_json_for`'s own axes), so `--override bias.scale=-1.0`
|
|
/// injects a single-value axis of a value the `Bias` node's own constructor
|
|
/// rejects (`Bias::new` asserts `scale > 0.0`) — must stay the deliberate
|
|
/// per-cell exit 3, never 101. Pinned here since no existing test drove an
|
|
/// out-of-domain VALUE through `--override` specifically (only through a
|
|
/// document's own axis values, e.g. `aura_sweep_synthetic_member_panic_is_contained`).
|
|
#[test]
|
|
fn exec_campaign_override_out_of_domain_value_is_contained() {
|
|
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("ovpanic.process.json")),
|
|
ScratchPath::File(dir.join("ovpanic.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-override-panic-seed");
|
|
let proc_id = register_process_doc(&dir, "ovpanic.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "ovpanic.campaign.json", &doc);
|
|
|
|
let (out, code) =
|
|
run_code_in(&dir, &["exec", "ovpanic.campaign.json", "--override", "bias.scale=-1.0"]);
|
|
assert!(
|
|
out.lines().any(|l| l.contains("aura: warning: ") && l.contains("Bias scale must be > 0")),
|
|
"the member fault surfaces as one class-marked warning naming the fault: {out}"
|
|
);
|
|
assert!(!out.contains("panicked at"), "no raw panic report reaches the consumer: {out}");
|
|
assert_eq!(
|
|
code, Some(3),
|
|
"a contained member fault is the deliberate failed-cells exit 3, never 101: {out}"
|
|
);
|
|
}
|
|
|
|
/// An override path colliding with a document-declared axis (`fast.length`,
|
|
/// declared by `campaign_doc_json_for`) refuses: an override overrides a
|
|
/// bound value, never an axis.
|
|
#[test]
|
|
fn exec_campaign_override_colliding_with_a_document_axis_refuses() {
|
|
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("ovcollide.process.json")),
|
|
ScratchPath::File(dir.join("ovcollide.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-override-collide-seed");
|
|
let proc_id = register_process_doc(&dir, "ovcollide.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "ovcollide.campaign.json", &doc);
|
|
|
|
let (out, code) =
|
|
run_code_in(&dir, &["exec", "ovcollide.campaign.json", "--override", "fast.length=9"]);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("--override"), "stdout/stderr: {out}");
|
|
assert!(out.contains("declared axis"), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// A repeated `--override` path on the campaign leg refuses at the SAME
|
|
/// shared lexer (`parse_override_tokens`) both legs use — before the
|
|
/// per-strategy injection loop (`run_campaign_returning`) ever runs. Prior
|
|
/// to the fix, the first occurrence's own injection made the SECOND
|
|
/// occurrence look like a document-declared axis, misattributing the
|
|
/// refusal to "collides with a declared axis" (review Minor-1) even though
|
|
/// `bias.scale` is not declared by the document at all.
|
|
#[test]
|
|
fn exec_campaign_override_duplicate_path_refuses_at_the_shared_lexer() {
|
|
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("ovdup.process.json")),
|
|
ScratchPath::File(dir.join("ovdup.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-override-dup-seed");
|
|
let proc_id = register_process_doc(&dir, "ovdup.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "ovdup.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(
|
|
&dir,
|
|
&[
|
|
"exec",
|
|
"ovdup.campaign.json",
|
|
"--override",
|
|
"bias.scale=0.75",
|
|
"--override",
|
|
"bias.scale=0.9",
|
|
],
|
|
);
|
|
assert_eq!(code, Some(2), "stdout/stderr: {out}");
|
|
assert!(out.contains("bias.scale"), "the duplicated path must be named: {out}");
|
|
assert!(
|
|
!out.contains("collides with a declared axis"),
|
|
"must not misattribute to the axis-collision cause: {out}"
|
|
);
|
|
}
|
|
|
|
/// An override path naming no param of any strategy (a typo'd raw name)
|
|
/// falls out of `validate_campaign_refs`'s existing `AxisNotInParamSpace`
|
|
/// did-you-mean prose — pinned literally in `research_docs.rs` (:797-822);
|
|
/// here only the family of the message is asserted.
|
|
#[test]
|
|
fn exec_campaign_override_unknown_path_speaks_the_did_you_mean_family() {
|
|
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("ovtypo.process.json")),
|
|
ScratchPath::File(dir.join("ovtypo.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-override-typo-seed");
|
|
let proc_id = register_process_doc(&dir, "ovtypo.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "ovtypo.campaign.json", &doc);
|
|
|
|
let (out, code) =
|
|
run_code_in(&dir, &["exec", "ovtypo.campaign.json", "--override", "fast.lenght=9"]);
|
|
assert_ne!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("did you mean") || out.contains("not in the param space"),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 5: zero-trade note migration to the campaign walk-forward leg.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A `[std::grid, std::walk_forward]` process — the #256 fork B shape a
|
|
/// hand-authored (non-sugar) op-script uses — copied verbatim from
|
|
/// `research_docs.rs::GRID_THEN_WF_PROCESS_DOC`.
|
|
const GRID_THEN_WF_PROCESS_DOC: &str = r#"{
|
|
"format_version": 1,
|
|
"kind": "process",
|
|
"name": "grid-then-walkforward",
|
|
"pipeline": [
|
|
{ "block": "std::grid" },
|
|
{ "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000,
|
|
"step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" }
|
|
]
|
|
}"#;
|
|
|
|
/// `campaign_doc_json_for`'s shape with the axis VALUES parameterized (Task 5
|
|
/// needs the degenerate zero-trade point — both SMA lengths pinned equal —
|
|
/// which the fixed 2/4 and 8/16 defaults don't reach).
|
|
fn campaign_doc_json_walkforward(
|
|
instrument: &str,
|
|
bp_id: &str,
|
|
proc_id: &str,
|
|
window: (i64, i64),
|
|
fast_values: &str,
|
|
slow_values: &str,
|
|
) -> String {
|
|
format!(
|
|
r#"{{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "run-seam-wf",
|
|
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
|
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
|
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }},
|
|
"slow.length": {{ "kind": "I64", "values": [{slow_values}] }} }} }} ],
|
|
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
|
"seed": 7,
|
|
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
|
}}"#,
|
|
from = window.0,
|
|
to = window.1,
|
|
)
|
|
}
|
|
|
|
/// E2E (#313, campaign path): a walk-forward whose every window records zero
|
|
/// trades emits exactly one zero-trade note on `exec`'s campaign leg — the
|
|
/// same producer `cli_run.rs::aura_walkforward_all_zero_trade_windows_emit_one_note`
|
|
/// pins on the retiring synthetic-verb path, now reached through a
|
|
/// hand-authored `[std::grid, std::walk_forward]` campaign document. Equal
|
|
/// fast/slow lengths make the SMA difference constantly zero, so no window
|
|
/// trades — the same degenerate point, over real (SYMA) rather than
|
|
/// synthetic data (the property is data-source-agnostic).
|
|
#[test]
|
|
fn exec_campaign_walkforward_all_zero_trade_windows_emit_one_note() {
|
|
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("wfzero.process.json")),
|
|
ScratchPath::File(dir.join("wfzero.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-wf-zero-seed");
|
|
let proc_id = register_process_doc(&dir, "wfzero.process.json", GRID_THEN_WF_PROCESS_DOC);
|
|
let doc = campaign_doc_json_walkforward(
|
|
"SYMA",
|
|
&bp_id,
|
|
&proc_id,
|
|
(1709251200000, 1719791999999),
|
|
"8",
|
|
"8",
|
|
);
|
|
write_doc(&dir, "wfzero.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", "wfzero.campaign.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert_eq!(
|
|
out.matches("recorded zero trades").count(),
|
|
1,
|
|
"exactly one zero-trade note: {out}"
|
|
);
|
|
assert!(out.contains("aura: note: "), "the note carries the class marker: {out}");
|
|
}
|
|
|
|
/// E2E (#313 negative twin, campaign path): a walk-forward with at least one
|
|
/// traded window emits no zero-trade note — the fixture is the same document
|
|
/// shape as the zero-trade test above, but with the axes campaign_doc_json_for
|
|
/// already uses (fast∈{2,4}, slow∈{8,16} — known to produce trades over the
|
|
/// synthetic SYMA archive, per `campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap`).
|
|
#[test]
|
|
fn exec_campaign_walkforward_with_trades_emits_no_zero_trade_note() {
|
|
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("wftrades.process.json")),
|
|
ScratchPath::File(dir.join("wftrades.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-wf-trades-seed");
|
|
let proc_id = register_process_doc(&dir, "wftrades.process.json", GRID_THEN_WF_PROCESS_DOC);
|
|
let doc = campaign_doc_json_walkforward(
|
|
"SYMA",
|
|
&bp_id,
|
|
&proc_id,
|
|
(1709251200000, 1719791999999),
|
|
"2, 4",
|
|
"8, 16",
|
|
);
|
|
write_doc(&dir, "wftrades.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", "wftrades.campaign.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(!out.contains("recorded zero trades"), "a traded run emits no zero-trade note: {out}");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// #324 (re-scoped): zero-trade note for the non-walk-forward campaign leg —
|
|
// a cell whose families are ALL non-walk-forward (grid sweep here) has no
|
|
// per-window vocabulary, so it needs its own cell-level sibling note.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// E2E (#324, non-wf twin of the #313 wf note): a grid-sweep-only cell (no
|
|
/// `std::walk_forward` stage — `SWEEP_ONLY_PROCESS_DOC`) whose every
|
|
/// reported member trades zero times emits the cell-level zero-trade note.
|
|
/// Same degenerate equal-length point the wf fixture above uses (constant
|
|
/// zero SMA-difference, no window trades).
|
|
#[test]
|
|
fn exec_campaign_grid_cell_all_zero_trades_emits_cell_note() {
|
|
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("gridzero.process.json")),
|
|
ScratchPath::File(dir.join("gridzero.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-grid-zero-seed");
|
|
let proc_id = register_process_doc(&dir, "gridzero.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_walkforward(
|
|
"SYMA",
|
|
&bp_id,
|
|
&proc_id,
|
|
(1709251200000, 1719791999999),
|
|
"8",
|
|
"8",
|
|
);
|
|
write_doc(&dir, "gridzero.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", "gridzero.campaign.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("recorded zero trades") && out.contains("its metrics are vacuous"),
|
|
"the cell-level zero-trade note must fire: {out}"
|
|
);
|
|
assert!(out.contains("aura: note: "), "the note carries the class marker: {out}");
|
|
}
|
|
|
|
/// E2E (#324 negative twin): a grid-sweep-only cell with at least one traded
|
|
/// member emits no zero-trade note — the fixture is `campaign_doc_json_for`'s
|
|
/// known-trading axes (fast∈{2,4}, slow∈{8,16}), the same shape the override
|
|
/// tests above already run over `SWEEP_ONLY_PROCESS_DOC`.
|
|
#[test]
|
|
fn exec_campaign_grid_cell_with_trades_emits_no_zero_trade_note() {
|
|
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("gridtrades.process.json")),
|
|
ScratchPath::File(dir.join("gridtrades.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-grid-trades-seed");
|
|
let proc_id = register_process_doc(&dir, "gridtrades.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "gridtrades.campaign.json", &doc);
|
|
|
|
let (out, code) = run_code_in(&dir, &["exec", "gridtrades.campaign.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(!out.contains("recorded zero trades"), "a traded cell emits no zero-trade note: {out}");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 9: `aura-runner` family-builder retirement — the run-semantics half of
|
|
// the retired `blueprint_sweep_family` unit test, ported onto the surviving
|
|
// campaign path.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// [`campaign_doc_json_for`]'s shape with exactly ONE declared axis
|
|
/// (`fast.length`) at caller-chosen values — `slow.length`/`bias.scale` stay
|
|
/// at their bound defaults (4 and 0.5, `examples/r_sma.json`), isolating the
|
|
/// swept dimension for a clean member-by-member comparison.
|
|
fn campaign_doc_json_one_axis(
|
|
instrument: &str,
|
|
bp_id: &str,
|
|
proc_id: &str,
|
|
window: (i64, i64),
|
|
fast_values: &str,
|
|
) -> String {
|
|
format!(
|
|
r#"{{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "member-parity",
|
|
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
|
"strategies": [ {{ "ref": {{ "content_id": "{bp}" }},
|
|
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }} }} }} ],
|
|
"process": {{ "ref": {{ "content_id": "{proc}" }} }},
|
|
"seed": 7,
|
|
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
|
}}"#,
|
|
from = window.0,
|
|
to = window.1,
|
|
bp = bp_id,
|
|
proc = proc_id,
|
|
)
|
|
}
|
|
|
|
/// Property, ported from the retired `blueprint_sweep_family` unit test
|
|
/// `blueprint_sweep_member_equals_single_run_and_shares_topology_hash` (#319,
|
|
/// the family builders retired): a campaign family member computes the
|
|
/// IDENTICAL trading result a STANDALONE one-cell campaign at that same point
|
|
/// computes — the sweep terminal reuses the same member-run path regardless
|
|
/// of how many sibling points ride along — and every member of one family
|
|
/// carries the SAME `topology_hash` (the loaded blueprint's, not a per-point
|
|
/// value). `exec`'s blueprint leg has no `--real` (invariant 7: "a real-data
|
|
/// single run is a one-cell campaign document"), so the "single run"
|
|
/// comparator here IS the one-cell campaign the retirement itself designates
|
|
/// as that replacement, run over the identical instrument+window as the
|
|
/// multi-member family.
|
|
#[test]
|
|
fn exec_campaign_member_equals_a_standalone_single_cell_and_shares_topology_hash() {
|
|
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("parity.process.json")),
|
|
ScratchPath::File(dir.join("parity_family.campaign.json")),
|
|
ScratchPath::File(dir.join("parity_single.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-parity-seed");
|
|
let proc_id = register_process_doc(&dir, "parity.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let window = (1709251200000, 1719791999999);
|
|
|
|
// (A) a 2-member family: fast.length in {2, 8}; slow.length/bias.scale
|
|
// stay at their bound defaults.
|
|
let doc_family = campaign_doc_json_one_axis("SYMA", &bp_id, &proc_id, window, "2, 8");
|
|
write_doc(&dir, "parity_family.campaign.json", &doc_family);
|
|
let (out_family, code_family) = run_code_in(&dir, &["exec", "parity_family.campaign.json"]);
|
|
assert_eq!(code_family, Some(0), "family run: {out_family}");
|
|
let family_members: Vec<serde_json::Value> = out_family
|
|
.lines()
|
|
.filter(|l| l.starts_with(r#"{"family_id":"#))
|
|
.map(|l| serde_json::from_str(l).expect("member line parses"))
|
|
.collect();
|
|
assert_eq!(family_members.len(), 2, "a 2-value axis yields 2 members: {out_family}");
|
|
let topo_values: HashSet<&str> = family_members
|
|
.iter()
|
|
.map(|m| m["report"]["manifest"]["topology_hash"].as_str().expect("topology_hash present"))
|
|
.collect();
|
|
assert_eq!(
|
|
topo_values.len(),
|
|
1,
|
|
"every member of one family shares ONE topology_hash: {family_members:?}"
|
|
);
|
|
let member_8 = family_members
|
|
.iter()
|
|
.find(|m| {
|
|
m["report"]["manifest"]["params"]
|
|
.as_array()
|
|
.expect("params array")
|
|
.iter()
|
|
.any(|e| e[0] == "fast.length" && e[1] == serde_json::json!({"I64": 8}))
|
|
})
|
|
.expect("the fast.length=8 member is present");
|
|
|
|
// (B) a standalone one-cell campaign pinned at the SAME point.
|
|
let doc_single = campaign_doc_json_one_axis("SYMA", &bp_id, &proc_id, window, "8");
|
|
write_doc(&dir, "parity_single.campaign.json", &doc_single);
|
|
let (out_single, code_single) = run_code_in(&dir, &["exec", "parity_single.campaign.json"]);
|
|
assert_eq!(code_single, Some(0), "single-cell run: {out_single}");
|
|
let single_member: serde_json::Value = out_single
|
|
.lines()
|
|
.find(|l| l.starts_with(r#"{"family_id":"#))
|
|
.map(|l| serde_json::from_str(l).expect("member line parses"))
|
|
.expect("the one-cell campaign's single member line");
|
|
|
|
assert_eq!(
|
|
member_8["report"]["metrics"], single_member["report"]["metrics"],
|
|
"a family member computes the identical result a standalone single-cell run at \
|
|
the same point computes"
|
|
);
|
|
assert_eq!(
|
|
member_8["report"]["manifest"]["topology_hash"],
|
|
single_member["report"]["manifest"]["topology_hash"],
|
|
"the standalone single-cell run shares the same topology_hash"
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Task 8: `cli_run.rs` disposition — ports onto `exec` (retire+port half only;
|
|
// the port-to-campaign-document half is a separate follow-up dispatch).
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Property (#249, ported from `cli_run.rs::run_manifest_stamps_untouched_bound_defaults`):
|
|
/// a plain `exec` of a fully bound blueprint records the untouched bound param
|
|
/// values directly in the manifest — `params` stays `[]` (nothing varied) and
|
|
/// every bound value lands in `defaults`, RAW (#328) and ordered by
|
|
/// `bound_param_space()` (fast, slow, bias — node-declaration order).
|
|
#[test]
|
|
fn exec_manifest_stamps_untouched_bound_defaults() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(out.contains("\"params\":[]"), "params stays empty on a plain exec: {out}");
|
|
assert!(
|
|
out.contains(
|
|
r#""defaults":[["fast.length",{"I64":2}],["slow.length",{"I64":4}],["bias.scale",{"F64":0.5}]]"#
|
|
),
|
|
"manifest must carry the untouched bound defaults: {out}",
|
|
);
|
|
}
|
|
|
|
/// Property (ported from `cli_run.rs::run_manifest_commit_carries_real_git_head`):
|
|
/// the RunManifest is self-identifying — its `commit` carries the real git
|
|
/// HEAD that produced the run, never the `"unknown"` placeholder. Structural
|
|
/// (contains, not equals) since the working tree may be dirty.
|
|
#[test]
|
|
fn exec_manifest_commit_carries_real_git_head() {
|
|
let head = std::process::Command::new("git")
|
|
.args(["rev-parse", "HEAD"])
|
|
.output()
|
|
.expect("spawn git rev-parse HEAD");
|
|
assert!(head.status.success(), "git rev-parse HEAD failed");
|
|
let head_sha = String::from_utf8(head.stdout).expect("utf-8 sha");
|
|
let head_sha = head_sha.trim();
|
|
assert!(!head_sha.is_empty(), "empty HEAD sha");
|
|
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
let key = "\"commit\":\"";
|
|
let start = out.find(key).expect("manifest has a commit field") + key.len();
|
|
let rest = &out[start..];
|
|
let end = rest.find('"').expect("commit field is a closed string");
|
|
let commit = &rest[..end];
|
|
assert_ne!(commit, "unknown", "manifest.commit is still the placeholder: {out}");
|
|
assert!(
|
|
commit.contains(head_sha),
|
|
"manifest.commit {commit:?} should contain the current HEAD sha {head_sha:?}: {out}"
|
|
);
|
|
}
|
|
|
|
/// Property (#275, ported from `cli_run.rs::run_prints_json_and_exits_zero` — a
|
|
/// disposition-table gap: the plain synthetic single-run report shape was never
|
|
/// re-pinned onto `exec` although its property, a synthetic blueprint-leg
|
|
/// single run, belongs squarely to this leg): the canonical record-line shape
|
|
/// — nested manifest + metrics, stable keys, exactly one stdout line.
|
|
#[test]
|
|
fn exec_prints_json_and_exits_zero() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert_eq!(out.lines().count(), 1, "stdout was: {out:?}");
|
|
let line = out.trim_end();
|
|
assert!(line.starts_with("{\"manifest\":{\"commit\":\""), "got: {line}");
|
|
assert!(line.contains("\"broker\":\"sim-optimal+risk-executor(pip_size=0.0001)\""), "got: {line}");
|
|
assert!(line.contains("\"window\":[1,18]"), "got: {line}");
|
|
assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}");
|
|
assert!(line.contains("\"r\":{\"expectancy_r\":"), "got: {line}");
|
|
}
|
|
|
|
/// A blueprint referencing a node type outside the closed vocabulary fails
|
|
/// clean at load (invariant 9), in house-style prose (mirrors
|
|
/// `cli_run.rs::aura_run_rejects_an_unknown_node_blueprint`).
|
|
#[test]
|
|
fn exec_rejects_an_unknown_node_blueprint() {
|
|
let (out, code) = run_code(&["exec", "tests/fixtures/unknown_node.json"]);
|
|
assert_ne!(code, Some(0), "an unknown node type must fail the run: {out}");
|
|
assert!(out.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {out}");
|
|
assert!(!out.contains("UnknownNodeType"), "does not leak the Debug variant name: {out}");
|
|
}
|
|
|
|
/// Property (#231 task 4): `exec`'s blueprint leg has no `--real` at all
|
|
/// (invariant 7 — a real-data single run is a one-cell campaign document), so
|
|
/// a multi-column blueprint always hits the synthetic-only refusal — mirrors
|
|
/// `cli_run.rs::run_synthetic_refuses_a_multi_column_blueprint` over the
|
|
/// shipped `examples/r_channel.json`.
|
|
#[test]
|
|
fn exec_refuses_a_multi_column_blueprint() {
|
|
let (out, code) = run_code(&["exec", "examples/r_channel.json"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(out.contains("consumes columns beyond close"), "names the shape: {out}");
|
|
assert!(out.contains("--real"), "names the remedy: {out}");
|
|
}
|
|
|
|
/// A CLOSED high/low blueprint (zero free knobs — copied verbatim from
|
|
/// `cli_run.rs::HL_RANGE_CLOSED_BLUEPRINT`) drives the exact-prose twin of the
|
|
/// refusal above, data-free (the guard fires directly after binding
|
|
/// resolution, before any archive access) — mirrors
|
|
/// `cli_run.rs::run_synthetic_refuses_a_multi_column_blueprint_before_data_access`.
|
|
const HL_RANGE_CLOSED_BLUEPRINT: &str = r#"{
|
|
"format_version": 1,
|
|
"blueprint": {
|
|
"name": "hl_range",
|
|
"nodes": [ {"primitive":{"type":"Sub"}} ],
|
|
"edges": [],
|
|
"input_roles": [
|
|
{"name":"high","targets":[{"node":0,"slot":0}],"source":"F64"},
|
|
{"name":"low","targets":[{"node":0,"slot":1}],"source":"F64"}
|
|
],
|
|
"output": [{"node":0,"field":0,"name":"bias"}]
|
|
}
|
|
}"#;
|
|
|
|
#[test]
|
|
fn exec_refuses_a_multi_column_blueprint_before_data_access() {
|
|
let cwd = temp_cwd("exec_multicolumn_refusal");
|
|
let bp_path = cwd.join("hl_range.json");
|
|
std::fs::write(&bp_path, HL_RANGE_CLOSED_BLUEPRINT).expect("write fixture");
|
|
let (out, code) = run_code_in(&cwd, &["exec", bp_path.to_str().expect("utf-8 path")]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert_eq!(
|
|
out.trim_end(),
|
|
"aura: strategy \"hl_range\" consumes columns beyond close (high, low) — synthetic \
|
|
data generates a close series only; run with --real <SYMBOL>"
|
|
);
|
|
}
|
|
|
|
/// Property (#210, dissolution — content-addressed idempotency), ported onto
|
|
/// `exec`'s campaign FILE leg (the register-then-run sugar,
|
|
/// `campaign_run.rs::run_campaign`'s target resolution): re-`exec`ing the
|
|
/// IDENTICAL campaign document file does not litter the store with a second
|
|
/// registered campaign document — `put_campaign` writes under a content-id
|
|
/// filename, so two runs of the same file collapse onto one registered
|
|
/// document while each invocation still records its own `campaign_runs.jsonl`
|
|
/// line (a run is an event, a document is content) — mirrors
|
|
/// `cli_run.rs::generalize_repeated_identical_invocation_does_not_litter_the_store`,
|
|
/// whose translator generated the document; here the document is authored and
|
|
/// `exec` performs only the file leg's own register-then-run idempotency.
|
|
#[test]
|
|
fn exec_campaign_file_repeated_identical_invocation_does_not_litter_the_store() {
|
|
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("idem.process.json")),
|
|
ScratchPath::File(dir.join("idem.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-idempotence-seed");
|
|
let proc_id = register_process_doc(&dir, "idem.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let doc = campaign_doc_json_for("SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999));
|
|
write_doc(&dir, "idem.campaign.json", &doc);
|
|
|
|
let (first_out, first_code) = run_code_in(&dir, &["exec", "idem.campaign.json"]);
|
|
assert_eq!(first_code, Some(0), "first run: {first_out}");
|
|
let (second_out, second_code) = run_code_in(&dir, &["exec", "idem.campaign.json"]);
|
|
assert_eq!(second_code, Some(0), "second run: {second_out}");
|
|
|
|
let campaigns_count = std::fs::read_dir(dir.join("runs").join("campaigns"))
|
|
.map(|d| d.count())
|
|
.unwrap_or(0);
|
|
assert_eq!(campaigns_count, 1, "two identical file-target invocations register one campaign document");
|
|
let runs_log = std::fs::read_to_string(dir.join("runs").join("campaign_runs.jsonl"))
|
|
.expect("campaign_runs.jsonl exists");
|
|
assert_eq!(runs_log.lines().count(), 2, "each invocation still records its own campaign run");
|
|
}
|
|
|
|
/// `campaign_doc_json_for`'s shape with the axis grid parameterized — needed
|
|
/// so the distinctness twin below can vary campaign content without touching
|
|
/// the window (keeping the known-good data-availability window fixed).
|
|
fn campaign_doc_json_for_axis_grid(
|
|
instrument: &str,
|
|
bp_id: &str,
|
|
proc_id: &str,
|
|
window: (i64, i64),
|
|
fast_values: &str,
|
|
slow_values: &str,
|
|
) -> String {
|
|
format!(
|
|
r#"{{
|
|
"format_version": 1,
|
|
"kind": "campaign",
|
|
"name": "run-seam-grid",
|
|
"data": {{ "instruments": ["{instrument}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }},
|
|
"strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }},
|
|
"axes": {{ "fast.length": {{ "kind": "I64", "values": [{fast_values}] }},
|
|
"slow.length": {{ "kind": "I64", "values": [{slow_values}] }} }} }} ],
|
|
"process": {{ "ref": {{ "content_id": "{proc_id}" }} }},
|
|
"seed": 7,
|
|
"presentation": {{ "persist_taps": [], "emit": ["family_table"] }}
|
|
}}"#,
|
|
from = window.0,
|
|
to = window.1,
|
|
)
|
|
}
|
|
|
|
/// Property (#210, dissolution — content-addressed distinctness), ported onto
|
|
/// `exec`'s campaign FILE leg: two DIFFERENT campaign documents (differing
|
|
/// axis-grid content, same window) persist as TWO distinct registered
|
|
/// campaign documents — the twin of the idempotency test above, ruling out a
|
|
/// store that always overwrites one fixed filename regardless of content —
|
|
/// mirrors `cli_run.rs::generalize_distinct_invocations_persist_distinct_campaign_documents`.
|
|
#[test]
|
|
fn exec_campaign_file_distinct_invocations_persist_distinct_campaign_documents() {
|
|
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("distinct.process.json")),
|
|
ScratchPath::File(dir.join("distinct_a.campaign.json")),
|
|
ScratchPath::File(dir.join("distinct_b.campaign.json")),
|
|
]);
|
|
let bp_id = seed_blueprint(&dir, "exec-distinct-seed");
|
|
let proc_id = register_process_doc(&dir, "distinct.process.json", SWEEP_ONLY_PROCESS_DOC);
|
|
let window = (1709251200000, 1719791999999);
|
|
let doc_a = campaign_doc_json_for_axis_grid("SYMA", &bp_id, &proc_id, window, "2", "8");
|
|
write_doc(&dir, "distinct_a.campaign.json", &doc_a);
|
|
let (out_a, code_a) = run_code_in(&dir, &["exec", "distinct_a.campaign.json"]);
|
|
assert_eq!(code_a, Some(0), "first document run: {out_a}");
|
|
|
|
let doc_b = campaign_doc_json_for_axis_grid("SYMA", &bp_id, &proc_id, window, "2, 4", "8, 16");
|
|
write_doc(&dir, "distinct_b.campaign.json", &doc_b);
|
|
let (out_b, code_b) = run_code_in(&dir, &["exec", "distinct_b.campaign.json"]);
|
|
assert_eq!(code_b, Some(0), "second document run: {out_b}");
|
|
|
|
let campaigns_count = std::fs::read_dir(dir.join("runs").join("campaigns"))
|
|
.map(|d| d.count())
|
|
.unwrap_or(0);
|
|
assert_eq!(campaigns_count, 2, "two distinct documents persist as two distinct campaign documents");
|
|
}
|
|
|
|
/// Property (C22 first-contact ergonomics, #20), ported onto the surviving
|
|
/// verb set (mirrors `cli_run.rs::help_flag_prints_usage_to_stdout_and_exits_zero`,
|
|
/// whose "the usage names the only subcommand a newcomer can run" assertion
|
|
/// named the now-retired `run` — the newcomer's reflex first command is
|
|
/// `exec` today): `--help`/`-h` prints to stdout and exits 0; an unknown
|
|
/// subcommand keeps the error path (exit 2).
|
|
#[test]
|
|
fn exec_help_flag_prints_usage_to_stdout_and_exits_zero() {
|
|
for flag in ["--help", "-h"] {
|
|
let (out, code) = run_code(&[flag]);
|
|
assert_eq!(code, Some(0), "`aura {flag}` exit: {out}");
|
|
assert!(!out.trim().is_empty(), "`aura {flag}` should print help to stdout: {out}");
|
|
assert!(out.contains("exec"), "`aura {flag}` help should mention the `exec` subcommand: {out}");
|
|
}
|
|
let (out, code) = run_code(&["frobnicate"]);
|
|
assert_eq!(code, Some(2), "unknown subcommand must stay exit 2: {out}");
|
|
}
|
|
|
|
/// Property (post-clap migration, #175), ported onto the surviving verb set
|
|
/// (mirrors `cli_run.rs::per_subcommand_help_is_scoped_stdout_exit_zero`,
|
|
/// which enumerated the now-retired quintet): every surviving subcommand's
|
|
/// `--help`/`-h` prints its own scoped Options section to stdout, exit 0,
|
|
/// nothing on stderr.
|
|
#[test]
|
|
fn exec_per_subcommand_help_is_scoped_stdout_exit_zero() {
|
|
for sub in ["exec", "chart", "graph", "runs", "reproduce", "new", "nodes", "process", "campaign", "data", "measure"] {
|
|
for help in ["--help", "-h"] {
|
|
let (out, code) = run_code(&[sub, help]);
|
|
assert_eq!(code, Some(0), "{sub} {help} exit: {out}");
|
|
assert!(!out.trim().is_empty(), "{sub} {help} stdout empty");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Property (mirrors `cli_run.rs::subcommand_help_is_scoped_not_uniform`): a
|
|
/// subcommand's `--help` is NOT byte-identical to another's — each documents
|
|
/// its own options, never one shared global blob.
|
|
#[test]
|
|
fn exec_subcommand_help_is_scoped_not_uniform() {
|
|
let (exec_help, exec_code) = run_code(&["exec", "--help"]);
|
|
let (graph_help, graph_code) = run_code(&["graph", "--help"]);
|
|
assert_eq!(exec_code, Some(0), "exec --help exit");
|
|
assert_eq!(graph_code, Some(0), "graph --help exit");
|
|
assert!(exec_help.contains("--override"), "exec help names its flags: {exec_help:?}");
|
|
assert_ne!(exec_help, graph_help, "per-subcommand help must be scoped, not uniform");
|
|
}
|
|
|
|
/// The GNU `--flag=value` equals form is accepted (mirrors
|
|
/// `cli_run.rs::gnu_equals_form_is_accepted`; the surviving vehicle flag is
|
|
/// `--override`, since `--seed` retired with `run`'s built-in-harness
|
|
/// grammar, invariant 7).
|
|
#[test]
|
|
fn exec_gnu_equals_form_is_accepted() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override=fast.length=8"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// The `--` end-of-options terminator is recognized (mirrors
|
|
/// `cli_run.rs::double_dash_terminator_is_recognized`).
|
|
#[test]
|
|
fn exec_double_dash_terminator_is_recognized() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--override", "fast.length=8", "--"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// GNU long-option abbreviation (mirrors
|
|
/// `cli_run.rs::long_option_abbreviation_is_accepted`): an unambiguous prefix
|
|
/// of a long flag is accepted (`--over` -> `--override`), via clap's root
|
|
/// `infer_long_args`.
|
|
#[test]
|
|
fn exec_long_option_abbreviation_is_accepted() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json", "--over", "fast.length=8"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
}
|
|
|
|
/// exec's target classification, ported/merged from two retired dual-grammar
|
|
/// guards (`cli_run.rs::dual_grammar_stray_positional_is_a_usage_error_not_swallowed`
|
|
/// and the retired `run_with_trailing_token_is_strict_and_exits_two`'s negative
|
|
/// half): `target` is exec's one REQUIRED positional, so clap always parses
|
|
/// it — there is no more optional-positional-plus-hidden-default to guard
|
|
/// against. A bare, non-`.json` stray token is instead classified at RUNTIME
|
|
/// by the campaign leg's own target resolution, which refuses naming both
|
|
/// readings it tried, never silently defaulting to anything.
|
|
#[test]
|
|
fn exec_stray_target_that_is_neither_a_file_nor_a_content_id_refuses() {
|
|
let (dir, _fixture) = fresh_project();
|
|
let (out, code) = run_code_in(&dir, &["exec", "bogus"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("'bogus' is neither a readable .json file nor a 64-hex content id"),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
}
|
|
|
|
/// exec's discriminator keys on `is_file()`, not the `.json` suffix (mirrors
|
|
/// `cli_run.rs::dual_grammar_discriminator_requires_an_existing_file_not_just_json_suffix`):
|
|
/// a `.json`-suffixed name that does not exist on disk is NOT read as a
|
|
/// blueprint (which would fail with a file-read error) — it falls through to
|
|
/// the same neither-file-nor-content-id classification the bare stray token
|
|
/// above gets. The positive `.json`-file path stays covered by
|
|
/// `exec_blueprint_file_emits_the_single_run_record_line`.
|
|
#[test]
|
|
fn exec_json_suffixed_nonexistent_target_is_not_read_as_a_blueprint() {
|
|
let (dir, _fixture) = fresh_project();
|
|
let (out, code) = run_code_in(&dir, &["exec", "definitely-not-a-real-file.json"]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains("'definitely-not-a-real-file.json' is neither a readable .json file nor a 64-hex content id"),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
}
|
|
|
|
/// A target that IS a well-formed 64-hex content id, but names no
|
|
/// registered campaign, refuses at runtime (exit 1, C14 — #342 item 5): no
|
|
/// file is ever read for a bare id target, so there is no argument
|
|
/// *content* to have gotten wrong (contrast the wrong-kind / op-script /
|
|
/// not-valid-JSON FILE-content faults above, all exit 2) — only recorded
|
|
/// state (a registered campaign under this id) that turns out to be
|
|
/// absent, the same runtime class as the missing-file / stray-token cases
|
|
/// immediately above.
|
|
#[test]
|
|
fn exec_well_formed_but_unregistered_content_id_refuses_at_runtime() {
|
|
let (dir, _fixture) = fresh_project();
|
|
let unregistered = "a".repeat(64);
|
|
let (out, code) = run_code_in(&dir, &["exec", &unregistered]);
|
|
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
|
assert!(
|
|
out.contains(&format!("no campaign {unregistered} in the project store")),
|
|
"stdout/stderr: {out}"
|
|
);
|
|
}
|
|
|
|
/// The usage/runtime exit-code partition (#175 iteration 2, deviation #8),
|
|
/// ported onto exec's own grammar (mirrors
|
|
/// `cli_run.rs::exit_codes_partition_usage_two_from_runtime_one`, whose
|
|
/// runtime half rode `run --real NONEXISTENT` — a flag exec's blueprint leg
|
|
/// no longer has, invariant 7): an unknown flag is a command-line error
|
|
/// (exit 2); `--override` naming no param of the blueprint is exec's own
|
|
/// well-formed-but-unresolvable runtime failure (exit 1).
|
|
#[test]
|
|
fn exec_exit_codes_partition_usage_two_from_runtime_one() {
|
|
let (usage_out, usage_code) = run_code(&["exec", "examples/r_sma.json", "--bogus"]);
|
|
assert_eq!(usage_code, Some(2), "unknown flag is a usage error -> 2: {usage_out}");
|
|
let (runtime_out, runtime_code) =
|
|
run_code(&["exec", "examples/r_sma.json", "--override", "nope.knob=1"]);
|
|
assert_eq!(
|
|
runtime_code, Some(1),
|
|
"a well-formed command with an unresolvable override is a runtime failure -> 1: {runtime_out}"
|
|
);
|
|
}
|
|
|
|
/// `exec` on the shipped closed r-sma example reproduces the built-in r-sma
|
|
/// grade (mirrors `cli_run.rs::shipped_r_sma_example_reproduces_the_builtin_grade`).
|
|
#[test]
|
|
fn exec_shipped_r_sma_example_reproduces_the_builtin_grade() {
|
|
let (out, code) = run_code(&["exec", "examples/r_sma.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(out.contains("\"expectancy_r\":1.2710005136982836"), "{out}");
|
|
assert!(out.contains("\"sqn\":3.141496526818299"), "{out}");
|
|
assert!(out.contains("\"total_pips\":0.34185000000002036"), "{out}");
|
|
assert!(out.contains("\"n_trades\":3"), "{out}");
|
|
}
|
|
|
|
/// `exec` on the shipped closed r-meanrev example runs end to end through the
|
|
/// real, separately-linked `aura` binary (mirrors
|
|
/// `cli_run.rs::shipped_r_meanrev_example_runs_end_to_end_via_aura_run`). The
|
|
/// synthetic stream never crosses the band at this window/k, so the grade is
|
|
/// genuinely all-zero (no trade) — still a meaningful pin: a regression that
|
|
/// broke the `Scale` band's CLI wiring would surface as a non-zero exit or a
|
|
/// load error here, not as a metrics drift.
|
|
#[test]
|
|
fn exec_shipped_r_meanrev_example_runs_end_to_end() {
|
|
let (out, code) = run_code(&["exec", "examples/r_meanrev.json"]);
|
|
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
|
assert!(out.contains("\"topology_hash\":\""), "manifest carries the topology hash: {out}");
|
|
assert!(out.contains("\"expectancy_r\":0.0"), "{out}");
|
|
assert!(out.contains("\"n_trades\":0"), "{out}");
|
|
assert!(out.contains("\"total_pips\":0.0"), "{out}");
|
|
}
|