feat(aura-cli): exec — the one executor verb, campaign legs + blueprint single run
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
This commit is contained in:
@@ -1151,6 +1151,9 @@ struct Cli {
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Command {
|
||||
/// Execute a document: a campaign (file or content id) or a signal
|
||||
/// blueprint (single run).
|
||||
Exec(ExecCmd),
|
||||
/// Run a single backtest (built-in harness or a loaded blueprint).
|
||||
///
|
||||
/// Part of the research sugar surface; the canonical, document-first form
|
||||
@@ -1414,6 +1417,30 @@ struct ReproduceCmd {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct ExecCmd {
|
||||
/// A campaign document (.json file or 64-hex content id) or a serialized
|
||||
/// signal blueprint (.json file, single run).
|
||||
target: String,
|
||||
/// Bound on distinct instruments resident in parallel (campaign targets;
|
||||
/// 1 reproduces the sequential loop's footprint).
|
||||
#[arg(
|
||||
long,
|
||||
default_value_t = aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
value_parser = crate::research_docs::parse_parallel_instruments,
|
||||
)]
|
||||
parallel_instruments: std::num::NonZeroUsize,
|
||||
/// Override one bound param for this execution (repeatable,
|
||||
/// NODE.PARAM=VALUE — the #246 reopen mechanism; the value is recorded
|
||||
/// raw in the run manifest). Both target classes.
|
||||
#[arg(long = "override", value_name = "NODE.PARAM=VALUE")]
|
||||
r#override: Vec<String>,
|
||||
/// Subscribe a declared tap to a fold for this run (repeatable, TAP=FOLD;
|
||||
/// blueprint targets). Fold roster: `aura graph introspect --folds`.
|
||||
#[arg(long = "tap", value_name = "TAP=FOLD")]
|
||||
tap: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
struct RunCmd {
|
||||
/// A serialized signal blueprint (.json). An existing file selects the
|
||||
@@ -1563,6 +1590,21 @@ fn is_blueprint_file(arg: &Option<String>) -> Option<&str> {
|
||||
.filter(|a| a.ends_with(".json") && std::path::Path::new(a).is_file())
|
||||
}
|
||||
|
||||
/// `exec`'s FILE-target leg discriminator, layered on top of
|
||||
/// `is_blueprint_file`: a research-document envelope (process/campaign)
|
||||
/// carries a required top-level `"kind"` field; a blueprint envelope
|
||||
/// (`{"format_version":N,"blueprint":{…}}`) carries none. `is_blueprint_file`
|
||||
/// alone only tells "an existing `.json` file" from a bare harness-kind/id
|
||||
/// token — it cannot tell a campaign document ending in `.json` from a
|
||||
/// blueprint file, both being ordinary readable `.json` files — so `exec`'s
|
||||
/// routing peeks this one cheap key before choosing a leg.
|
||||
fn is_campaign_document_file(path: &str) -> bool {
|
||||
std::fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok())
|
||||
.is_some_and(|v| v.get("kind").is_some())
|
||||
}
|
||||
|
||||
/// Resolve a `[blueprint].json`-branch `--real`/`--from`/`--to` into a `RunData`,
|
||||
/// mirroring the old `parse_blueprint_run_args` window guard (`--from`/`--to`
|
||||
/// require `--real`; empty symbol rejected). Refuses in place (stderr + exit 2).
|
||||
@@ -1908,6 +1950,93 @@ fn tap_plan_from_args(args: &[String]) -> Result<TapPlan, String> {
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
/// The one executor verb (#319): a campaign document (file or content id)
|
||||
/// or a signal blueprint (single run). Flag discipline is per-leg.
|
||||
fn dispatch_exec(a: ExecCmd, env: &aura_runner::project::Env) {
|
||||
let target = Some(a.target.clone());
|
||||
let blueprint_path = is_blueprint_file(&target).filter(|p| !is_campaign_document_file(p));
|
||||
match blueprint_path {
|
||||
Some(path) => {
|
||||
let _ = &a.r#override; // threaded in Task 3, not this task
|
||||
exec_blueprint_leg(path, &a.tap, env)
|
||||
}
|
||||
None => {
|
||||
if !a.tap.is_empty() {
|
||||
eprintln!(
|
||||
"aura: exec: --tap applies only to a blueprint target; a campaign \
|
||||
document declares its persisted taps itself"
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
exit_on_campaign_result(crate::campaign_run::run_campaign(&a.target, env, a.parallel_instruments));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `exec` blueprint leg (#319): a synthetic single run of a fully-bound
|
||||
/// blueprint envelope, composed 1:1 from `dispatch_run`'s `.json`-branch
|
||||
/// building blocks (`main.rs:1970-2029` at the time of writing) — same
|
||||
/// intake gate, same closed-blueprint guard, same tap plan, same
|
||||
/// `run_signal_r` call and record-line serializer, so the record line stays
|
||||
/// byte-identical to `run`'s. `--override` is NOT handled here (Task 3);
|
||||
/// `run`'s `--params`/`--seed`/`--real`/`--from`/`--to` do not exist on
|
||||
/// `exec` at all (invariant 7 — a real-data or seeded single run is a
|
||||
/// one-cell campaign document).
|
||||
fn exec_blueprint_leg(path: &str, tap: &[String], env: &aura_runner::project::Env) {
|
||||
let doc = std::fs::read_to_string(path).unwrap_or_else(|e| {
|
||||
eprintln!("aura: {path}: {e}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
let signal = blueprint_from_json(&doc, &|t| env.resolve(t)).unwrap_or_else(|e| {
|
||||
let mut msg = graph_construct::blueprint_load_prose(&e);
|
||||
if let Some(hint) = graph_construct::unresolved_namespace_hint(&e, env) {
|
||||
msg.push_str(" — ");
|
||||
msg.push_str(&hint);
|
||||
}
|
||||
eprintln!("aura: {path}: {msg}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
// Same root-name gate as `dispatch_run`'s FILE intake (#331 delta
|
||||
// re-review): `signal.name()` feeds `bind_tap_plan` -> `TraceStore::begin_run`
|
||||
// unsanitized (`trace_store.rs`).
|
||||
if let Err(msg) = graph_construct::gate_authored_root_name(signal.name()) {
|
||||
eprintln!("aura: {path}: {msg}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
let has_bias = signal.output().iter().any(|o| o.name == "bias");
|
||||
let tap_plan = match tap_plan_from_args(tap) {
|
||||
Ok(p) => p,
|
||||
Err(m) => {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
};
|
||||
if has_bias {
|
||||
// Refuse an open (free-knob) blueprint at the dispatch boundary,
|
||||
// mirroring `dispatch_run`'s own closed-blueprint guard (#176): exec
|
||||
// bootstraps over the EMPTY point, so a free knob would panic in
|
||||
// `compile_with_params` — reject it clean instead.
|
||||
let free = blueprint_axis_probe(&doc, env).param_space();
|
||||
if !free.is_empty() {
|
||||
eprintln!(
|
||||
"aura: run requires a closed blueprint (no free parameters); {} free knob(s) — \
|
||||
bind them or use `aura sweep --axis`",
|
||||
free.len()
|
||||
);
|
||||
std::process::exit(2);
|
||||
}
|
||||
let params: Vec<Scalar> = Vec::new();
|
||||
let report = run_signal_r(signal, ¶ms, RunData::Synthetic, 0, env, tap_plan);
|
||||
println!("{}", report.to_json());
|
||||
} else {
|
||||
eprintln!(
|
||||
"aura: `aura exec` needs a `bias` output (a strategy); this blueprint exposes \
|
||||
none — the measurement leg is not yet carried by exec"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// `aura run`: the loaded-blueprint branch (an existing `.json` first-positional) or
|
||||
/// the built-in harness-kind dispatch.
|
||||
fn dispatch_run(a: RunCmd, env: &aura_runner::project::Env) {
|
||||
@@ -2826,6 +2955,7 @@ fn main() {
|
||||
}
|
||||
};
|
||||
match cli.command {
|
||||
Command::Exec(a) => dispatch_exec(a, &env),
|
||||
Command::Run(a) => dispatch_run(a, &env),
|
||||
Command::Chart(a) => dispatch_chart(a, &env),
|
||||
Command::Graph(a) => dispatch_graph(a, &env),
|
||||
|
||||
@@ -65,7 +65,7 @@ impl DocIntrospectCmd {
|
||||
/// Parse `--parallel-instruments` in domain terms: clap's built-in
|
||||
/// `NonZeroUsize` parser would leak the Rust type name ("number would be
|
||||
/// zero for non-zero type") at exactly the moment a user needs guidance.
|
||||
fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
|
||||
pub(crate) fn parse_parallel_instruments(s: &str) -> Result<std::num::NonZeroUsize, String> {
|
||||
s.parse::<usize>().ok().and_then(std::num::NonZeroUsize::new).ok_or_else(|| {
|
||||
"must be a whole number of at least 1 — it bounds how many distinct \
|
||||
instruments are resident in parallel"
|
||||
@@ -411,19 +411,6 @@ pub enum CampaignSub {
|
||||
Introspect(DocIntrospectCmd),
|
||||
/// Register a valid campaign document into the store under the runs root.
|
||||
Register { file: PathBuf },
|
||||
/// Execute a stored campaign into a realized run-set (a .json file is
|
||||
/// register-then-run sugar; the canonical address is the content id).
|
||||
Run {
|
||||
target: String,
|
||||
/// Bound on distinct instruments resident in parallel (the RAM
|
||||
/// lever; 1 reproduces the sequential loop's footprint).
|
||||
#[arg(
|
||||
long,
|
||||
default_value_t = aura_campaign::DEFAULT_PARALLEL_INSTRUMENTS,
|
||||
value_parser = parse_parallel_instruments,
|
||||
)]
|
||||
parallel_instruments: std::num::NonZeroUsize,
|
||||
},
|
||||
/// List stored campaign realizations, or dump one campaign's records
|
||||
/// (the bare store lines, not the run-emit wrapper).
|
||||
Runs { campaign: Option<String>, run: Option<usize> },
|
||||
@@ -542,18 +529,7 @@ fn campaign_runs(campaign: Option<&str>, run: Option<usize>, env: &Env) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// #272: `Run` threads a failed-cell count (exit 3 on completed-with-failures,
|
||||
/// via `exit_on_campaign_result`), so it is pulled out of the unified
|
||||
/// `Result<(), String>` match the other four subcommands still share.
|
||||
pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
if let CampaignSub::Run { target, parallel_instruments } = &cmd.sub {
|
||||
crate::exit_on_campaign_result(crate::campaign_run::run_campaign(
|
||||
target,
|
||||
env,
|
||||
*parallel_instruments,
|
||||
));
|
||||
return;
|
||||
}
|
||||
let result = match &cmd.sub {
|
||||
CampaignSub::Validate { file } => validate_campaign_file(file, env),
|
||||
CampaignSub::Introspect(i) => {
|
||||
@@ -563,7 +539,6 @@ pub fn campaign_cmd(cmd: CampaignCmd, env: &Env) {
|
||||
CampaignSub::Register { file } => register_campaign(file, env),
|
||||
CampaignSub::Runs { campaign, run } => campaign_runs(campaign.as_deref(), *run, env),
|
||||
CampaignSub::Show { id } => show_campaign(id, env),
|
||||
CampaignSub::Run { .. } => unreachable!("handled above"),
|
||||
};
|
||||
if let Err(m) = result {
|
||||
eprintln!("aura: {m}");
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
//! 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}");
|
||||
}
|
||||
@@ -324,7 +324,7 @@ fn project_node_open_param_runs_one_campaign_cell() {
|
||||
);
|
||||
std::fs::write(dir.join("knob.campaign.json"), &campaign).expect("write campaign doc");
|
||||
|
||||
let run = aura_in(dir, &["campaign", "run", "knob.campaign.json"]);
|
||||
let run = aura_in(dir, &["exec", "knob.campaign.json"]);
|
||||
let out = format!(
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&run.stdout),
|
||||
|
||||
@@ -1494,7 +1494,7 @@ const WF_PROCESS_DOC: &str = r#"{
|
||||
fn campaign_run_outside_project_refuses() {
|
||||
let dir = temp_cwd("campaign-run-outside-project");
|
||||
write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "c.campaign.json"]);
|
||||
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!(
|
||||
@@ -1508,7 +1508,7 @@ fn campaign_run_outside_project_refuses() {
|
||||
#[test]
|
||||
fn campaign_run_bogus_target_refuses() {
|
||||
let (dir, _fixture) = fresh_project();
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "no-such-target"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "no-such-target"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("'no-such-target' is neither a readable .json file nor a 64-hex content id"),
|
||||
@@ -1521,7 +1521,7 @@ fn campaign_run_bogus_target_refuses() {
|
||||
fn campaign_run_unknown_id_refuses() {
|
||||
let (dir, _fixture) = fresh_project();
|
||||
let id = "0".repeat(64);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", &id]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains(&format!("no campaign {id} in the project store")),
|
||||
@@ -1639,7 +1639,7 @@ fn campaign_run_refuses_mc_before_walk_forward() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-mcwf-seed");
|
||||
let proc_id = register_process_doc(&dir, "mcwf.process.json", MC_BEFORE_WF_PROCESS_DOC);
|
||||
write_doc(&dir, "mcwf.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mcwf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mcwf.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -1689,7 +1689,7 @@ fn campaign_run_refuses_a_non_terminal_selection_free_sweep() {
|
||||
let proc_id =
|
||||
register_process_doc(&dir, "selfree.process.json", SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC);
|
||||
write_doc(&dir, "selfree.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "selfree.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "selfree.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains(
|
||||
@@ -1803,7 +1803,7 @@ fn campaign_run_refuses_a_grid_stage_not_immediately_before_walk_forward() {
|
||||
"gridgate.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridgate.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gridgate.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -1856,7 +1856,7 @@ fn campaign_run_synthetic_e2e_grid_then_wf_persists_no_sweep_family() {
|
||||
"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gridwf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gridwf.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
|
||||
let line = out
|
||||
@@ -1918,7 +1918,7 @@ fn campaign_run_synthetic_e2e_cell_order_is_document_order_under_parallel_instru
|
||||
for k in ["1", "2"] {
|
||||
let (out, code) = run_code_in(
|
||||
&dir,
|
||||
&["campaign", "run", "cellorder.campaign.json", "--parallel-instruments", k],
|
||||
&["exec", "cellorder.campaign.json", "--parallel-instruments", k],
|
||||
);
|
||||
assert_eq!(code, Some(0), "K={k}: campaign run failed: {out}");
|
||||
let line = out
|
||||
@@ -1979,7 +1979,7 @@ fn campaign_run_synthetic_e2e_parallel_instruments_contains_a_real_per_cell_faul
|
||||
|
||||
let (out, code) = run_code_in(
|
||||
&dir,
|
||||
&["campaign", "run", "parfault.campaign.json", "--parallel-instruments", "2"],
|
||||
&["exec", "parfault.campaign.json", "--parallel-instruments", "2"],
|
||||
);
|
||||
assert_eq!(code, Some(3), "one contained fault, one success: {out}");
|
||||
let line = out
|
||||
@@ -2034,7 +2034,7 @@ fn campaign_run_synthetic_e2e_gate_emptied_cell_carries_the_note_marker() {
|
||||
write_doc(&dir, "gateempty.campaign.json", &doc);
|
||||
|
||||
let (out, code) =
|
||||
run_code_in(&dir, &["campaign", "run", "gateempty.campaign.json"]);
|
||||
run_code_in(&dir, &["exec", "gateempty.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "a zero-survivor cell is a valid result, not a fault: {out}");
|
||||
assert!(
|
||||
out.contains("aura: note: cell "),
|
||||
@@ -2066,7 +2066,7 @@ fn campaign_run_refuses_single_instrument_generalize() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-gen1-seed");
|
||||
let proc_id = register_process_doc(&dir, "gen1.process.json", GENERALIZE_PROCESS_DOC);
|
||||
write_doc(&dir, "gen1.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gen1.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gen1.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("std::generalize needs at least 2"),
|
||||
@@ -2127,7 +2127,7 @@ fn campaign_run_refuses_generalize_non_r_metric() {
|
||||
}}"#
|
||||
);
|
||||
write_doc(&dir, "genpip.campaign.json", &campaign_doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "genpip.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "genpip.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
// #207 (fieldtest 0108 F8): the refusal names the REAL rule (the rankable
|
||||
// R-expectancy family), never the mislabel "pip metric" — max_r_drawdown is
|
||||
@@ -2191,7 +2191,7 @@ fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-wfmc-seed");
|
||||
let proc_id = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC);
|
||||
write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "wfmc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wfmc.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("std::walk_forward cannot follow std::monte_carlo"),
|
||||
@@ -2400,7 +2400,7 @@ fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() {
|
||||
let bp_id = seed_blueprint(&dir, "campaign-run-zeromc-seed");
|
||||
let proc_id = register_process_doc(&dir, "zeromc.process.json", ZERO_RESAMPLES_MC_PROCESS_DOC);
|
||||
write_doc(&dir, "zeromc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "zeromc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "zeromc.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("process stage 1: monte_carlo resamples must be > 0"),
|
||||
@@ -2435,7 +2435,7 @@ fn campaign_run_refuses_unknown_tap_at_validate() {
|
||||
"badtap.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "badtap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "badtap.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
@@ -2478,7 +2478,7 @@ fn campaign_run_valid_tap_reaches_the_member_data_seam() {
|
||||
"taps.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"equity\"", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "taps.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
!out.contains("unknown tap"),
|
||||
@@ -2548,7 +2548,7 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
|
||||
"\"family_table\", \"selection_report\"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "wf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "wf.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -2725,7 +2725,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_pooled_oos_bootstrap() {
|
||||
// Both runs share the store (the cost-e2e sibling pattern) — the record
|
||||
// line is read from each run's own stdout, so no isolation is needed.
|
||||
let pooled = |doc: &str| -> serde_json::Value {
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", doc]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
let line = out
|
||||
.lines()
|
||||
@@ -2802,7 +2802,7 @@ fn campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap() {
|
||||
write_doc(&dir, "net_ps.campaign.json", &with_cost);
|
||||
|
||||
let per_survivor = |doc: &str| -> Vec<(u64, serde_json::Value)> {
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", doc]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", doc]);
|
||||
assert_eq!(code, Some(0), "campaign run failed: {out}");
|
||||
let line = out
|
||||
.lines()
|
||||
@@ -2864,7 +2864,7 @@ fn campaign_run_synthetic_e2e_per_instrument_cost_resolves_per_cell() {
|
||||
(1709251200000, 1717199999999),
|
||||
);
|
||||
write_doc(&dir, "percost.campaign.json", &doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "percost.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "percost.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
|
||||
// Member emit lines carry report.manifest; group the stamped resolved
|
||||
@@ -2925,7 +2925,7 @@ fn campaign_run_synthetic_e2e_vol_tf_regime_realizes_and_stamps() {
|
||||
);
|
||||
assert_ne!(with_risk, base, "replacen must actually match the seed field");
|
||||
write_doc(&dir, "voltf.campaign.json", &with_risk);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "voltf.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "voltf.campaign.json"]);
|
||||
assert_eq!(code, Some(0), "stdout/stderr: {out}");
|
||||
|
||||
let record_line = out
|
||||
@@ -3050,7 +3050,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
|
||||
write_doc(&dir, "close.campaign.json", &base);
|
||||
write_doc(&dir, "open.campaign.json", &rebound);
|
||||
|
||||
let (out_close, code_close) = run_code_in(&dir, &["campaign", "run", "close.campaign.json"]);
|
||||
let (out_close, code_close) = run_code_in(&dir, &["exec", "close.campaign.json"]);
|
||||
if code_close == Some(3)
|
||||
&& (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument"))
|
||||
{
|
||||
@@ -3058,7 +3058,7 @@ fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() {
|
||||
return;
|
||||
}
|
||||
assert_eq!(code_close, Some(0), "stdout/stderr: {out_close}");
|
||||
let (out_open, code_open) = run_code_in(&dir, &["campaign", "run", "open.campaign.json"]);
|
||||
let (out_open, code_open) = run_code_in(&dir, &["exec", "open.campaign.json"]);
|
||||
assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}");
|
||||
|
||||
let first_member = |out: &str| -> serde_json::Value {
|
||||
@@ -3113,7 +3113,7 @@ fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() {
|
||||
);
|
||||
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "regime.campaign.json", &with_regime);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "regime.campaign.json"]);
|
||||
|
||||
if code == Some(3)
|
||||
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
|
||||
@@ -3183,7 +3183,7 @@ fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() {
|
||||
);
|
||||
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "tworegimes.campaign.json", &with_regimes);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "tworegimes.campaign.json"]);
|
||||
|
||||
if code == Some(3)
|
||||
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
|
||||
@@ -3270,7 +3270,7 @@ fn campaign_persist_non_default_regime_does_not_false_fail_c1() {
|
||||
);
|
||||
assert_ne!(with_regime, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "regime-persist.campaign.json", &with_regime);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "regime-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "regime-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3350,7 +3350,7 @@ fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "cost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "cost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "cost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3445,7 +3445,7 @@ fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "net-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "net-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "net-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3559,7 +3559,7 @@ fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "volcost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "volcost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "volcost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3648,7 +3648,7 @@ fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() {
|
||||
);
|
||||
assert_ne!(with_regimes, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "tworegimes-persist.campaign.json", &with_regimes);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "tworegimes-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "tworegimes-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3726,7 +3726,7 @@ fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() {
|
||||
"",
|
||||
),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mixedtap.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -3842,7 +3842,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
|
||||
// (a) the cost-less baseline: net == gross, and net_r_equity skips with
|
||||
// the remedy notice while equity still persists.
|
||||
let (out_gross, code_gross) = run_code_in(&dir, &["campaign", "run", "costless.campaign.json"]);
|
||||
let (out_gross, code_gross) = run_code_in(&dir, &["exec", "costless.campaign.json"]);
|
||||
if code_gross == Some(3)
|
||||
&& (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument"))
|
||||
{
|
||||
@@ -3864,7 +3864,7 @@ fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() {
|
||||
|
||||
// (b) the costed run: gross identical member-for-member, net strictly
|
||||
// below gross, no skip notice, net_r_equity persisted on disk.
|
||||
let (out_net, code_net) = run_code_in(&dir, &["campaign", "run", "costnet.campaign.json"]);
|
||||
let (out_net, code_net) = run_code_in(&dir, &["exec", "costnet.campaign.json"]);
|
||||
assert_eq!(code_net, Some(0), "costed run: {out_net}");
|
||||
assert!(
|
||||
!out_net.contains("skipped"),
|
||||
@@ -3963,7 +3963,7 @@ fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() {
|
||||
);
|
||||
assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field");
|
||||
write_doc(&dir, "carrycost-persist.campaign.json", &with_cost);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "carrycost-persist.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "carrycost-persist.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -4035,7 +4035,7 @@ fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() {
|
||||
"mc.campaign.json",
|
||||
&campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""),
|
||||
);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "mc.campaign.json"]);
|
||||
|
||||
// Skip on a data-less machine: the member-data refusal, never a panic.
|
||||
if code == Some(3)
|
||||
@@ -4106,7 +4106,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
||||
let proc_id = register_process_doc(&dir, "mc2.process.json", MC_PROCESS_DOC);
|
||||
write_doc(&dir, "mc2.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
|
||||
let (file_out, file_code) = run_code_in(&dir, &["campaign", "run", "mc2.campaign.json"]);
|
||||
let (file_out, file_code) = run_code_in(&dir, &["exec", "mc2.campaign.json"]);
|
||||
assert_eq!(file_code, Some(3), "stdout/stderr: {file_out}");
|
||||
|
||||
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]);
|
||||
@@ -4121,7 +4121,7 @@ fn campaign_run_by_content_id_matches_file_sugar_refusal() {
|
||||
.expect("id")
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (id_out, id_code) = run_code_in(&dir, &["exec", &id]);
|
||||
assert_eq!(id_code, Some(3), "stdout/stderr: {id_out}");
|
||||
|
||||
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
|
||||
@@ -4169,8 +4169,8 @@ fn campaign_run_tolerates_content_prefix_on_target() {
|
||||
.trim_start_matches("content:")
|
||||
.to_string();
|
||||
|
||||
let (bare_out, _) = run_code_in(&dir, &["campaign", "run", &id]);
|
||||
let (pfx_out, _) = run_code_in(&dir, &["campaign", "run", &format!("content:{id}")]);
|
||||
let (bare_out, _) = run_code_in(&dir, &["exec", &id]);
|
||||
let (pfx_out, _) = run_code_in(&dir, &["exec", &format!("content:{id}")]);
|
||||
|
||||
let pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line");
|
||||
assert!(
|
||||
@@ -4222,7 +4222,7 @@ fn campaign_run_accepts_a_graph_register_seeded_strategy() {
|
||||
|
||||
let proc_id = register_process_doc(&dir, "onramp.process.json", SWEEP_ONLY_PROCESS_DOC);
|
||||
write_doc(&dir, "onramp.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""));
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "onramp.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "onramp.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("no recorded geometry") || out.contains("no data for instrument"),
|
||||
@@ -4251,7 +4251,7 @@ fn campaign_run_invalid_file_refuses_before_touching_store() {
|
||||
let bad_path = write_doc(&dir, "bad.campaign.json", &bad);
|
||||
let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone()), ScratchPath::File(bad_path)]);
|
||||
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "bad.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "bad.campaign.json"]);
|
||||
assert_eq!(code, Some(1), "stdout/stderr: {out}");
|
||||
assert!(
|
||||
out.contains("aura: campaign document invalid:"),
|
||||
@@ -4304,7 +4304,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
|
||||
}}"#,
|
||||
);
|
||||
write_doc(&dir, "gap.campaign.json", &doc);
|
||||
let (out, code) = run_code_in(&dir, &["campaign", "run", "gap.campaign.json"]);
|
||||
let (out, code) = run_code_in(&dir, &["exec", "gap.campaign.json"]);
|
||||
assert_eq!(code, Some(3), "stdout/stderr: {out}");
|
||||
|
||||
let record_line = out
|
||||
@@ -4346,7 +4346,7 @@ fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() {
|
||||
/// clap refuses it as a usage error (exit 2) before any target resolution.
|
||||
#[test]
|
||||
fn campaign_run_rejects_a_zero_parallel_instruments_bound() {
|
||||
let (out, code) = run_code(&["campaign", "run", "unused", "--parallel-instruments", "0"]);
|
||||
let (out, code) = run_code(&["exec", "unused", "--parallel-instruments", "0"]);
|
||||
assert_eq!(code, Some(2), "clap usage error expected, got: {out}");
|
||||
// Domain prose, not clap's raw NonZeroUsize wording ("number would be
|
||||
// zero for non-zero type") — the fieldtest friction finding.
|
||||
|
||||
Reference in New Issue
Block a user