//! 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) { 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 `. 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) { 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 = (1..R_SMA_PRICES.len()) .map(|i| (R_SMA_PRICES[i - 1] + R_SMA_PRICES[i]) / 2.0) .collect(); let expected = sma.iter().sum::() / 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}" ); } /// 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`). #[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}" ); } // --------------------------------------------------------------------------- // 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 = 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}" ); } } /// 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}"); } /// 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}"); }