//! End-to-end coverage for the `aura process` verb family (#188/#189): drives //! the built `aura` binary over argv/file, the exact surface a data-level //! author uses. use std::path::{Path, PathBuf}; use std::sync::{Mutex, MutexGuard, OnceLock}; /// A fresh, unique working directory for a process test that persists /// content-addressed documents under `./runs/` (so `aura process register` /// never dirties the repo). Unique per test + per process; no external /// tempfile dependency (mirrors `cli_run.rs` / `cli_broken_pipe.rs`). fn temp_cwd(name: &str) -> PathBuf { let dir = std::env::temp_dir().join(format!("aura-cli-{}-{}", std::process::id(), 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 run_code(args: &[&str]) -> (String, Option) { run_code_in(Path::new("."), args) } const PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "wf-deflated-screen", "pipeline": [ { "block": "std::sweep", "metric": "net_expectancy_r", "select": "plateau:worst", "deflate": true }, { "block": "std::gate", "all": [ { "metric": "net_expectancy_r", "cmp": "gt", "value": 0.0 }, { "metric": "overfit_probability", "cmp": "lt", "value": 0.1 } ] }, { "block": "std::walk_forward", "in_sample_ms": 4000, "out_of_sample_ms": 1000, "step_ms": 1000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" } ] }"#; fn write_doc(dir: &Path, name: &str, text: &str) -> PathBuf { let p = dir.join(name); std::fs::write(&p, text).expect("write doc"); p } #[test] fn process_validate_reports_intrinsic_ok() { let dir = temp_cwd("process-validate-ok"); write_doc(&dir, "p.process.json", PROCESS_DOC); let (out, code) = run_code_in(&dir, &["process", "validate", "p.process.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains("process document valid (intrinsic): 3 pipeline blocks, 2 gate predicates")); } #[test] fn process_validate_refuses_unknown_metric_as_prose_exit_1() { let dir = temp_cwd("process-validate-bad-metric"); let bad = PROCESS_DOC.replacen( "net_expectancy_r\", \"select\": \"plateau:worst", "netto_r\", \"select\": \"plateau:worst", 1, ); write_doc(&dir, "bad.process.json", &bad); let (out, code) = run_code_in(&dir, &["process", "validate", "bad.process.json"]); assert_eq!(code, Some(1)); assert!(out.contains("unknown metric \"netto_r\"")); assert!(!out.contains("UnknownMetric"), "Debug leak: {out}"); } #[test] fn process_validate_refuses_zero_walk_forward_length_as_prose_exit_1() { let dir = temp_cwd("process-validate-zero-wf"); let bad = PROCESS_DOC.replacen("\"step_ms\": 1000", "\"step_ms\": 0", 1); write_doc(&dir, "bad.process.json", &bad); let (out, code) = run_code_in(&dir, &["process", "validate", "bad.process.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("pipeline[2]: walk_forward step_ms must be > 0"), "stdout/stderr: {out}"); assert!(!out.contains("ZeroWalkForwardLength"), "Debug leak: {out}"); } /// A document authored against the vocabulary `std::walk_forward` carried /// before cycle 0107 (`folds`/`in_sample_bars`/`out_of_sample_bars`, none of /// which the engine's `WindowRoller` accepts) is refused end-to-end through /// the CLI's malformed-document path (a parse failure, distinct from the /// validate-fault path exercised above) with prose naming the offending /// slot — never a Debug-formatted `DocError`, never a silent accept of the /// retired shape. #[test] fn process_validate_refuses_retired_walk_forward_vocabulary_as_prose_exit_1() { let dir = temp_cwd("process-validate-retired-wf-vocab"); let bad = PROCESS_DOC.replacen( "{ \"block\": \"std::walk_forward\", \"in_sample_ms\": 4000, \"out_of_sample_ms\": 1000,\n \"step_ms\": 1000, \"mode\": \"rolling\", \"metric\": \"net_expectancy_r\", \"select\": \"argmax\" }", "{ \"block\": \"std::walk_forward\", \"folds\": 4, \"in_sample_bars\": 4000, \"out_of_sample_bars\": 1000, \"metric\": \"net_expectancy_r\", \"select\": \"argmax\" }", 1, ); assert_ne!(bad, PROCESS_DOC, "replacen must actually match the fixture's walk_forward stage"); write_doc(&dir, "bad.process.json", &bad); let (out, code) = run_code_in(&dir, &["process", "validate", "bad.process.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("unknown slot \"folds\""), "stdout/stderr: {out}"); assert!(!out.contains("Malformed"), "Debug leak: {out}"); } /// The roller's second `RollMode` — `anchored` (growing, origin-fixed /// in-sample) — round-trips through the CLI's actual validate path, not /// just the `slot_kind_label` description string: a data author who writes /// `"mode": "anchored"` gets a valid document, exercising the `WfMode` /// serde oracle's non-default variant end to end. #[test] fn process_validate_accepts_anchored_walk_forward_mode() { let dir = temp_cwd("process-validate-anchored-wf-mode"); let anchored = PROCESS_DOC.replacen("\"mode\": \"rolling\"", "\"mode\": \"anchored\"", 1); assert_ne!(anchored, PROCESS_DOC, "replacen must actually match the fixture's mode field"); write_doc(&dir, "anchored.process.json", &anchored); let (out, code) = run_code_in(&dir, &["process", "validate", "anchored.process.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains("process document valid (intrinsic): 3 pipeline blocks, 2 gate predicates")); } #[test] fn process_introspect_vocabulary_block_and_content_id() { let (out, code) = run_code(&["process", "introspect", "--vocabulary"]); assert_eq!(code, Some(0)); for id in ["std::sweep", "std::gate", "std::walk_forward", "std::monte_carlo", "std::generalize"] { assert!(out.contains(id), "vocabulary misses {id}: {out}"); } let (out, code) = run_code(&["process", "introspect", "--block", "std::sweep"]); assert_eq!(code, Some(0)); assert!(out.contains("metric")); assert!(out.contains("required")); let (out, code) = run_code(&["process", "introspect", "--block", "std::walk_forward"]); assert_eq!(code, Some(0)); for slot in ["in_sample_ms", "out_of_sample_ms", "step_ms", "mode"] { assert!(out.contains(slot), "walk_forward describe misses {slot}: {out}"); } assert!(out.contains("one of: rolling | anchored"), "mode label missing: {out}"); assert!(!out.contains("folds"), "retired slot still advertised: {out}"); let dir = temp_cwd("process-introspect-content-id"); write_doc(&dir, "p.process.json", PROCESS_DOC); let (out, code) = run_code_in(&dir, &["process", "introspect", "--content-id", "p.process.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // #194: --content-id prints the bare 64-hex id (matching `graph introspect // --content-id` and the store filename), no glued `content:` display prefix. let line = out.lines().last().expect("id line").trim(); assert_eq!(line.len(), 64, "bare 64-hex id, no content: prefix: {out}"); assert!(line.chars().all(|c| c.is_ascii_hexdigit()), "bare hex id: {out}"); assert!(!out.contains("content:"), "--content-id must not glue the content: prefix: {out}"); } #[test] fn process_introspect_unknown_block_is_prose_exit_1() { let (out, code) = run_code(&["process", "introspect", "--block", "std::nope"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("unknown block \"std::nope\""), "stdout/stderr: {out}"); } #[test] fn process_introspect_unwired_lists_open_slots() { let dir = temp_cwd("process-introspect-unwired"); let partial = r#"{ "format_version": 1, "kind": "process", "pipeline": [ { "block": "std::sweep", "metric": "sqn" } ] }"#; write_doc(&dir, "partial.process.json", partial); let (out, code) = run_code_in(&dir, &["process", "introspect", "--unwired", "partial.process.json"]); assert_eq!(code, Some(0)); assert!(out.contains("open slot: name")); assert!(out.contains("open slot: pipeline[0].select")); } #[test] fn process_introspect_no_flag_is_usage_exit_2() { let (_out, code) = run_code(&["process", "introspect"]); assert_eq!(code, Some(2)); } /// #197 (fieldtest 0107 F9): the metric roster is enumerable from the public /// surface. `aura process introspect --metrics` lists every name in /// `aura_research::metric_vocabulary()` (17), each annotated with where it is /// applicable — `rankable` (a sweep/walk_forward `select` metric, from /// `aura_campaign::RANKABLE_METRICS`), `gate` (a gate-predicate per-member /// scalar, from `aura_campaign::PER_MEMBER_METRICS`), or `annotation` (in /// neither roster — a selection annotation that can neither rank nor gate). The /// split the executor enforces at preflight (a metric may exist yet not be /// rankable, F9) is thereby readable before any trial run. Substrings are /// pinned, not exact column widths. #[test] fn process_introspect_metrics_lists_vocabulary_with_applicability() { let (out, code) = run_code(&["process", "introspect", "--metrics"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // One line per vocabulary name, nothing else — the whole 17-name roster. let lines: Vec<&str> = out.lines().filter(|l| !l.trim().is_empty()).collect(); assert_eq!(lines.len(), 17, "one line per metric_vocabulary() name: {out}"); // Locate a metric's line by its exact name column (first whitespace token), // so `expectancy_r` never matches `net_expectancy_r`'s line. let line_for = |name: &str| -> String { out.lines() .find(|l| l.split_whitespace().next() == Some(name)) .unwrap_or_else(|| panic!("no line for {name}: {out}")) .to_string() }; // expectancy_r is BOTH rankable and a per-member gate scalar. let er = line_for("expectancy_r"); assert!(er.contains("rankable") && er.contains("gate"), "expectancy_r tags: {er}"); // total_pips likewise (a rankable scalar that is also per-member). let tp = line_for("total_pips"); assert!(tp.contains("rankable") && tp.contains("gate"), "total_pips tags: {tp}"); // win_rate is per-member (gate) but NOT rankable. let wr = line_for("win_rate"); assert!(wr.contains("gate") && !wr.contains("rankable"), "win_rate tags: {wr}"); // deflated_score is a selection annotation — in neither roster. let ds = line_for("deflated_score"); assert!(ds.contains("annotation"), "deflated_score tag: {ds}"); } /// `--metrics` joins the existing one-mode guard: exactly one introspect mode /// is allowed, so `--metrics` alongside another mode refuses with the usage /// line (exit 2), and that usage line is extended to advertise `--metrics` as /// one of the modes. (The guard's own `Usage: aura process introspect /// --vocabulary …` line is asserted — never clap's unknown-arg error, which /// spells the flag list as `[OPTIONS]`.) #[test] fn process_introspect_metrics_plus_another_mode_is_usage_exit_2() { let (out, code) = run_code(&["process", "introspect", "--metrics", "--vocabulary"]); assert_eq!(code, Some(2), "stdout/stderr: {out}"); assert!( out.contains("Usage: aura process introspect --vocabulary"), "the one-mode guard usage line must fire: {out}" ); assert!(out.contains("--metrics"), "the usage line must advertise --metrics: {out}"); } #[test] fn process_register_stores_content_addressed_under_runs_root() { let dir = temp_cwd("process-register"); write_doc(&dir, "p.process.json", PROCESS_DOC); let (out, code) = run_code_in(&dir, &["process", "register", "p.process.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // #194: the register verb prints the bare id — `registered process ()`. let line = out.lines().find(|l| l.starts_with("registered process ")).expect("line"); assert!(!line.contains("content:"), "register line prints the bare id, no content: prefix: {line}"); let id = line .trim_start_matches("registered process ") .split(' ') .next() .expect("id"); assert!(dir.join("runs").join("processes").join(format!("{id}.json")).is_file()); } /// The demo-project fixture (Aura.toml present), built once — mirrors /// `project_load.rs`'s `built_fixture` (a separate test binary, so it needs /// its own `OnceLock`, but `cargo build` is idempotent). fn built_project() -> &'static PathBuf { static BUILT: OnceLock = OnceLock::new(); BUILT.get_or_init(|| { let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project"); let out = std::process::Command::new("cargo") .arg("build") .current_dir(&dir) .output() .expect("spawn cargo build for the fixture project"); assert!( out.status.success(), "fixture build failed:\n{}", String::from_utf8_lossy(&out.stderr) ); dir }) } /// Serializes every test that touches the shared demo-project fixture store /// (they remove/re-seed `/runs`, so parallel test threads would race /// on it). A poisoned lock is taken over: one failed test must not cascade /// into unrelated lock panics. fn project_lock() -> MutexGuard<'static, ()> { static LOCK: Mutex<()> = Mutex::new(()); LOCK.lock().unwrap_or_else(|e| e.into_inner()) } /// A scratch filesystem entry this test writes under the git-tracked /// demo-project fixture root (only `runs/` there is fixture-gitignored), /// removed on drop — including during a mid-test panic — so a failed /// assertion never leaks scratch docs into tracked working-tree state. enum ScratchPath { File(PathBuf), Dir(PathBuf), } struct ScratchGuard(Vec); impl Drop for ScratchGuard { fn drop(&mut self) { for p in &self.0 { match p { ScratchPath::File(p) => { let _ = std::fs::remove_file(p); } ScratchPath::Dir(p) => { let _ = std::fs::remove_dir_all(p); } } } } } /// The `aura campaign validate` referential tier (the campaign family's /// headline addition over the intrinsic-only process family), exercised at /// the CLI seam inside a real project: a fully-resolved document prints the /// "valid (referential)" success line, and an unresolved reference prints /// the "do not resolve" fault block through `ref_fault_prose`. The /// constituents (`validate_campaign_refs`, `ref_fault_prose`) are unit- /// tested elsewhere; this pins that `aura campaign validate` actually wires /// them together end to end. #[test] fn campaign_validate_in_project_reports_referential_tier_end_to_end() { let _fixture = project_lock(); let dir = built_project(); 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("seed.process.json")), ScratchPath::File(dir.join("ref-ok.campaign.json")), ScratchPath::File(dir.join("ref-bad.campaign.json")), ]); // Seed one real, content-addressed, open-param blueprint into the // project's own store via a real sweep (mirrors `project_load.rs`'s // anchor test). let open_bp = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR")); let (sweep_out, sweep_code) = run_code_in( dir, &[ "sweep", &open_bp, "--axis", "sma_signal.fast.length=2,4", "--axis", "sma_signal.slow.length=8,16", "--name", "campaign-ref-seed", ], ); assert_eq!(sweep_code, Some(0), "seed sweep failed: {sweep_out}"); let bp_id = std::fs::read_dir(runs_dir.join("blueprints")) .expect("blueprints dir") .next() .expect("one stored blueprint") .expect("dir entry") .path() .file_stem() .expect("stem") .to_string_lossy() .into_owned(); // Register a valid process into the same project store. write_doc(dir, "seed.process.json", PROCESS_DOC); let (proc_out, proc_code) = run_code_in(dir, &["process", "register", "seed.process.json"]); assert_eq!(proc_code, Some(0), "seed process register failed: {proc_out}"); let proc_id = proc_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(); // "fast.length": the axis name is the RAW composite's `param_space` name. // `validate_campaign_refs` loads the stored blueprint bare, unlike the // sweep's `wrap_r`-wrapped axis probe, so it does NOT carry the // "sma_signal." prefix `aura sweep --axis` binds against. let campaign = format!( r#"{{ "format_version": 1, "kind": "campaign", "name": "ref-check", "data": {{ "instruments": ["GER40"], "windows": [ {{ "from_ms": 1, "to_ms": 2 }} ] }}, "strategies": [ {{ "ref": {{ "content_id": "{bp}" }}, "axes": {{ "fast.length": {{ "kind": "I64", "values": [2] }}, "slow.length": {{ "kind": "I64", "values": [6] }} }} }} ], "process": {{ "ref": {{ "content_id": "{proc}" }} }}, "seed": 1, "presentation": {{ "persist_taps": [], "emit": ["family_table"] }} }}"#, bp = bp_id, proc = proc_id ); write_doc(dir, "ref-ok.campaign.json", &campaign); let (out, code) = run_code_in(dir, &["campaign", "validate", "ref-ok.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains( "campaign document valid (referential): all references resolve, axes are in the param space" )); // An unresolved process reference: the fault block + `ref_fault_prose` // mapping, prose exit 1, at the same CLI seam. let bad = campaign.replace(&proc_id, "0000000000000000000000000000000000000000000000000000000000000000"); write_doc(dir, "ref-bad.campaign.json", &bad); let (out2, code2) = run_code_in(dir, &["campaign", "validate", "ref-bad.campaign.json"]); assert_eq!(code2, Some(1)); assert!(out2.contains("campaign references do not resolve:"), "stdout/stderr: {out2}"); assert!(out2.contains("not found in the project store"), "stdout/stderr: {out2}"); // `_cleanup` (a `ScratchGuard`) removes the scratch docs and `runs/` on // drop, at the end of this scope — including on a mid-test panic. } 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"] } }"#; #[test] fn campaign_validate_outside_project_skips_referential_tier() { let dir = temp_cwd("campaign-validate-outside-project"); write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC); let (out, code) = run_code_in(&dir, &["campaign", "validate", "c.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains("campaign document valid (intrinsic):")); assert!(out.contains("referential checks skipped (no Aura.toml found up from ")); } #[test] fn campaign_validate_refuses_empty_axis_prose_exit_1() { let dir = temp_cwd("campaign-validate-empty-axis"); let bad = CAMPAIGN_DOC.replacen(r#""values": [8]"#, r#""values": []"#, 1); write_doc(&dir, "bad.campaign.json", &bad); let (out, code) = run_code_in(&dir, &["campaign", "validate", "bad.campaign.json"]); assert_eq!(code, Some(1)); assert!(out.contains("axes.fast: an axis is a non-empty finite set")); assert!(!out.contains("EmptyAxis"), "Debug leak: {out}"); } #[test] fn campaign_introspect_vocabulary_lists_sections() { let (out, code) = run_code(&["campaign", "introspect", "--vocabulary"]); assert_eq!(code, Some(0)); for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation"] { assert!(out.contains(id), "vocabulary misses {id}: {out}"); } } #[test] fn campaign_introspect_unwired_reports_the_spec_example_slots() { let dir = temp_cwd("campaign-introspect-unwired"); let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft", "data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] }, "strategies": [ { "ref": { "content_id": "9f3a" }, "axes": { "slow": { "kind": "I64", "values": [] } } } ], "seed": 1, "presentation": { "persist_taps": [], "emit": [] } }"#; write_doc(&dir, "draft.campaign.json", draft); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]); assert_eq!(code, Some(0)); assert!(out.contains("open slot: process.ref (required, content id of a process document)")); assert!(out.contains("open slot: strategies[0].axes.slow (axis declared empty — a P1 axis is a non-empty finite set)")); } /// #195 (fieldtest 0106 F4): the consumer view of the placeholder-ref probe. /// A draft that spells its open refs as `"ref": {}` / `"ref": null` — the /// natural drafting placeholders — has `--unwired` enumerate BOTH as open slots /// (exit 0), instead of the old cryptic serde refusal / silent "no open slots". #[test] fn campaign_introspect_unwired_reports_placeholder_refs() { let dir = temp_cwd("campaign-introspect-unwired-placeholder"); let draft = r#"{ "format_version": 1, "kind": "campaign", "name": "draft", "data": { "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] }, "strategies": [ { "ref": null, "axes": { "slow": { "kind": "I64", "values": [10, 20] } } } ], "process": { "ref": {} }, "seed": 1, "presentation": { "persist_taps": [], "emit": [] } }"#; write_doc(&dir, "draft.campaign.json", draft); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "draft.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains("open slot: strategies[0].ref (required, one of: content_id, identity_id)"), "strategy `ref: null` placeholder not enumerated: {out}" ); assert!( out.contains("open slot: process.ref (required, content id of a process document)"), "process `ref: {{}}` placeholder not enumerated: {out}" ); } /// The campaign twin of `process_introspect_vocabulary_block_and_content_id`'s /// `--content-id` branch: pins that `campaign introspect --content-id` wires /// `parse_valid_campaign` + `campaign_to_json` + `content_id_of` end to end /// (the process branch was covered; this one was not). #[test] fn campaign_introspect_content_id_prints_hash() { let dir = temp_cwd("campaign-introspect-content-id"); write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--content-id", "c.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // #194: --content-id prints the bare 64-hex id, no glued `content:` prefix. let line = out.lines().last().expect("id line").trim(); assert_eq!(line.len(), 64, "bare 64-hex id, no content: prefix: {out}"); assert!(line.chars().all(|c| c.is_ascii_hexdigit()), "bare hex id: {out}"); assert!(!out.contains("content:"), "--content-id must not glue the content: prefix: {out}"); } #[test] fn campaign_register_stores_content_addressed_under_runs_root() { let dir = temp_cwd("campaign-register"); write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC); let (out, code) = run_code_in(&dir, &["campaign", "register", "c.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // #194: the register verb prints the bare id — `registered campaign ()`. let line = out.lines().find(|l| l.starts_with("registered campaign ")).expect("line"); assert!(!line.contains("content:"), "register line prints the bare id, no content: prefix: {line}"); let id = line .trim_start_matches("registered campaign ") .split(' ') .next() .expect("id"); assert!(dir.join("runs").join("campaigns").join(format!("{id}.json")).is_file()); } /// `guard_one_mode` is shared, family-parameterized code (`process` and /// `campaign` both route through it). The process family already pins the /// zero-flags arm; this pins that the campaign family gets its own family /// name threaded into the usage line, not a copy-pasted "process" string. #[test] fn campaign_introspect_no_flag_usage_names_the_campaign_family() { let (out, code) = run_code(&["campaign", "introspect"]); assert_eq!(code, Some(2), "stdout/stderr: {out}"); assert!( out.contains("Usage: aura campaign introspect --vocabulary | --block | --unwired | --content-id "), "stdout/stderr: {out}" ); } /// `guard_one_mode` requires *exactly* one mode: the zero-flags arm is /// pinned elsewhere, but nothing previously exercised the over-selection /// arm (`selected_modes() != 1` is not just `== 0`). Two modes at once must /// refuse with the same usage-exit-2 idiom, not silently pick one. #[test] fn campaign_introspect_two_flags_is_usage_exit_2() { let (out, code) = run_code(&["campaign", "introspect", "--vocabulary", "--block", "std::data"]); assert_eq!(code, Some(2), "stdout/stderr: {out}"); assert!(out.contains("Usage: aura campaign introspect"), "stdout/stderr: {out}"); } /// #197: the metrics roster is process-side R-vocabulary, but `--metrics` rides /// the shared `DocIntrospectCmd` both families route through, so `aura campaign /// introspect --metrics` WORKS too rather than usage-refusing. Ground for the /// pin: a mode the shared struct carries needs no per-family exclusion, and the /// vocabulary a campaign's referenced process must name is thus discoverable /// from the campaign verb as well (simplest honest reading). Same annotated /// roster as the process family. #[test] fn campaign_introspect_metrics_also_lists_the_vocabulary() { let (out, code) = run_code(&["campaign", "introspect", "--metrics"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); let er = out .lines() .find(|l| l.split_whitespace().next() == Some("expectancy_r")) .unwrap_or_else(|| panic!("no expectancy_r line: {out}")); assert!(er.contains("rankable") && er.contains("gate"), "expectancy_r tags: {er}"); assert!( out.lines() .any(|l| l.split_whitespace().next() == Some("deflated_score") && l.contains("annotation")), "the annotation roster is reachable from the campaign family too: {out}" ); } /// Register must be a gate, not a passthrough: an invalid document is /// refused with prose (exit 1) and — the property that matters for a /// content-addressed store — no file is ever written under `runs/campaigns/` /// for it. A store that could contain unvalidated documents would make /// every downstream reader re-derive the checks register was supposed to /// have already made. #[test] fn campaign_register_refuses_invalid_document_and_writes_nothing() { let dir = temp_cwd("campaign-register-invalid"); let bad = CAMPAIGN_DOC.replacen(r#""values": [8]"#, r#""values": []"#, 1); write_doc(&dir, "bad.campaign.json", &bad); let (out, code) = run_code_in(&dir, &["campaign", "register", "bad.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("refusing to register:"), "stdout/stderr: {out}"); assert!(out.contains("axes.fast: an axis is a non-empty finite set"), "stdout/stderr: {out}"); assert!( !dir.join("runs").join("campaigns").exists(), "register must not create a store entry for an invalid document" ); } /// Seed one open-param blueprint into the built demo project's store via a /// real sweep and return its content id (the referential test's recipe). fn seed_blueprint(dir: &Path, name: &str) -> String { let open_bp = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR")); let (out, code) = run_code_in( dir, &[ "sweep", &open_bp, "--axis", "sma_signal.fast.length=2,4", "--axis", "sma_signal.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. /// Asserts register exits 0 — an intrinsically valid document (an mc-bearing /// one included) must always register; only `campaign run` draws the v1 line. 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() } /// A referentially-resolving campaign over the seeded blueprint. Axes name /// the RAW `param_space` names (`fast.length` / `slow.length` — see the /// naming note in the referential test above). `persist_taps`/`emit` are /// spliced verbatim (pass `""` for empty, `"\"family_table\""` etc.). fn campaign_doc_json( bp_id: &str, proc_id: &str, window: (i64, i64), persist_taps: &str, emit: &str, ) -> String { format!( r#"{{ "format_version": 1, "kind": "campaign", "name": "run-seam", "data": {{ "instruments": ["GER40"], "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": [{persist_taps}], "emit": [{emit}] }} }}"#, from = window.0, to = window.1, ) } /// An mc-bearing process: intrinsically VALID (register accepts it) but past /// the executable v1 boundary (`campaign run` refuses it at preflight). const MC_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "mc-screen", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::monte_carlo", "resamples": 100, "block_len": 5 } ] }"#; /// The minimal executable v1 pipeline (one sweep stage). const SWEEP_ONLY_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "sweep-only", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" } ] }"#; /// The full v1 shape for the gated e2e: sweep -> gate -> walk_forward. The /// gate (`n_trades ge 0`) passes every member, so walk-forward always has /// survivors. Roller: 14d IS / 7d OOS / 7d step in epoch-ms, tiling the /// ~30-day GER40 Sept-2024 campaign window. const WF_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "screen-then-walkforward", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] }, { "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 run` outside a project refuses up front — before target /// resolution, so not even the file-sugar registration touches a store. #[test] 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"]); 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 target that is neither a readable file nor a 64-hex id refuses naming /// both readings (inside the project, past the project gate). #[test] fn campaign_run_bogus_target_refuses() { let _fixture = project_lock(); let dir = built_project(); let (out, code) = run_code_in(dir, &["campaign", "run", "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"), "stdout/stderr: {out}" ); } /// A well-formed but unknown content id refuses with the store-miss prose. #[test] fn campaign_run_unknown_id_refuses() { let _fixture = project_lock(); let dir = built_project(); let id = "0".repeat(64); let (out, code) = run_code_in(dir, &["campaign", "run", &id]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains(&format!("no campaign {id} in the project store")), "stdout/stderr: {out}" ); } /// The v1 boundary: `process register` ACCEPTS an mc-bearing document (it is /// intrinsically valid — asserted inside `register_process_doc`); only /// `campaign run` refuses it, at preflight, before any member runs (so no /// data is needed), with path-addressed Debug-free prose. #[test] fn campaign_run_v1_boundary_refuses_mc_process() { let _fixture = project_lock(); let dir = built_project(); 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("mc.process.json")), ScratchPath::File(dir.join("mc.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-mc-seed"); let proc_id = register_process_doc(dir, "mc.process.json", MC_PROCESS_DOC); write_doc(dir, "mc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); let (out, code) = run_code_in(dir, &["campaign", "run", "mc.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("not executable in v1"), "stdout/stderr: {out}"); assert!(out.contains("std::monte_carlo"), "the prose names the block: {out}"); assert!(!out.contains("UnsupportedStage"), "Debug leak: {out}"); } /// A v2 process doc with `std::walk_forward` following the annotator /// `std::monte_carlo` — intrinsically valid (order across population vs. /// annotator stages is not a document-tier concern), refused only by the /// executor's v2 preflight shape guard. const WF_AFTER_MC_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "wf-after-mc", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::monte_carlo", "resamples": 100, "block_len": 5 }, { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, "step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "argmax" } ] }"#; /// Property (0108 task 2, mirroring aura-research's characterization pin /// `validate_process_permits_walk_forward_after_an_annotator`): the /// intrinsic/executor tier boundary is observable across TWO CLI commands. /// `process validate` accepts `[sweep, monte_carlo, walk_forward]` (exit 0 — /// the document tier does not order population vs. annotator stages) while /// `campaign run`'s v2 preflight refuses the very same document, before any /// member executes (data-free: the [1, 2] 1970-epoch-ms window is never /// reached), because `std::walk_forward` cannot follow the annotator /// `std::monte_carlo`. A regression that let the executor silently accept the /// intrinsic tier's looser order (or that widened the intrinsic tier to /// match) would collapse this two-command divergence. #[test] fn process_validate_permits_wf_after_mc_but_campaign_run_refuses_it() { // intrinsic tier: no project needed, and it accepts the doc outright. let vdir = temp_cwd("wf-after-mc-validate"); write_doc(&vdir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); let (vout, vcode) = run_code_in(&vdir, &["process", "validate", "wfmc.process.json"]); assert_eq!(vcode, Some(0), "intrinsic validate must accept sweep->mc->walk_forward: {vout}"); assert!( vout.contains("process document valid (intrinsic): 3 pipeline blocks"), "stdout/stderr: {vout}" ); // executor tier: campaign run's v2 preflight refuses the same shape. let _fixture = project_lock(); let dir = built_project(); 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("wfmc.process.json")), ScratchPath::File(dir.join("wfmc.campaign.json")), ]); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("std::walk_forward cannot follow std::monte_carlo"), "stdout/stderr: {out}" ); assert!(!out.contains("PipelineShape {"), "Debug leak: {out}"); } /// A v2 process doc with a static `std::monte_carlo resamples: 0` defect. const ZERO_RESAMPLES_MC_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "zero-resamples-mc", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::monte_carlo", "resamples": 0, "block_len": 5 } ] }"#; /// Property (0108 task 2): `std::monte_carlo`'s `resamples > 0` guard is a /// STATIC executor-preflight refusal, wired end to end from the on-disk /// document through to CLI stdout, firing before any member runs (data-free: /// the [1, 2] 1970-epoch-ms window is never reached) and naming the stage /// index and the offending field. A regression that dropped the static guard /// (deferring to the engine's runtime all-zero-resamples degenerate instead) /// would let this refusal disappear or drift to a different message. #[test] fn campaign_run_refuses_zero_resamples_monte_carlo_before_any_member_runs() { let _fixture = project_lock(); let dir = built_project(); 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("zeromc.process.json")), ScratchPath::File(dir.join("zeromc.campaign.json")), ]); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("process stage 1: monte_carlo resamples must be > 0"), "stdout/stderr: {out}" ); assert!(!out.contains("ZeroBootstrapParam"), "Debug leak: {out}"); } /// A v2 process doc that ends in `std::generalize`. const GENERALIZE_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "generalize-screen", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::generalize", "metric": "net_expectancy_r" } ] }"#; /// Property (0108 task 2): `std::generalize`'s campaign-level instrument- /// arity guard (>= 2 instruments) is a STATIC executor-preflight refusal, /// wired end to end from the campaign document's `data.instruments` through /// to CLI stdout, firing before any member runs (data-free) and naming the /// actual instrument count. `campaign_doc_json`'s fixture campaign carries /// exactly one instrument (`GER40`), so this exercises the campaign /// parameter `preflight` newly reads (previously unused in v1) rather than /// anything reachable from the process document alone. A regression that /// left the campaign parameter unread would silently accept a single- /// instrument `std::generalize` and only fail later, opaquely, inside the /// generalization math. #[test] fn campaign_run_refuses_single_instrument_generalize() { let _fixture = project_lock(); let dir = built_project(); 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("gen.process.json")), ScratchPath::File(dir.join("gen.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-gen-seed"); let proc_id = register_process_doc(dir, "gen.process.json", GENERALIZE_PROCESS_DOC); write_doc(dir, "gen.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); let (out, code) = run_code_in(dir, &["campaign", "run", "gen.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("campaign has 1 instrument(s); std::generalize needs at least 2"), "stdout/stderr: {out}" ); assert!(!out.contains("GeneralizeNeedsInstruments"), "Debug leak: {out}"); } /// Non-empty persist_taps defers LOUDLY, and the note's ORDER is pinned: it /// prints before member execution, so it is asserted here on a run /// that refuses at the member-data seam. The [1, 2] epoch-ms window (1970) /// makes that refusal deterministic on every machine: a data-less host /// refuses on missing geometry, a data-ful host on a window no archive file /// overlaps — exit 1 either way, tap note already on stderr. #[test] fn campaign_run_persist_taps_deferred_loudly() { let _fixture = project_lock(); let dir = built_project(); 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("taps.process.json")), ScratchPath::File(dir.join("taps.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-taps-seed"); let proc_id = register_process_doc(dir, "taps.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( dir, "taps.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "\"r_record\"", ""), ); let (out, code) = run_code_in(dir, &["campaign", "run", "taps.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("aura: persist_taps not yet honored (1 tap(s) ignored)"), "the tap note must precede the member-data refusal: {out}" ); assert!( out.contains("no recorded geometry") || out.contains("no data for instrument"), "the refusal names the data condition: {out}" ); } /// Gated real-data e2e (the `cli_run.rs` skip idiom): a full /// sweep -> gate -> walk_forward campaign over GER40 Sept-2024 where the /// local archive is present — exit 0, emit-gated family/selection lines, a /// final line parseable as JSON with top-level `campaign_run` linking a sweep /// family id and a walk-forward family id, and the `campaign_runs.jsonl` /// sibling store written. Skips with a note elsewhere so /// `cargo test --workspace` stays green on a data-less machine. #[test] fn campaign_run_real_e2e_sweep_gate_walkforward() { let _fixture = project_lock(); let dir = built_project(); 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("wf.process.json")), ScratchPath::File(dir.join("wf.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-e2e-seed"); let proc_id = register_process_doc(dir, "wf.process.json", WF_PROCESS_DOC); // The GER40 Sept-2024 window (inclusive Unix-ms) — the same gated window // cli_run.rs drives; ~30 days, so the (14d, 7d, 7d) roller tiles it. write_doc( dir, "wf.campaign.json", &campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "", "\"family_table\", \"selection_report\"", ), ); let (out, code) = run_code_in(dir, &["campaign", "run", "wf.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(1) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign e2e"); return; } assert_eq!(code, Some(0), "stdout/stderr: {out}"); let record_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(record_line).expect("campaign_run line parses as JSON"); let cells = v["campaign_run"]["cells"].as_array().expect("cells array"); assert_eq!(cells.len(), 1, "one (strategy, instrument, window) cell: {record_line}"); let stages = cells[0]["stages"].as_array().expect("stages array"); assert_eq!(stages.len(), 3, "sweep + gate + walk_forward realized: {record_line}"); assert!( stages[0]["family_id"].as_str().is_some(), "sweep stage links a family: {record_line}" ); assert_eq!( stages[1]["survivor_ordinals"].as_array().map(|a| a.len()), Some(4), "the always-true gate keeps all four members: {record_line}" ); assert!( stages[2]["family_id"].as_str().is_some(), "walk-forward stage links a family: {record_line}" ); // Emit-gated lines: per-member family_table lines (4 sweep members plus // the walk-forward OOS members) and at least one selection_report line. assert!( out.lines().filter(|l| l.starts_with("{\"family_id\":")).count() >= 4, "family_table member lines emitted: {out}" ); assert!( out.lines().any(|l| l.starts_with("{\"selection_report\":")), "selection_report line emitted: {out}" ); // The registry's new sibling store carries the realization record. assert!( runs_dir.join("campaign_runs.jsonl").is_file(), "campaign_runs.jsonl written beside runs.jsonl" ); } /// `run_campaign` resolves a target two ways — a readable file (register-then- /// run sugar) or a bare content id (direct store address) — then funnels BOTH /// through one shared post-resolution path: "fetch the stored canonical bytes /// by id ... so file addressing and id addressing produce the same /// realization by construction" (the driver's own doc comment). This pins /// that property observably: registering a document and then running it once /// by FILE and once by its own resulting content ID must refuse with the /// byte-identical prose — proof the two addressing modes are not two /// independently drifting code paths. No real data needed: both runs refuse /// at the (data-free) v1-pipeline-shape preflight. #[test] fn campaign_run_by_content_id_matches_file_sugar_refusal() { let _fixture = project_lock(); let dir = built_project(); 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("mc2.process.json")), ScratchPath::File(dir.join("mc2.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-id-seed"); 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"]); assert_eq!(file_code, Some(1), "stdout/stderr: {file_out}"); let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "mc2.campaign.json"]); assert_eq!(reg_code, Some(0), "register failed: {reg_out}"); let id = reg_out .lines() .find(|l| l.starts_with("registered campaign ")) .expect("register line") .trim_start_matches("registered campaign ") .split(' ') .next() .expect("id") .trim_start_matches("content:") .to_string(); let (id_out, id_code) = run_code_in(dir, &["campaign", "run", &id]); assert_eq!(id_code, Some(1), "stdout/stderr: {id_out}"); let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line"); let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line"); assert_eq!(file_line, id_line, "file- and id-addressed runs must refuse identically"); } /// #194 (the prefix trap): a `campaign run` target may carry the display /// `content:` prefix copied verbatim from the tool's own register/introspect /// output — the CLI strips it and resolves the bare store address. Pinned /// observably: running the SAME registered campaign by bare id and by its /// `content:`-prefixed form must refuse identically (proof the prefix is /// stripped, not read as a second, unresolvable address). Both refuse at the /// data-free v1 preflight (MC process), so no real data is needed. #[test] fn campaign_run_tolerates_content_prefix_on_target() { let _fixture = project_lock(); let dir = built_project(); 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("prefix.process.json")), ScratchPath::File(dir.join("prefix.campaign.json")), ]); let bp_id = seed_blueprint(dir, "campaign-run-prefix-seed"); let proc_id = register_process_doc(dir, "prefix.process.json", MC_PROCESS_DOC); write_doc(dir, "prefix.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", "")); let (reg_out, reg_code) = run_code_in(dir, &["campaign", "register", "prefix.campaign.json"]); assert_eq!(reg_code, Some(0), "register failed: {reg_out}"); let id = reg_out .lines() .find(|l| l.starts_with("registered campaign ")) .expect("register line") .trim_start_matches("registered campaign ") .split(' ') .next() .expect("id") .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 pfx_line = pfx_out.lines().find(|l| l.starts_with("aura: ")).expect("prefixed refusal line"); assert!( !pfx_line.contains("neither a readable .json file nor a 64-hex content id"), "a content:-prefixed target must resolve as the bare id, not refuse as an unknown target: {pfx_line}" ); let bare_line = bare_out.lines().find(|l| l.starts_with("aura: ")).expect("bare refusal line"); assert_eq!(bare_line, pfx_line, "bare-id and content:-prefixed runs must resolve identically"); } /// Property (#196 on-ramp x #198 executor): a blueprint registered through /// `aura graph register` — NOT the pre-#196 `aura sweep` side-effect /// `seed_blueprint` relies on elsewhere in this file — is a fully valid /// campaign strategy ref: its content id resolves through the referential /// gate, and its raw param space (`fast.length`/`slow.length`, the same /// names `graph introspect --params` reports for this fixture) binds against /// the campaign document's axes in `bind_axes`. This closes fieldtest F5 /// ("strategy refs unresolvable from the public surface — no blueprint /// register verb") end to end: the run reaches the member-data seam (the /// same [1, 2] 1970 window used elsewhere in this file for a deterministic, /// data-free refusal) rather than failing earlier at referencing or binding. #[test] fn campaign_run_accepts_a_graph_register_seeded_strategy() { let _fixture = project_lock(); let dir = built_project(); 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("onramp.process.json")), ScratchPath::File(dir.join("onramp.campaign.json")), ]); let open_bp = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR")); let (reg_out, reg_code) = run_code_in(dir, &["graph", "register", &open_bp]); assert_eq!(reg_code, Some(0), "graph register failed: {reg_out}"); let bp_id = reg_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(); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("no recorded geometry") || out.contains("no data for instrument"), "referencing + axis-binding must succeed, reaching the member-data \ seam rather than a referential or bind refusal: {out}" ); } /// `campaign run`'s file-sugar branch calls `parse_valid_campaign` directly /// (unwrapped), the SAME intrinsic validator `campaign register` calls but /// wraps in its own "refusing to register:" prefix. This pins that `run` /// does NOT inherit register's wrapper — an intrinsically invalid document /// refuses with the bare "campaign document invalid:" prose — and, like /// register, never touches the store for it. No project scaffolding, no /// data, no seeded blueprint needed: the refusal fires before any of that is /// read. #[test] fn campaign_run_invalid_file_refuses_before_touching_store() { // Run inside the built project (campaign run needs one, per the project // gate); the document fails intrinsic validation before any store or // referential check is reached, so the project's own store is untouched. let _fixture = project_lock(); let dir = built_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); let bad = CAMPAIGN_DOC.replacen(r#""values": [8]"#, r#""values": []"#, 1); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("aura: campaign document invalid:"), "run must surface the bare doc-tier prose, not register's wrapper: {out}" ); assert!( out.contains("axes.fast: an axis is a non-empty finite set"), "the doc-tier fault names the offending axis: {out}" ); assert!(!out.contains("refusing to register:"), "run must not reuse register's prefix: {out}"); assert!( !dir.join("runs").join("campaigns").exists(), "an invalid document must not create a store entry" ); }