//! 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}; mod common; use common::{ ScratchGuard, ScratchPath, fresh_project, fresh_project_with_data, fresh_project_with_gapped_data, }; /// 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 = Path::new(env!("CARGO_TARGET_TMPDIR")).join(format!("aura-cli-{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")); // The selection group (metric/select/deflate) is optional — a sweep with // no selection is a terminal, selection-free stage (#210). assert!(out.contains("optional")); 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"); // std::gate's "all" slot is required (unlike std::sweep's now-optional // selection group, #210), so it still exercises the pipeline-level open // slot listing alongside the top-level "name" slot. let partial = r#"{ "format_version": 1, "kind": "process", "pipeline": [ { "block": "std::gate" } ] }"#; 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].all")); } #[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. #207 (fieldtest /// 0108 F8) adds a fourth tag `generalize` on the rankable R-expectancy family /// (`expectancy_r`, `net_expectancy_r`, `sqn`, `sqn_normalized`) — exactly the /// names `aura_registry::check_r_metric` accepts, i.e. std::generalize's /// cross-instrument accept-set — so an author can read WHICH metrics generalize /// before authoring the stage. A rankable non-R metric (`total_pips`) and an /// R-denominated-but-unranked metric (`max_r_drawdown`) both stay OUT of it: /// the tag tracks `check_r_metric`, not the presence of `_r` in the name. /// 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}"); // #207: the four rankable R-expectancy names — std::generalize's // cross-instrument accept-set (aura_registry::check_r_metric) — carry the // `generalize` tag, so an author reads WHICH metrics generalize from the // roster instead of guessing at the stage. for name in ["expectancy_r", "net_expectancy_r", "sqn", "sqn_normalized"] { let l = line_for(name); assert!(l.contains("generalize"), "{name} must carry the generalize tag: {l}"); } // A rankable-but-not-R metric (total_pips) and an R-DENOMINATED-but-unranked // metric (max_r_drawdown, a drawdown IN R) both stay out of the generalize // set: the tag tracks check_r_metric, never the `_r` in the name. assert!(!tp.contains("generalize"), "total_pips must not carry generalize: {tp}"); let mrd = line_for("max_r_drawdown"); assert!(!mrd.contains("generalize"), "max_r_drawdown must not carry generalize: {mrd}"); } /// `--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 `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 (dir, _fixture) = fresh_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 blueprint into the project's own // store via a real sweep over its bound params, which the axes reopen // as overridable defaults (#246) (mirrors `project_load.rs`'s anchor // test). let closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let (sweep_out, sweep_code) = run_code_in( &dir, &[ "sweep", &closed_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 AND executable process into the same project store: // this test's OK campaign must pass all three validate tiers, and // PROCESS_DOC's sweep (deflate: true + plateau:worst) is intrinsically // valid but statically refused by the executor preflight (#205's third // tier) — so the referential fixture gets an argmax/no-deflate twin. let exec_process = PROCESS_DOC .replacen( "\"select\": \"plateau:worst\", \"deflate\": true", "\"select\": \"argmax\", \"deflate\": false", 1, ) // ... and its gate predicates over the annotation metric // overfit_probability (not per-member) flip to a per-member scalar. .replacen("\"overfit_probability\", \"cmp\": \"lt\", \"value\": 0.1", "\"win_rate\", \"cmp\": \"lt\", \"value\": 1.1", 1); assert_ne!(exec_process, PROCESS_DOC, "replacen must match the sweep stage"); write_doc(&dir, "seed.process.json", &exec_process); 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}"); } /// #210 risk-regime axis: a campaign document carrying a `risk` section /// round-trips through the CLI's actual `campaign validate` binary path /// (parse -> `validate_campaign`) without a fault. Before this iteration the /// `RiskRegime`/`CampaignDoc.risk` wiring was proven only inside /// `aura-research`'s own unit tests; this pins that the new field is actually /// reachable — and accepted when well-formed — at the CLI seam a data author /// drives, not merely inside the library. #[test] fn campaign_validate_accepts_a_valid_risk_section_end_to_end() { let dir = temp_cwd("campaign-validate-risk-ok"); let with_risk = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 1.5 } } ],", 1, ); assert_ne!(with_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "risk-ok.campaign.json", &with_risk); let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-ok.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!(out.contains("campaign document valid (intrinsic):"), "stdout/stderr: {out}"); assert!(!out.contains("BadRegime"), "Debug leak: {out}"); } /// #210: a non-positive stop regime (`k <= 0`, here `k: 0.0`) is refused by /// `validate_campaign`'s `DocFault::BadRegime` and — the property under test — /// that fault reaches the CLI's `doc_fault_prose` mapping end to end: exit 1, /// prose naming the offending index and BOTH malformed fields ("stop length /// must be >= 1 and k must be > 0"), never the bare `BadRegime` Debug variant. #[test] fn campaign_validate_refuses_bad_risk_regime_prose_exit_1() { let dir = temp_cwd("campaign-validate-risk-bad"); let with_bad_risk = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 0.0 } } ],", 1, ); assert_ne!(with_bad_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "risk-bad.campaign.json", &with_bad_risk); let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-bad.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("risk[0]: stop length must be >= 1 and k must be > 0"), "stdout/stderr: {out}" ); assert!(!out.contains("BadRegime"), "Debug leak: {out}"); } /// #262: the `vol_tf` regime variant is refused end-to-end by `campaign /// validate` on a non-positive `period_minutes`, the same CLI seam #210's /// `vol`-regime refusal test pins — before this, `RiskRegime::VolTf`'s /// `period_minutes < 1` check was proven only inside `aura-research`'s own /// unit tests (`validate_campaign_flags_non_positive_period_minutes_regime`); /// this pins that a malformed vol_tf regime is actually unreachable at the /// CLI seam a data author drives: exit 1, no bare `BadRegime` Debug leak. #[test] fn campaign_validate_refuses_bad_vol_tf_regime_prose_exit_1() { let dir = temp_cwd("campaign-validate-vol-tf-bad"); let with_bad_risk = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"risk\": [ { \"vol_tf\": { \"period_minutes\": 0, \"length\": 3, \"k\": 1.5 } } ],", 1, ); assert_ne!(with_bad_risk, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "risk-bad-vol-tf.campaign.json", &with_bad_risk); let (out, code) = run_code_in(&dir, &["campaign", "validate", "risk-bad-vol-tf.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("risk[0]:"), "stdout/stderr: {out}"); assert!(!out.contains("BadRegime"), "Debug leak: {out}"); assert!(!out.contains("VolTf"), "Debug leak: {out}"); } /// #216: the intrinsic validate summary reports the realized matrix — the /// exec cross-product strategies × instruments × windows × regimes — so an /// 8-cell document no longer reads as regime-less. Points (the axis grid) are /// deliberately NOT a cell factor: 2 points here, yet 8 cells. #[test] fn campaign_validate_counts_regimes_and_cells() { let dir = temp_cwd("campaign-validate-cells"); let doc = r#"{ "format_version": 1, "kind": "campaign", "name": "screen", "data": { "instruments": ["GER40", "FRA40"], "windows": [ { "from_ms": 1, "to_ms": 2 }, { "from_ms": 2, "to_ms": 3 } ] }, "risk": [ { "vol": { "length": 3, "k": 1.5 } }, { "vol": { "length": 3, "k": 3.0 } } ], "strategies": [ { "ref": { "content_id": "9f3a" }, "axes": { "fast": { "kind": "I64", "values": [8, 16] } } } ], "process": { "ref": { "content_id": "4e2d" } }, "seed": 1, "presentation": { "persist_taps": [], "emit": ["family_table"] } }"#; write_doc(&dir, "cells.campaign.json", doc); let (out, code) = run_code_in(&dir, &["campaign", "validate", "cells.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains("campaign document valid (intrinsic): 1 strategy(ies), 1 axes (2 points), 2 instrument(s), 2 window(s), 2 regime(s) — 8 cell(s)"), "stdout/stderr: {out}" ); } /// #216: a risk-less document counts its implicit default regime honestly — /// one regime, marked "(default)" — instead of reading as regime-less. #[test] fn campaign_validate_counts_the_default_regime() { let dir = temp_cwd("campaign-validate-default-regime"); 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): 1 strategy(ies), 1 axes (1 points), 1 instrument(s), 1 window(s), 1 regime(s) (default) — 1 cell(s)"), "stdout/stderr: {out}" ); } #[test] fn campaign_introspect_vocabulary_lists_sections() { let (out, code) = run_code(&["campaign", "introspect", "--vocabulary"]); assert_eq!(code, Some(0)); // #216: std::risk joins the roster the CLI's own --vocabulary listing // enumerates — the regime axis is discoverable from the public surface, // not only from the design ledger or a generated document. for id in ["std::data", "std::strategy", "std::process_ref", "std::presentation", "std::risk", "std::cost"] { assert!(out.contains(id), "vocabulary misses {id}: {out}"); } } /// #216: `--block std::risk` (the CLI's own introspection, not the /// `aura-research` library directly) names the regime shape and its /// absent-means-default semantics — the second half of "the risk-regime axis /// is undiscoverable from the ... introspection surface" (mirrors the /// existing `--block std::presentation` / tap-vocabulary pin). #[test] fn campaign_introspect_block_risk_describes_the_regime_shape() { let (out, code) = run_code(&["campaign", "introspect", "--block", "std::risk"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains("list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime"), "the block description must name the regime shape: {out}" ); assert!( out.contains("optional, list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime"), "the risk slot must render as optional: {out}" ); assert!( out.contains("std::risk — campaign section: the structural risk axis"), "stdout/stderr: {out}" ); } /// #234: `--block std::cost` names the component shape and its /// absent-means-zero-costs semantics — the cost model is discoverable from /// the public introspection surface (the `std::risk` #216 precedent). #[test] fn campaign_introspect_block_cost_describes_the_component_shape() { let (out, code) = run_code(&["campaign", "introspect", "--block", "std::cost"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains("optional, list of cost components { constant: { cost_per_trade } } | { vol_slippage: { slip_vol_mult } } | { carry: { carry_per_cycle } }; absent = zero costs (net == gross)"), "the cost slot must render as optional with the component shape: {out}" ); assert!( out.contains("std::cost — campaign section: the cost model"), "stdout/stderr: {out}" ); } /// #216: a complete-but-risk-less campaign document (the exact shape a /// verb-generated document has before a `--stop-*` flag is added) has its one /// remaining open slot — the optional risk axis — enumerated by the public /// `campaign introspect --unwired` CLI surface. Before this iteration the /// slot existed only in the `aura-research` library's own unit tests; a /// consumer with just the built binary had no way to discover it (#216's /// exact complaint: "never lists the risk slot"). #[test] fn campaign_introspect_unwired_lists_the_risk_slot_when_absent() { let dir = temp_cwd("campaign-introspect-unwired-risk"); write_doc(&dir, "c.campaign.json", CAMPAIGN_DOC); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "c.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains( "open slot: risk (optional, list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime)" ), "stdout/stderr: {out}" ); } /// #216: the bare-envelope probe (`{}` — the issue's exact reproduction) /// names the optional risk axis alongside the required envelope slots. #[test] fn campaign_introspect_unwired_lists_the_optional_risk_slot_on_bare_envelope() { let dir = temp_cwd("campaign-introspect-unwired-risk-bare"); write_doc(&dir, "bare.json", "{}"); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "bare.json"]); assert_eq!(code, Some(0)); assert!( out.contains("open slot: risk (optional, list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime)"), "stdout/stderr: {out}" ); } /// #216/#234: bound (non-empty) risk AND cost lists close both optional /// slots — a complete document with both reports no open slots at all. #[test] fn campaign_introspect_unwired_omits_bound_risk_and_cost_slots() { let dir = temp_cwd("campaign-introspect-unwired-risk-bound"); let with_both = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } } ],\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } } ],", 1, ); assert_ne!(with_both, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "bound.campaign.json", &with_both); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "bound.campaign.json"]); assert_eq!(code, Some(0)); assert!(!out.contains("open slot: risk"), "bound risk must not list: {out}"); assert!(!out.contains("open slot: cost"), "bound cost must not list: {out}"); assert!(out.contains("no open slots"), "stdout/stderr: {out}"); } /// #216: an EXPLICIT `"risk": []` (key present, array empty) is still an open /// slot — same as the key being absent entirely. The probe distinguishes /// "bound" from "unbound" by non-emptiness, not by key presence /// (`v.get("risk").and_then(|r| r.as_array()).is_some_and(|a| !a.is_empty())`); /// a probe that instead branched on `is_none()` would wrongly treat an /// explicit empty list as closed, and no other test in this file exercises /// that branch (the absent-key and non-empty-array cases are covered above). #[test] fn campaign_introspect_unwired_lists_the_risk_slot_when_explicitly_empty() { let dir = temp_cwd("campaign-introspect-unwired-risk-explicit-empty"); let explicit_empty = CAMPAIGN_DOC.replacen("\"seed\": 1,", "\"seed\": 1,\n \"risk\": [],", 1); assert_ne!(explicit_empty, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "empty-risk.campaign.json", &explicit_empty); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "empty-risk.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains( "open slot: risk (optional, list of stop regimes { vol: { length, k } } | { vol_tf: { period_minutes, length, k } }; absent = one default regime)" ), "an explicit empty risk array must still count as an open slot: {out}" ); } /// #265: the cost and risk slots name their knobs but not their UNITS or /// SEMANTICS, and the two natural misreadings are silent (wrong by orders of /// magnitude / change an experiment's meaning, with no error). Property: when /// `campaign introspect --unwired` lists the open cost and risk slots, each of /// the four silently-misreadable knobs carries a one-line unit/semantics note /// that blocks its misreading — `constant.cost_per_trade` is in PRICE units /// (charged per close as cost/|entry−stop| in R), `vol_slippage.slip_vol_mult` /// is a MULTIPLIER on the per-cycle vol estimate (charged in R likewise), /// `vol.length` SMOOTHS the estimator and does NOT set the stop's timescale, /// and `vol.k` SCALES the stop distance. Autonomous: a bare `{}` envelope /// (both slots open) written inline, no project, hostless. Wording stays free /// (matched case-insensitively on load-bearing fragments, not whole lines); it /// fails today because the annotations are absent, not on scaffolding. #[test] fn campaign_introspect_unwired_annotates_cost_and_risk_knob_units() { let dir = temp_cwd("campaign-introspect-unwired-knob-units"); write_doc(&dir, "bare.json", "{}"); let (out, code) = run_code_in(&dir, &["campaign", "introspect", "--unwired", "bare.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); let low = out.to_lowercase(); // constant.cost_per_trade — price units, not R (the order-of-magnitude trap). assert!(low.contains("cost_per_trade"), "the cost knob must still be named: {out}"); assert!( low.contains("price units"), "cost_per_trade must be annotated as a PRICE-unit numerator (its misreading is a cost in R): {out}" ); // vol_slippage.slip_vol_mult — a multiplier on the per-cycle vol estimate. assert!(low.contains("slip_vol_mult"), "the vol-slippage knob must still be named: {out}"); assert!( low.contains("multiplier on the per-cycle vol"), "slip_vol_mult must be annotated as a MULTIPLIER on the per-cycle vol estimate: {out}" ); // vol.length — smooths the estimator; it does NOT set the stop's timescale. assert!( low.contains("smooths"), "vol.length must be annotated as SMOOTHING the per-cycle vol estimator: {out}" ); assert!( low.contains("does not set the stop's timescale"), "vol.length must state it does NOT set the stop's timescale (its exact misreading): {out}" ); // vol.k — scales the stop distance (the risk-unit lever). assert!( low.contains("scales the stop distance"), "vol.k must be annotated as SCALING the stop distance (the risk-unit lever): {out}" ); // vol_tf — period_minutes IS the timescale (the #262 knob the vol notes // say vol.length is NOT). assert!(low.contains("vol_tf"), "the vol_tf regime must be annotated: {out}"); assert!( low.contains("period_minutes is the stop's timescale"), "vol_tf must state that period_minutes sets the timescale: {out}" ); // carry.carry_per_cycle — the identical price-unit-vs-R trap, accruing per // held cycle (audit follow-up to #265: the note set must cover ALL three // cost components, not two of three). assert!(low.contains("carry_per_cycle"), "the carry knob must still be named: {out}"); assert!( low.contains("price units per held cycle"), "carry_per_cycle must be annotated as a PRICE-unit per-held-cycle accrual: {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 intrinsic tier value-checks `data.bindings` (no project, no store, /// #231): a value outside the closed column vocabulary refuses /// path-addressed, naming the vocabulary. NOT gated. #[test] fn campaign_validate_refuses_an_unknown_binding_column() { let dir = temp_cwd("campaign-validate-bad-binding"); let doc = r#"{ "format_version": 1, "kind": "campaign", "name": "bad-binding", "data": { "bindings": { "price": "hl2" }, "instruments": ["GER40"], "windows": [ { "from_ms": 1, "to_ms": 2 } ] }, "strategies": [ { "ref": { "content_id": "9f3a" }, "axes": { "slow": { "kind": "I64", "values": [10] } } } ], "process": { "ref": { "content_id": "9f3a" } }, "seed": 1, "presentation": { "persist_taps": [], "emit": [] } }"#; write_doc(&dir, "bad.campaign.json", doc); let (out, code) = run_code_in(&dir, &["campaign", "validate", "bad.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains(r#"data.bindings.price: "hl2" names no archive column"#), "the refusal is path-addressed and names the vocabulary: {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}" ); } /// 0109 task 1 (#201 decision 1): the closed tap vocabulary is discoverable /// through the actual CLI binary, not just the `aura-research` library — /// `campaign introspect --block std::presentation` composes /// `describe_block` + `slot_kind_label` over the new `SlotKind::TapKinds`, /// and this pins that composition reaches stdout with the exact four names, /// so a future edit to `tap_vocabulary()` that forgets the CLI-visible label /// (there is no other CLI test exercising `--block std::presentation`) fails /// here instead of only in a library-level unit test. #[test] fn campaign_introspect_block_presentation_names_the_tap_vocabulary() { let (out, code) = run_code(&["campaign", "introspect", "--block", "std::presentation"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); assert!( out.contains("list of: equity | exposure | r_equity | net_r_equity"), "the block description must name the closed tap vocabulary: {out}" ); assert!(out.contains("persist_taps"), "stdout/stderr: {out}"); } /// #193 (narrowed scope, cycle-0107 F3): the whole document envelope IS /// derivable — `introspect --unwired` over a bare `{}` enumerates every /// top-level field (seed, data, strategies, process.ref, presentation) as an /// open slot — but nothing pointed the author there, so the 0106 round /// iterated blind `validate` errors instead of starting from the skeleton. /// The shared `--unwired` help line now carries the skeleton hint naming that /// starting move, making it discoverable from `--help` alone (the process and /// campaign families share `DocIntrospectCmd`, so pinning one covers both). /// Whitespace is normalized before matching so clap's help-column wrapping /// (terminal-width dependent) can never split the pinned phrase. #[test] fn introspect_help_hints_the_bare_envelope_probe() { let (out, code) = run_code(&["campaign", "introspect", "--help"]); assert_eq!(code, Some(0), "clap --help exits 0: {out}"); let flat = out.split_whitespace().collect::>().join(" "); assert!( flat.contains("start from a bare {}"), "the --unwired help line must point at the bare-{{}} envelope probe: {out}" ); } /// #193 (narrowed scope, cycle-0106 F1): a document whose `kind` key carries /// the wrong value — the blind-author trap of pointing `process validate` at /// a campaign-kind file — is refused with prose that NAMES the `kind` key the /// author must fix ("the \"kind\" key must be \"process\""), instead of the /// opaque "wrong document kind" that never told them WHICH field to change. /// Autonomous: a minimal campaign-kind envelope written inline, refused at the /// envelope check before any deserialization (exit 1), Debug-free. #[test] fn wrong_kind_refusal_names_the_kind_key() { let dir = temp_cwd("wrong-kind-names-key"); write_doc( &dir, "mislabeled.process.json", r#"{ "format_version": 1, "kind": "campaign", "name": "x", "pipeline": [] }"#, ); let (out, code) = run_code_in(&dir, &["process", "validate", "mislabeled.process.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("the \"kind\" key must be \"process\""), "the refusal must name the kind key the author must fix: {out}" ); assert!(!out.contains("WrongKind"), "Debug leak: {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" ); } /// #210: the register validate-gate extends to the new risk axis — a /// document whose only fault is a non-positive stop regime is refused with /// prose (exit 1) and, the property that matters for a content-addressed /// store, no file is ever written under `runs/campaigns/`. A store that /// accepted a document because register only checked the OLD fault kinds /// would silently persist an unenforceable risk regime. #[test] fn campaign_register_refuses_invalid_risk_section_and_writes_nothing() { let dir = temp_cwd("campaign-register-invalid-risk"); let bad = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"risk\": [ { \"vol\": { \"length\": 0, \"k\": 2.0 } } ],", 1, ); assert_ne!(bad, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "bad-risk.campaign.json", &bad); let (out, code) = run_code_in(&dir, &["campaign", "register", "bad-risk.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("refusing to register:"), "stdout/stderr: {out}"); assert!( out.contains("risk[0]: stop length must be >= 1 and k must be > 0"), "stdout/stderr: {out}" ); assert!( !dir.join("runs").join("campaigns").exists(), "register must not create a store entry for an invalid risk section" ); } /// #234: the register validate-gate extends to the cost model — a document /// whose only fault is a negative cost knob is refused with prose (exit 1) /// and no file is ever written under `runs/campaigns/` (the risk-section /// precedent). #[test] fn campaign_register_refuses_invalid_cost_section_and_writes_nothing() { let dir = temp_cwd("campaign-register-invalid-cost"); let bad = CAMPAIGN_DOC.replacen( "\"seed\": 1,", "\"seed\": 1,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": -2.0 } } ],", 1, ); assert_ne!(bad, CAMPAIGN_DOC, "replacen must actually match the fixture's seed field"); write_doc(&dir, "bad-cost.campaign.json", &bad); let (out, code) = run_code_in(&dir, &["campaign", "register", "bad-cost.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("refusing to register:"), "stdout/stderr: {out}"); assert!( out.contains("cost[0]: the component's price-unit knob must be finite and >= 0"), "stdout/stderr: {out}" ); assert!( !dir.join("runs").join("campaigns").exists(), "register must not create a store entry for an invalid cost section" ); } /// 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 (the referential test's recipe). 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", "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 (annotator /// stages in any order included) must always register; only `campaign run`'s /// preflight draws the executable-shape 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.). /// [`campaign_doc_json`] with the instrument spliced — the synthetic-archive /// e2e targets `SYMA` (tests/common/mod.rs) instead of the hardcoded GER40. fn campaign_doc_json_for( instrument: &str, 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": ["{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": [{persist_taps}], "emit": [{emit}] }} }}"#, from = window.0, to = window.1, ) } fn campaign_doc_json( bp_id: &str, proc_id: &str, window: (i64, i64), persist_taps: &str, emit: &str, ) -> String { campaign_doc_json_for("GER40", bp_id, proc_id, window, persist_taps, emit) } /// [`campaign_doc_json_for`] over TWO instruments plus a verbatim cost block /// (#260) — the per-instrument-cost e2es need both synthetic symbols in one /// document. `cost` is spliced verbatim (a full `"cost": [...],` line /// including the trailing comma, or `""` for none). fn campaign_doc_json_two( instruments: (&str, &str), cost: &str, bp_id: &str, proc_id: &str, window: (i64, i64), ) -> String { format!( r#"{{ "format_version": 1, "kind": "campaign", "name": "run-seam", "data": {{ "instruments": ["{a}", "{b}"], "windows": [ {{ "from_ms": {from}, "to_ms": {to} }} ] }}, "strategies": [ {{ "ref": {{ "content_id": "{bp_id}" }}, "axes": {{ "fast.length": {{ "kind": "I64", "values": [2] }}, "slow.length": {{ "kind": "I64", "values": [8] }} }} }} ], "process": {{ "ref": {{ "content_id": "{proc_id}" }} }}, {cost}"seed": 7, "presentation": {{ "persist_taps": [], "emit": ["family_table"] }} }}"#, a = instruments.0, b = instruments.1, from = window.0, to = window.1, ) } /// An mc-bearing process: intrinsically valid AND (since the v2 executor) an /// executable pipeline shape — `sweep -> monte_carlo` annotates each sweep /// survivor. The two addressing-mode tests below run it over the [1, 2] 1970 /// window, so they refuse deterministically at the member-data seam. 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 } ] }"#; /// Intrinsically valid but NOT an executable v2 shape: the annotator sits /// before the population stage (`walk_forward` after `monte_carlo`). The /// intrinsic tier deliberately permits this order (the shipped /// `validate_process_permits_walk_forward_after_an_annotator` pin in /// aura-research), so register accepts it; only `campaign run`'s preflight /// refuses it. const MC_BEFORE_WF_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "mc-before-wf", "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": "sqn_normalized", "select": "argmax" } ] }"#; /// An executable generalize-bearing shape (`sweep -> generalize`). The /// refusal under test is the campaign-side static guard (std::generalize /// needs >= 2 instruments), not the pipeline shape. const GENERALIZE_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "gen-screen", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::generalize", "metric": "net_expectancy_r" } ] }"#; /// The minimal executable 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 annotated shape for the gated e2e: sweep -> gate -> walk_forward /// -> monte_carlo. The gate (`n_trades ge 0`) passes every member, so /// walk-forward always has survivors; the mc annotator bootstraps the pooled /// per-window OOS trade-R series (200 resamples, block 5). Roller: 14d IS / /// 7d OOS / 7d step in epoch-ms, tiling the ~30-day GER40 Sept-2024 campaign /// window. NO generalize stage: this campaign has ONE instrument (a second /// local archive is not guaranteed), and the single-instrument refusal is /// pinned data-free in `campaign_run_refuses_single_instrument_generalize`. 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" }, { "block": "std::monte_carlo", "resamples": 200, "block_len": 5 } ] }"#; /// `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 (dir, _fixture) = fresh_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 (dir, _fixture) = fresh_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}" ); } /// Property: the recorded campaign realizations are READABLE BACK. A read-back /// verb (`aura campaign runs`) both lists every stored campaign-run record on a /// scannable line carrying its campaign id, and — addressed by that id — dumps /// the BARE persisted store record (the `campaign_runs.jsonl` line form), NOT /// the `{"campaign_run": …}` stdout wrapper the `campaign run` emit uses (F10, /// fieldtest 0108). Today `runs` is an unrecognized campaign subcommand (clap /// exit 2) — the feature is absent. /// /// Hermetic seed: one hand-written record line in the exact bare store form /// serde emits (mirroring the aura-registry pre-widening parse line — /// canonical field order, sparse-skip fields omitted — so a re-serialized dump /// is byte-identical). Appending a real record needs a full campaign run over /// data; hand-writing the store line is the only data-free path and pins the /// bare-store-line contract exactly. #[test] fn campaign_runs_lists_and_dumps_the_bare_stored_record() { let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); std::fs::create_dir_all(&runs_dir).expect("create runs dir"); let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone())]); let campaign_id = "deadbeef".repeat(8); // a 64-hex content id let record_line = format!( r#"{{"campaign":"{campaign_id}","process":"beef","run":0,"seed":42,"cells":[{{"strategy":"3f9c","instrument":"GER40","window_ms":[1725148800000,1727740799999],"stages":[{{"block":"std::sweep","family_id":"sweep-0"}},{{"block":"std::gate","survivor_ordinals":[0,1,2,3]}}]}}]}}"# ); std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n")) .expect("seed the campaign-run store"); // LIST mode: one scannable line per stored record, carrying the campaign // id (bare 64-hex or a prefix), exit 0. let (list_out, list_code) = run_code_in(&dir, &["campaign", "runs"]); assert_eq!(list_code, Some(0), "list exits 0: {list_out}"); assert!( list_out.contains(&campaign_id[..8]), "the list line carries the campaign id (bare or prefix): {list_out}" ); // DUMP mode: addressed by the campaign content id, prints the BARE stored // record line — the store form, NOT the `{{"campaign_run": …}}` run-emit // wrapper (F10) — byte-identical to the seeded line, exit 0. let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]); assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}"); assert!( !dump_out.contains("\"campaign_run\""), "dump prints the bare store record, not the run-emit wrapper (F10): {dump_out}" ); assert_eq!( dump_out.trim(), record_line, "dump is byte-identical to the bare stored line (F10): {dump_out}" ); } /// 0109 task 2: `campaign runs ` dumps by deserializing the stored line /// into `CampaignRunRecord` and re-serializing it (`research_docs.rs`'s /// `campaign_runs` — "serde round-trip == the stored bytes"), so the /// byte-identical contract `campaign_runs_lists_and_dumps_the_bare_stored_record` /// pins above is only proven for a record where `trace_name` is absent /// (pre-0109 shape). This is the same round-trip pinned with the new field /// POPULATED: a stored line carrying `"trace_name":"deadbeef-3"` must dump /// back byte-identical — the new field's declaration position (after /// `generalizations`) and its `skip_serializing_if` must not perturb the /// existing sparse-skip / declaration-order contract the read-back verb /// depends on. #[test] fn campaign_runs_dump_round_trips_a_populated_trace_name() { let (dir, _fixture) = fresh_project(); let runs_dir = dir.join("runs"); std::fs::remove_dir_all(&runs_dir).ok(); std::fs::create_dir_all(&runs_dir).expect("create runs dir"); let _cleanup = ScratchGuard(vec![ScratchPath::Dir(runs_dir.clone())]); let campaign_id = "deadbeef".repeat(8); let record_line = format!( r#"{{"campaign":"{campaign_id}","process":"beef","run":3,"seed":42,"cells":[{{"strategy":"3f9c","instrument":"GER40","window_ms":[1725148800000,1727740799999],"stages":[{{"block":"std::sweep","family_id":"sweep-0"}}]}}],"trace_name":"deadbeef-3"}}"# ); std::fs::write(runs_dir.join("campaign_runs.jsonl"), format!("{record_line}\n")) .expect("seed the campaign-run store"); let (dump_out, dump_code) = run_code_in(&dir, &["campaign", "runs", &campaign_id]); assert_eq!(dump_code, Some(0), "dump exits 0: {dump_out}"); assert_eq!( dump_out.trim(), record_line, "a populated trace_name round-trips byte-identical through the read-back verb: {dump_out}" ); } /// The v2 boundary, order side: `[sweep, monte_carlo, walk_forward]` is /// intrinsically valid — `process register` ACCEPTS it (asserted inside /// `register_process_doc`; the intrinsic-tier ground is the shipped /// `validate_process_permits_walk_forward_after_an_annotator` pin) — but the /// executable pipeline shape fixes the suffix order (`std::sweep [std::gate]* /// [std::walk_forward]? [std::monte_carlo]? [std::generalize]?`), so /// `campaign run` refuses it at the data-free preflight, before any member /// runs, with Debug-free PipelineShape prose. #[test] fn campaign_run_refuses_mc_before_walk_forward() { let (dir, _fixture) = fresh_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("mcwf.process.json")), ScratchPath::File(dir.join("mcwf.campaign.json")), ]); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}"); assert!( out.contains("std::walk_forward cannot follow std::monte_carlo"), "the detail names the ordering violation: {out}" ); assert!(!out.contains("not executable in v1"), "retired v1 prose resurfaced: {out}"); assert!(!out.contains("PipelineShape"), "Debug leak: {out}"); } /// A v2 process doc whose `std::sweep` carries no selection group (no /// `metric`/`select`) followed by a `std::gate` stage: intrinsically valid /// (`process register` accepts a selection-free sweep as any other stage /// shape) but not an executable v2 shape, since a selection-free sweep is /// permitted ONLY as the terminal stage of its process (#210's terminal /// rule — the executor's preflight rule, not the intrinsic validator's). const SELECTION_FREE_SWEEP_THEN_GATE_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "sweep-then-gate-no-selection", "pipeline": [ { "block": "std::sweep" }, { "block": "std::gate", "all": [ { "metric": "n_trades", "cmp": "ge", "value": 0.0 } ] } ] }"#; /// Property (#210, sweep-dissolution-a): a selection-free `std::sweep` (no /// metric/select group) anywhere but the LAST stage of its process is /// refused by `campaign run`'s data-free preflight — the same seam that /// already refuses `[sweep, monte_carlo, walk_forward]`'s ordering fault /// above — before any member runs (the [1, 2] 1970-epoch-ms window is never /// reached), naming the offending stage index, Debug-free. A regression /// that let a non-terminal selection-free sweep through would silently /// drop a stage's nominee downstream (nothing to select a winner from, /// nothing for a following stage to consume). #[test] fn campaign_run_refuses_a_non_terminal_selection_free_sweep() { let (dir, _fixture) = fresh_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("selfree.process.json")), ScratchPath::File(dir.join("selfree.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-selfree-seed"); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains( "process stage 0: a sweep without a selection group (metric + select) must be \ the last stage of its process" ), "the detail names the terminal-only rule: {out}" ); assert!(!out.contains("SelectionFreeSweepNotTerminal"), "Debug leak: {out}"); } /// The same process shape, but the selection-free sweep IS the (only, hence /// terminal) stage: `campaign validate`'s third (executor-preflight) tier /// accepts it, data-free, exit 0 — the selection-free arm is a legitimate /// executable shape, not merely a document-tier-only accept. Pairs with the /// refusal above: together they pin the exact placement boundary (terminal: /// yes / anywhere else: no) the executor preflight enforces. const SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "sweep-only-no-selection", "pipeline": [ { "block": "std::sweep" } ] }"#; #[test] fn campaign_validate_accepts_a_terminal_selection_free_sweep() { let (dir, _fixture) = fresh_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("selfreeonly.process.json")), ScratchPath::File(dir.join("selfreeonly.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-validate-selfreeonly-seed"); let proc_id = register_process_doc(&dir, "selfreeonly.process.json", SELECTION_FREE_SWEEP_ONLY_PROCESS_DOC); write_doc( &dir, "selfreeonly.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""), ); let (out, code) = run_code_in(&dir, &["campaign", "validate", "selfreeonly.campaign.json"]); assert_eq!(code, Some(0), "a terminal selection-free sweep validates as executable: {out}"); assert!( out.contains("campaign document valid (executable): pipeline shape and static guards pass"), "stdout/stderr: {out}" ); } /// A hand-authored (non-sugar) `[std::grid, std::walk_forward]` process — the /// #256 fork B shape the `walkforward`/`mc` verb sugar now generates /// internally, but authored here directly as an op-script the way a data-only /// project author would reach it. 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" } ] }"#; /// The same leading `std::grid` stage, but with a `std::gate` interposed /// before `std::walk_forward` — the one placement `campaign run`'s preflight /// must refuse (#256 fork B: a `std::grid` first stage yields bare parameter /// points, not executed member reports, so it is legal ONLY immediately /// before `std::walk_forward`). const GRID_THEN_GATE_THEN_WF_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "grid-then-gate-then-walkforward", "pipeline": [ { "block": "std::grid" }, { "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" } ] }"#; /// Property (#256 fork B): `campaign run`'s data-free preflight, exercised /// through the real CLI binary over a hand-authored op-script (not the /// verb-sugar translation), refuses a `std::grid` first stage that is not /// immediately followed by `std::walk_forward` — here, a `std::gate` /// interposed between them — before any member runs (the [1, 2] 1970-epoch-ms /// window is never reached). A regression that let this placement through /// would hand `std::gate` bare parameter points instead of the executed /// member reports it retains on, silently corrupting the gate's survivor /// filter. The detail is Debug-free prose, not the `PipelineShape`/`ExecFault` /// identifier. #[test] fn campaign_run_refuses_a_grid_stage_not_immediately_before_walk_forward() { let (dir, _fixture) = fresh_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("gridgate.process.json")), ScratchPath::File(dir.join("gridgate.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-gridgate-seed"); let proc_id = register_process_doc( &dir, "gridgate.process.json", GRID_THEN_GATE_THEN_WF_PROCESS_DOC, ); write_doc( &dir, "gridgate.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1, 2), "", ""), ); let (out, code) = run_code_in(&dir, &["campaign", "run", "gridgate.campaign.json"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("process pipeline is not executable:"), "stdout/stderr: {out}"); assert!( out.contains( "a std::grid first stage must be immediately followed by std::walk_forward" ), "the detail names the adjacency rule: {out}" ); assert!(!out.contains("PipelineShape"), "Debug leak: {out}"); } /// Property (#256 fork B): a hand-authored `[std::grid, std::walk_forward]` /// op-script — not the verb-sugar translation, which already pins this shape /// in `cli_run.rs` — runs to completion over the synthetic SYMA archive, and /// the persisted `CampaignRunRecord`'s leading `std::grid` stage carries no /// `family_id`: the enumerate-only leading stage crosses the stage seam as /// bare points and never executes or persists a family of its own, while the /// following `std::walk_forward` stage does. (`aura runs families` is not /// used here: `seed_blueprint` itself runs a real `aura sweep` to register /// the blueprint, which stamps its own incidental Sweep family and would /// confound a registry-wide family count — the record's own stage array is /// the unconfounded source of truth.) A regression that made a hand-authored /// `std::grid` stage execute or persist a family would flip /// `stages[0]["family_id"]` from null here even though the sugar-generated /// shape (a distinct code path) stayed green. #[test] fn campaign_run_synthetic_e2e_grid_then_wf_persists_no_sweep_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("gridwf.process.json")), ScratchPath::File(dir.join("gridwf.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-gridwf-seed"); let proc_id = register_process_doc(&dir, "gridwf.process.json", GRID_THEN_WF_PROCESS_DOC); // SYMA's synthetic archive spans 2024-01..08; Mar-01..Jun-30 (~17 weeks) // gives the (14d, 7d, 7d) roller a comfortable tiling (the cost-e2e // siblings' window). write_doc( &dir, "gridwf.campaign.json", &campaign_doc_json_for( "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "", "", ), ); let (out, code) = run_code_in(&dir, &["campaign", "run", "gridwf.campaign.json"]); assert_eq!(code, Some(0), "campaign run failed: {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"); let stages = &v["campaign_run"]["cells"][0]["stages"]; assert_eq!(stages[0]["block"], "std::grid"); assert!(stages[0]["family_id"].is_null(), "std::grid persists no family: {stages}"); assert_eq!(stages[1]["block"], "std::walk_forward"); assert!(!stages[1]["family_id"].is_null(), "std::walk_forward persists its family: {stages}"); } /// The v2 boundary, campaign side: a generalize-bearing process is an /// executable shape, but `std::generalize` needs >= 2 instruments in the /// campaign — a STATIC preflight fact (the referential gate has run, no data /// is read), so the single-GER40 fixture campaign refuses with exit 1 before /// any member runs. This is the ONE home for the single-instrument refusal; /// the gated real-data e2e does not duplicate it (its campaign is also /// single-instrument, and this refusal is data-free by construction). #[test] fn campaign_run_refuses_single_instrument_generalize() { let (dir, _fixture) = fresh_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("gen1.process.json")), ScratchPath::File(dir.join("gen1.campaign.json")), ]); 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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!( out.contains("std::generalize needs at least 2"), "the refusal names the block and the minimum: {out}" ); assert!(out.contains("1 instrument"), "the refusal names the available count: {out}"); assert!(!out.contains("GeneralizeNeedsInstruments"), "Debug leak: {out}"); } /// An executable generalize-bearing shape whose metric is a pip metric, not /// an R metric (`total_pips` is a known, registered scalar — `process /// register` accepts it — but not R-denominated). const GENERALIZE_NON_R_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "gen-pip-screen", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::generalize", "metric": "total_pips" } ] }"#; /// The v2 boundary, metric side: `std::generalize`'s metric guard /// (`check_r_metric`) is a STATIC preflight fact independent of the /// instrument-arity guard above it in the same match arm — with >= 2 /// campaign instruments (so arity passes) and a pip metric, `campaign run` /// must still refuse data-free, naming the metric, and never fall through to /// the instrument-count prose or a Debug-formatted fault. A regression that /// let the arity check short-circuit past the metric check (or vice versa) /// would silently accept a pip-denominated `std::generalize`. #[test] fn campaign_run_refuses_generalize_non_r_metric() { let (dir, _fixture) = fresh_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("genpip.process.json")), ScratchPath::File(dir.join("genpip.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-genpip-seed"); let proc_id = register_process_doc(&dir, "genpip.process.json", GENERALIZE_NON_R_PROCESS_DOC); // Two instruments (arity passes), so the metric guard is the one that // actually fires. The [1, 2] 1970 window never matters: this refusal is // data-free, before any member runs. let campaign_doc = format!( r#"{{ "format_version": 1, "kind": "campaign", "name": "run-seam-genpip", "data": {{ "instruments": ["GER40", "EURUSD"], "windows": [ {{ "from_ms": 1, "to_ms": 2 }} ] }}, "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": [] }} }}"# ); write_doc(&dir, "genpip.campaign.json", &campaign_doc); let (out, code) = run_code_in(&dir, &["campaign", "run", "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 // R-denominated yet unranked, so "pip metric" was always wrong. assert!( out.contains("std::generalize metric \"total_pips\" is not in the rankable R-expectancy family"), "the refusal names the offending metric and the real rule: {out}" ); assert!(!out.contains("2 instrument"), "the arity guard must not fire when arity is fine: {out}"); assert!(!out.contains("GeneralizeNonRMetric"), "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 (dir, _fixture) = fresh_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}"); } /// Property (#205, fieldtest 0108 F6): `aura campaign validate` runs the /// executor's data-free static preflight (`aura_campaign::preflight`) as a /// THIRD tier, so a pure process-shape fault that needs no data is caught at /// validate time, not only at `campaign run` time — "valid" now means /// "runnable". Two faces of the one behaviour, both inside a project (the /// tier needs the store to fetch the referenced process, like the referential /// tier above it): /// - an executable pipeline shape gains a third line, `campaign document /// valid (executable): pipeline shape and static guards pass`, exit 0; /// - a `[sweep, monte_carlo, walk_forward]` process — intrinsically valid /// (`process register` accepts it) and referentially valid (its ref /// resolves) yet not an executable shape — refuses at VALIDATE with the /// executor fault header `campaign is not executable:` and the /// `exec_fault_prose` shape detail, exit 1, before any member runs. /// /// A regression that dropped the third tier would let an executor-unrunnable /// shape validate clean (exit 0), the F6 friction. #[test] fn campaign_validate_runs_the_executor_preflight_as_a_third_tier() { let (dir, _fixture) = fresh_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("runnable.process.json")), ScratchPath::File(dir.join("runnable.campaign.json")), ScratchPath::File(dir.join("wfmc.process.json")), ScratchPath::File(dir.join("wfmc.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-validate-3tier-seed"); // Face 1 — the happy path: an executable pipeline shape (a bare // std::sweep). Intrinsic + referential pass today (exit 0), but the // executor tier is invisible; after #205 validate gains the third // "valid (executable)" line while staying exit 0. let ok_proc = register_process_doc(&dir, "runnable.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc(&dir, "runnable.campaign.json", &campaign_doc_json(&bp_id, &ok_proc, (1, 2), "", "")); let (ok_out, ok_code) = run_code_in(&dir, &["campaign", "validate", "runnable.campaign.json"]); assert_eq!(ok_code, Some(0), "an executable campaign validates clean: {ok_out}"); assert!( ok_out.contains("campaign document valid (referential): all references resolve"), "the referential tier line survives the added third tier: {ok_out}" ); assert!( ok_out.contains( "campaign document valid (executable): pipeline shape and static guards pass" ), "the new third (executor-preflight) tier line is emitted: {ok_out}" ); // Face 2 — the refuse path: `[sweep, monte_carlo, walk_forward]` is // intrinsically valid (register exits 0, asserted in register_process_doc) // and referentially valid (same seeded blueprint + axes), so validate // reaches the third tier — which refuses the non-executable shape at // validate time (data-free: the [1, 2] 1970-epoch-ms window is never // reached), exit 1, with the executor fault header + the shape prose. let bad_proc = register_process_doc(&dir, "wfmc.process.json", WF_AFTER_MC_PROCESS_DOC); write_doc(&dir, "wfmc.campaign.json", &campaign_doc_json(&bp_id, &bad_proc, (1, 2), "", "")); let (bad_out, bad_code) = run_code_in(&dir, &["campaign", "validate", "wfmc.campaign.json"]); assert_eq!(bad_code, Some(1), "an executor-unrunnable shape refuses at validate: {bad_out}"); assert!( bad_out.contains("campaign is not executable:"), "the executor-tier fault header (mirrors 'campaign references do not resolve:'): {bad_out}" ); assert!( bad_out.contains("std::walk_forward cannot follow std::monte_carlo"), "the shape prose names the ordering violation via exec_fault_prose: {bad_out}" ); assert!(!bad_out.contains("PipelineShape"), "Debug leak: {bad_out}"); } /// #215: `walk_forward` selecting a plateau mode with NO gate ahead of it — /// the sweep survivors are the full parameter grid, so the plateau /// neighbourhood scoring `axis_lens` assumes is intact. const WF_PLATEAU_GATE_FREE_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "wf-plateau-gate-free", "pipeline": [ { "block": "std::sweep", "metric": "sqn_normalized", "select": "argmax" }, { "block": "std::walk_forward", "in_sample_ms": 1209600000, "out_of_sample_ms": 604800000, "step_ms": 604800000, "mode": "rolling", "metric": "net_expectancy_r", "select": "plateau:worst" } ] }"#; /// #215: the same `walk_forward` plateau select, but a `std::gate` stage now /// sits between the sweep and the walk-forward — the gate can filter /// survivors below the full grid, breaking the lattice the plateau /// neighbourhood assumes, so the executor preflight must still refuse it. const WF_PLATEAU_GATED_PROCESS_DOC: &str = r#"{ "format_version": 1, "kind": "process", "name": "wf-plateau-gated", "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": "plateau:worst" } ] }"#; /// Property (#215): the executor preflight's plateau-in-walk_forward refusal /// is narrowed to gate-preceded stages, not a blanket ban on plateau select /// in walk_forward — pinned through the full document surface (`campaign /// validate`'s third, executor tier — data-free, before any member runs), /// mirroring the pattern `campaign_validate_runs_the_executor_preflight_as_a_ /// third_tier` uses for the sibling shape guard. A regression that reverted /// to the blanket refusal would flip the gate-free face to exit 1; a /// regression that dropped the refusal entirely would flip the gated face to /// exit 0 — either direction goes red here. #[test] fn campaign_validate_narrows_plateau_in_wf_refusal_to_gate_preceded_stages() { let (dir, _fixture) = fresh_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-plateau-free.process.json")), ScratchPath::File(dir.join("wf-plateau-free.campaign.json")), ScratchPath::File(dir.join("wf-plateau-gated.process.json")), ScratchPath::File(dir.join("wf-plateau-gated.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-validate-wf-plateau-seed"); // Face 1 — gate-free: sweep -> walk_forward(plateau:worst). The lattice // is intact, so the executor tier accepts it (exit 0). let free_id = register_process_doc(&dir, "wf-plateau-free.process.json", WF_PLATEAU_GATE_FREE_PROCESS_DOC); write_doc( &dir, "wf-plateau-free.campaign.json", &campaign_doc_json(&bp_id, &free_id, (1, 2), "", ""), ); let (free_out, free_code) = run_code_in(&dir, &["campaign", "validate", "wf-plateau-free.campaign.json"]); assert_eq!(free_code, Some(0), "gate-free plateau walk_forward validates clean: {free_out}"); assert!( free_out.contains( "campaign document valid (executable): pipeline shape and static guards pass" ), "stdout/stderr: {free_out}" ); // Face 2 — gate-preceded: sweep -> gate -> walk_forward(plateau:worst). // The gate can break the lattice, so the executor tier still refuses it // (exit 1), naming the offending stage. let gated_id = register_process_doc(&dir, "wf-plateau-gated.process.json", WF_PLATEAU_GATED_PROCESS_DOC); write_doc( &dir, "wf-plateau-gated.campaign.json", &campaign_doc_json(&bp_id, &gated_id, (1, 2), "", ""), ); let (gated_out, gated_code) = run_code_in(&dir, &["campaign", "validate", "wf-plateau-gated.campaign.json"]); assert_eq!(gated_code, Some(1), "gate-preceded plateau walk_forward still refuses: {gated_out}"); assert!( gated_out.contains("campaign is not executable:"), "the executor-tier fault header: {gated_out}" ); assert!( gated_out.contains( "process stage 2: walk_forward cannot use a plateau select (a gated survivor subset has no parameter lattice)" ), "the exec_fault_prose naming the gate-broken lattice: {gated_out}" ); assert!(!gated_out.contains("PlateauInWalkForward"), "Debug leak: {gated_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 (dir, _fixture) = fresh_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}"); } /// 0109 split (a) — the tap namespace is CLOSED: the 0107 deferral fixture's /// made-up "r_record" tap now refuses INTRINSICALLY, exit 1, path-addressed /// and vocabulary-enumerating. The old test drove `campaign run` on a file /// target, so this pin drives the same path: the refusal fires at the run's /// parse-valid gate (`parse_valid_campaign` — the same `validate_campaign` + /// `doc_fault_prose` composition `campaign validate` shares), before /// registration, before the referential tier, before any member runs — and /// therefore before the retired loud-deferral note, whose absence is /// pinned here. #[test] fn campaign_run_refuses_unknown_tap_at_validate() { let (dir, _fixture) = fresh_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("badtap.process.json")), ScratchPath::File(dir.join("badtap.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-badtap-seed"); let proc_id = register_process_doc(&dir, "badtap.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( &dir, "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"]); assert_eq!(code, Some(1), "stdout/stderr: {out}"); assert!(out.contains("campaign document invalid:"), "stdout/stderr: {out}"); assert!( out.contains( "presentation.persist_taps[0]: unknown tap \"r_record\" \ (taps: equity | exposure | r_equity | net_r_equity)" ), "the refusal is path-addressed and enumerates the vocabulary: {out}" ); assert!( !out.contains("not yet honored"), "an invalid document must never reach the deferral note: {out}" ); assert!(!out.contains("UnknownTap"), "Debug leak: {out}"); } /// 0109 split (b) — the control: an in-vocabulary tap ("equity") sails /// through the new intrinsic gate (no UnknownTap prose) and the run proceeds /// to the member-data seam, exactly as a tap-less run over the [1, 2] 1970 /// epoch-ms window does (data-less host: missing geometry; data-ful host: no /// archive file overlaps) — #272: the member-data refusal is now a CONTAINED /// cell fault, not a campaign abort, so the run completes (exit 3, "completed /// with failed cells") and the always-on record line still carries the /// failed cell's fault. The retired 0107 loud-deferral note ("persist_taps not /// yet honored") must not appear — its absence is asserted below. #[test] fn campaign_run_valid_tap_reaches_the_member_data_seam() { let (dir, _fixture) = fresh_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), "\"equity\"", ""), ); let (out, code) = run_code_in(&dir, &["campaign", "run", "taps.campaign.json"]); assert_eq!(code, Some(3), "stdout/stderr: {out}"); assert!( !out.contains("unknown tap"), "\"equity\" is in the closed vocabulary and must pass validate: {out}" ); assert!( out.contains("no recorded geometry") || out.contains("no data for instrument"), "the refusal names the data condition: {out}" ); assert!( !out.contains("persist_taps not yet honored"), "the 0107 deferral line is retired; persist_taps is honored from 0109 on: {out}" ); // The record shows the cell as failed and the summary names the count — // not the brittle "no traces persisted" absence: a contained fault still // reaches (and passes through) the trace-persistence tail, which honestly // reports 0 cells written, so "traces persisted:" DOES appear now. assert!( out.contains("1 failed"), "the campaign-run summary names the one failed cell: {out}" ); let record_line = out .lines() .find(|l| l.starts_with("{\"campaign_run\":")) .expect("the always-on final campaign_run line"); assert!( record_line.contains("\"fault\""), "the recorded cell carries its fault: {record_line}" ); } /// Gated real-data e2e (the `cli_run.rs` skip idiom): a full /// sweep -> gate -> walk_forward -> monte_carlo 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, a walk-forward family id, and a /// pooled-OOS bootstrap on the terminal mc stage, and the /// `campaign_runs.jsonl` sibling store written. Since 0109 the campaign /// also requests `persist_taps: ["equity", "r_equity"]`: the record carries /// `trace_name`, the nominee's taps land under `traces///` /// (the persist re-run equality-asserted against the recorded nominee — /// C1), and `aura chart /` reads them back through the /// shipped viewer. 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 (dir, _fixture) = fresh_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), "\"equity\", \"r_equity\"", "\"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(3) && (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(), 4, "sweep + gate + walk_forward + monte_carlo 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}" ); assert_eq!( stages[3]["block"].as_str(), Some("std::monte_carlo"), "the annotator stage is realized last: {record_line}" ); let pooled = &stages[3]["bootstrap"]["pooled_oos"]; assert!( pooled.is_object(), "an mc stage after walk_forward records ONE pooled-OOS bootstrap: {record_line}" ); assert_eq!( pooled["n_resamples"].as_u64(), Some(200), "the bootstrap carries the doc's resamples verbatim: {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. let member_lines: Vec<&str> = out .lines() .filter(|l| l.starts_with("{\"family_id\":")) .collect(); assert!( member_lines.len() >= 4, "family_table member lines emitted: {out}" ); // Property (sweep-dissolution.md Goal point 5): the campaign member runner // (the real `CliMemberRunner` path, `campaign_run.rs`'s // `report.manifest.instrument = Some(cell.instrument.clone())`) stamps every // emitted member's manifest with the instrument under test — GER40 here. A // regression that dropped or mis-stamped the instrument on the campaign path // would slip past the count-only assertion above but fail here. for line in &member_lines { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); assert_eq!( v["report"]["manifest"]["instrument"].as_str(), Some("GER40"), "campaign member manifest must be stamped with the instrument: {line}" ); } 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" ); // 0109: the record claims the trace family it persisted under — // "{campaign8}-{run}" (fresh store => run 0). let trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); assert!( trace_name.len() == 10 && trace_name.ends_with("-0"), "trace_name is {{campaign8}}-{{run}}: {trace_name}" ); assert!( trace_name[..8].chars().all(|c| c.is_ascii_hexdigit()), "trace_name prefix is the campaign id's first 8 hex chars: {trace_name}" ); // The summary stderr line names the tap x cell arity (2 requested taps, // one (strategy, instrument, window) cell). assert!( out.contains(&format!("aura: traces persisted: {trace_name} (2 tap(s) x 1 cell(s))")), "the persist summary line: {out}" ); // The taps land under traces/// — the cell key is // content-derived: strategy8-instrument-w. let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); assert!( cell_dir.join("equity.json").is_file(), "equity tap persisted at {}", cell_dir.display() ); assert!( cell_dir.join("r_equity.json").is_file(), "r_equity tap persisted at {}", cell_dir.display() ); assert!( !cell_dir.join("exposure.json").exists(), "unrequested taps are not persisted" ); // The shipped viewer reads the member back (the cli_run.rs chart // idiom): exit 0 and an HTML page carrying both persisted tap series. let (chart_out, chart_code) = run_code_in(&dir, &["chart", &format!("{trace_name}/{cell_key}")]); assert_eq!(chart_code, Some(0), "aura chart reads the persisted member: {chart_out}"); assert!( chart_out.contains("\"equity\"") && chart_out.contains("\"r_equity\""), "the chart page carries the two persisted taps: {chart_out}" ); } /// Property (#259): a campaign's cost block reaches the walk-forward → /// monte-carlo evidence chain — the pooled-OOS bootstrap resamples the /// COST-NETTED per-trade series, so the same matrix run with and without a /// cost block records DIFFERENT `pooled_oos` stats (today's bug: identical to /// every digit). Hostless: runs over the synthetic SYMA archive, no /// data-mount skip-guard. #[test] fn campaign_run_synthetic_e2e_cost_block_nets_the_pooled_oos_bootstrap() { 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("netmc.process.json")), ScratchPath::File(dir.join("gross.campaign.json")), ScratchPath::File(dir.join("net.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-netmc-seed"); let proc_id = register_process_doc(&dir, "netmc.process.json", WF_PROCESS_DOC); // SYMA's synthetic archive spans 2024-01..08; Mar-01..Jun-30 (~17 weeks) // gives the (14d, 7d, 7d) roller a comfortable tiling. let base = campaign_doc_json_for( "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "", "", ); // The costed twin: the SAME document plus a constant cost block — the // only difference (the replacen pattern of the cost e2e siblings). let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 0.5 } } ],", 1, ); assert_ne!(with_cost, base, "replacen must actually match the seed field"); write_doc(&dir, "gross.campaign.json", &base); write_doc(&dir, "net.campaign.json", &with_cost); // 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]); assert_eq!(code, Some(0), "campaign run failed: {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"); v["campaign_run"]["cells"][0]["stages"][3]["bootstrap"]["pooled_oos"].clone() }; let gross = pooled("gross.campaign.json"); let net = pooled("net.campaign.json"); let n_gross = gross["n_trades"].as_u64().expect("gross n_trades"); let n_net = net["n_trades"].as_u64().expect("net n_trades"); assert!(n_gross > 0, "the synthetic window must produce OOS trades: {gross}"); assert_eq!( n_gross, n_net, "cost is a feed-forward subtraction — it must not change the trade population" ); let mean_gross = gross["e_r"]["mean"].as_f64().expect("gross mean"); let mean_net = net["e_r"]["mean"].as_f64().expect("net mean"); assert!( mean_net < mean_gross, "the costed bootstrap must resample the NET series (#259): gross {mean_gross} vs net {mean_net}" ); let p_gross = gross["prob_le_zero"].as_f64().expect("gross prob_le_zero"); let p_net = net["prob_le_zero"].as_f64().expect("net prob_le_zero"); assert!( p_net >= p_gross, "a strictly-lower resample mean cannot lower prob_le_zero: gross {p_gross} vs net {p_net}" ); } /// Property (#259), the OTHER mc bootstrap input shape: with no /// `walk_forward` in the pipeline (`sweep -> monte_carlo`), each surviving /// sweep member gets its OWN bootstrap (`PerSurvivor`, keyed by ordinal) — /// a distinct code path from `PooledOos` above (`member_net_trade_rs` / /// `SweepPoint` in aura-registry, not the wf-family pooling in /// aura-campaign). That path must resample the same cost-netted /// `net_trade_rs` conduit: a cost block must shift EVERY surviving member's /// resampled mean down, never leave any member's bootstrap gross-identical. /// Hostless: runs over the synthetic SYMA archive, no data-mount skip-guard. /// The gated real-data sibling (`campaign_run_real_e2e_sweep_monte_carlo_per_survivor`) /// exercises this shape but never with a cost block, so it cannot pin this property. #[test] fn campaign_run_synthetic_e2e_cost_block_nets_the_per_survivor_bootstrap() { 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("persurvivor.process.json")), ScratchPath::File(dir.join("gross_ps.campaign.json")), ScratchPath::File(dir.join("net_ps.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-persurvivor-seed"); let proc_id = register_process_doc(&dir, "persurvivor.process.json", MC_PROCESS_DOC); // Same SYMA window as the pooled-OOS sibling above — known to produce // trades over the synthetic archive. let base = campaign_doc_json_for( "SYMA", &bp_id, &proc_id, (1709251200000, 1719791999999), "", "", ); let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 0.5 } } ],", 1, ); assert_ne!(with_cost, base, "replacen must actually match the seed field"); write_doc(&dir, "gross_ps.campaign.json", &base); 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]); assert_eq!(code, Some(0), "campaign run failed: {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"); v["campaign_run"]["cells"][0]["stages"][1]["bootstrap"]["per_survivor"] .as_array() .expect("no walk_forward precedes: the PerSurvivor arm, not pooled_oos") .iter() .map(|pair| (pair[0].as_u64().expect("ordinal"), pair[1].clone())) .collect() }; let gross = per_survivor("gross_ps.campaign.json"); let net = per_survivor("net_ps.campaign.json"); assert_eq!(gross.len(), 4, "2x2 axes -> four surviving members: {gross:?}"); assert_eq!(gross.len(), net.len(), "cost must not change the surviving-member population"); for ((g_ord, g), (n_ord, n)) in gross.iter().zip(net.iter()) { assert_eq!(g_ord, n_ord, "the two runs must enumerate members in the same ordinal order"); let n_trades_g = g["n_trades"].as_u64().expect("gross n_trades"); let n_trades_n = n["n_trades"].as_u64().expect("net n_trades"); assert!(n_trades_g > 0, "member {g_ord} must have produced trades: {g}"); assert_eq!( n_trades_g, n_trades_n, "member {g_ord}: cost is a feed-forward subtraction — it must not change the trade population" ); let mean_g = g["e_r"]["mean"].as_f64().expect("gross mean"); let mean_n = n["e_r"]["mean"].as_f64().expect("net mean"); assert!( mean_n < mean_g, "member {g_ord}: the costed PerSurvivor bootstrap must resample the NET series (#259): gross {mean_g} vs net {mean_n}" ); } } /// Property (#260): an instrument-keyed cost knob resolves PER CELL — each /// emitted member's manifest stamps the value for ITS instrument under the /// unchanged cost[k]. key. Hostless over the synthetic SYMA+SYMB /// archive (SYMB's 2024-03..06 span contains the window). #[test] fn campaign_run_synthetic_e2e_per_instrument_cost_resolves_per_cell() { 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("percost.process.json")), ScratchPath::File(dir.join("percost.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-percost-seed"); let proc_id = register_process_doc(&dir, "percost.process.json", SWEEP_ONLY_PROCESS_DOC); let cost = "\"cost\": [ { \"constant\": { \"cost_per_trade\": { \"SYMA\": 2.5, \"SYMB\": 0.25 } } } ],\n "; let doc = campaign_doc_json_two( ("SYMA", "SYMB"), cost, &bp_id, &proc_id, (1709251200000, 1717199999999), ); write_doc(&dir, "percost.campaign.json", &doc); let (out, code) = run_code_in(&dir, &["campaign", "run", "percost.campaign.json"]); assert_eq!(code, Some(0), "stdout/stderr: {out}"); // Member emit lines carry report.manifest; group the stamped resolved // value by the member's instrument stamp (the #220-era assertion idiom). let mut seen = std::collections::BTreeMap::new(); for line in out.lines().filter(|l| l.starts_with("{\"family_id\":")) { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses"); let inst = v["report"]["manifest"]["instrument"] .as_str() .expect("instrument stamped") .to_string(); let params = v["report"]["manifest"]["params"].as_array().expect("params array"); let cost_param = params .iter() .find(|p| p[0].as_str() == Some("cost[0].cost_per_trade")) .unwrap_or_else(|| panic!("cost stamp missing on {inst} member: {line}")); let val = cost_param[1]["F64"].as_f64().expect("stamped as F64"); seen.insert(inst, val); } assert_eq!( seen, std::collections::BTreeMap::from([ ("SYMA".to_string(), 2.5), ("SYMB".to_string(), 0.25), ]), "each cell must stamp ITS instrument's resolved cost: {out}" ); } /// Property (#262): a two-regime risk matrix realizes one cell per regime /// with distinct ordinals, and each cell's members stamp THEIR regime's stop /// knobs — the vol_tf cells carry stop_period_minutes (the round-trip pin), /// the vol cells do not. Hostless over the synthetic SYMA archive. #[test] fn campaign_run_synthetic_e2e_vol_tf_regime_realizes_and_stamps() { 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("voltf.process.json")), ScratchPath::File(dir.join("voltf.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-voltf-seed"); let proc_id = register_process_doc(&dir, "voltf.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json_for( "SYMA", &bp_id, &proc_id, (1709251200000, 1717199999999), "", "\"family_table\"", ); let with_risk = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 3, \"k\": 2.0 } }, { \"vol_tf\": { \"period_minutes\": 60, \"length\": 6, \"k\": 2.0 } } ],", 1, ); 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"]); 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("record parses"); let cells = v["campaign_run"]["cells"].as_array().expect("cells"); assert_eq!(cells.len(), 2, "one cell per regime: {record_line}"); // regime_ordinal 0 (the first/default-slot regime) is skip_serialize'd // (CellRealization::is_zero_ordinal, byte-stability for pre-#219 stored // lines) — absent means 0, not a missing field. let ordinals: Vec = cells.iter().map(|c| c["regime_ordinal"].as_u64().unwrap_or(0)).collect(); assert_eq!(ordinals, vec![0, 1], "distinct regime ordinals: {record_line}"); // Member emit lines: vol_tf members stamp stop_period_minutes, vol members do not. let (mut tf_members, mut vol_members) = (0usize, 0usize); for line in out.lines().filter(|l| l.starts_with("{\"family_id\":")) { let m: serde_json::Value = serde_json::from_str(line).expect("member line parses"); let params = m["report"]["manifest"]["params"].as_array().expect("params"); let has_tf = params.iter().any(|p| p[0].as_str() == Some("stop_period_minutes")); if has_tf { let pm = params .iter() .find(|p| p[0].as_str() == Some("stop_period_minutes")) .expect("present"); assert_eq!(pm[1]["I64"].as_i64(), Some(60), "the stamped timescale: {line}"); tf_members += 1; } else { vol_members += 1; } } assert!(tf_members > 0, "vol_tf members must exist and stamp their timescale: {out}"); assert!(vol_members > 0, "vol members must exist without the vol_tf stamp: {out}"); } /// Property (#260): map-key mismatches refuse at validate with prose naming /// the component and instruments — never mid-run, never a Debug struct. #[test] fn campaign_validate_refuses_a_cost_map_missing_an_instrument() { let dir = temp_cwd("campaign-validate-percost-missing"); let cost = "\"cost\": [ { \"constant\": { \"cost_per_trade\": { \"SYMA\": 2.5 } } } ],\n "; let doc = campaign_doc_json_two( ("SYMA", "SYMB"), cost, "9f3a", "9f3a", (1709251200000, 1717199999999), ); write_doc(&dir, "bad.campaign.json", &doc); let (out, code) = run_code_in(&dir, &["campaign", "validate", "bad.campaign.json"]); assert_eq!(code, Some(1), "a key-set mismatch is a validate fault: {out}"); assert!( out.contains("cost[0]") && out.contains("SYMB"), "prose names the component and the missing instrument: {out}" ); assert!(!out.contains("CostInstrumentKeys"), "Debug leak: {out}"); } /// Property (#260): an instrument map naming a key that is NOT one of the /// campaign's instruments (a typo'd symbol) is refused too — the sibling /// defect to the missing-instrument case above, and a distinct prose branch /// (`doc_fault_prose`'s `extra` arm) that was otherwise never exercised past /// the raw `DocFault` value in the aura-research unit tests. #[test] fn campaign_validate_refuses_a_cost_map_with_an_unknown_instrument_key() { let dir = temp_cwd("campaign-validate-percost-extra"); let cost = "\"cost\": [ { \"constant\": { \"cost_per_trade\": { \"SYMA\": 2.5, \"SYMB\": 0.25, \"SYMA.cash\": 1.0 } } } ],\n "; let doc = campaign_doc_json_two( ("SYMA", "SYMB"), cost, "9f3a", "9f3a", (1709251200000, 1717199999999), ); write_doc(&dir, "bad_extra.campaign.json", &doc); let (out, code) = run_code_in(&dir, &["campaign", "validate", "bad_extra.campaign.json"]); assert_eq!(code, Some(1), "an unmatched map key is a validate fault: {out}"); assert!( out.contains("cost[0]") && out.contains("SYMA.cash"), "prose names the component and the unmatched key: {out}" ); assert!( out.contains("names no campaign instrument"), "prose uses the extra-key phrasing, not the missing-instrument one: {out}" ); assert!(!out.contains("CostInstrumentKeys"), "Debug leak: {out}"); } /// The campaign `data.bindings` override end to end (#231, 6b): the same /// seeded strategy, window, and seed run twice — bare (price<-close default) /// and with `price` rebound to the open column. Both exit 0; the realized /// member metrics differ (the open series is not the close series); the /// strategy blueprint is untouched (both docs reference the same content id). /// Gated on the local GER40 archive; skips cleanly when absent. #[test] fn campaign_run_real_e2e_bindings_override_rebinds_price_to_open() { let (dir, _fixture) = fresh_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("sweep.process.json")), ScratchPath::File(dir.join("close.campaign.json")), ScratchPath::File(dir.join("open.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "binding-override-seed"); let proc_id = register_process_doc(&dir, "sweep.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "", "\"family_table\"", ); let rebound = base.replace( r#""data": { "instruments": ["GER40"],"#, r#""data": { "bindings": { "price": "open" }, "instruments": ["GER40"],"#, ); assert_ne!(rebound, base, "the override splice must hit the data section"); 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"]); if code_close == Some(3) && (out_close.contains("no recorded geometry") || out_close.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the binding-override e2e"); 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"]); assert_eq!(code_open, Some(0), "stdout/stderr: {out_open}"); let first_member = |out: &str| -> serde_json::Value { out.lines() .find(|l| l.starts_with("{\"family_id\":")) .map(|l| serde_json::from_str(l).expect("member line parses")) .expect("a family_table member line") }; let close_metrics = first_member(&out_close)["report"]["metrics"].clone(); let open_metrics = first_member(&out_open)["report"]["metrics"].clone(); assert_ne!( close_metrics, open_metrics, "rebinding price<-open must change the realized member metrics" ); } /// Property (#210 T3, Task 4 Step 9 — the regime->stop map): a campaign /// document's non-default risk regime (`risk: [{ "vol": { length, k } }]`) /// reaches the real `CliMemberRunner::run_member` path and IS the stop actually /// stamped into each emitted member's manifest — `stop_length`/`stop_k` equal /// the regime's own values (5, 3.5), not `R_SMA_STOP_LENGTH`/`R_SMA_STOP_K` /// (3, 2.0). Every other campaign-run test in this file is regime-less (`risk` /// absent), so they all only ever exercise the `None` arm of the campaign /// member runner's regime match and would keep passing even if a regression /// dropped `cell.regime` on the floor and always stamped the baked default; /// this is the one test that actually drives the `Some(RiskRegime::Vol { .. })` /// arm end to end. Gated on the local GER40 archive like its sibling e2e tests /// above; skips on a data-less host. #[test] fn campaign_run_real_e2e_non_default_regime_stamps_its_own_stop() { let (dir, _fixture) = fresh_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("regime.process.json")), ScratchPath::File(dir.join("regime.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-regime-seed"); let proc_id = register_process_doc(&dir, "regime.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "", "\"family_table\"", ); let with_regime = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.5 } } ],", 1, ); 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"]); if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign regime e2e"); return; } assert_eq!(code, Some(0), "stdout/stderr: {out}"); let member_lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect(); assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}"); for line in &member_lines { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); let params = v["report"]["manifest"]["params"] .as_array() .expect("manifest.params is an array"); let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name)); assert_eq!( get("stop_length").and_then(|p| p[1]["I64"].as_i64()), Some(5), "the campaign's own regime length is stamped, not the R_SMA default: {line}" ); assert_eq!( get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(3.5), "the campaign's own regime k is stamped, not the R_SMA default: {line}" ); } } /// Property (#210 T3/T4 — per-cell regime resolution, not a shared last-write): /// a campaign with TWO distinct non-default risk regimes stamps EACH regime's /// OWN stop into ITS OWN cell's members, never the other regime's values and /// never both collapsing onto one. The single-regime sibling test above cannot /// catch a bug where the executor's per-cell loop resolves the WRONG regime for /// a cell (e.g. an off-by-one on `regime_ordinal`, or a closure that captures /// the last-seen `regime` instead of the current one) — with only one non- /// default entry, such a bug is invisible (there is nothing else it could pick). /// Distinguishes the two cells' member lines by the `-r{regime_ordinal}-` /// segment `run_cell` bakes into the family name (exec.rs), which is the only /// externally observable handle on which cell a member line belongs to. Gated /// on the local GER40 archive like its siblings; skips on a data-less host. #[test] fn campaign_run_real_e2e_two_regimes_each_stamp_their_own_stop() { let (dir, _fixture) = fresh_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("tworegimes.process.json")), ScratchPath::File(dir.join("tworegimes.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-two-regimes-seed"); let proc_id = register_process_doc(&dir, "tworegimes.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "", "\"family_table\"", ); let with_regimes = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.5 } }, \ { \"vol\": { \"length\": 8, \"k\": 1.2 } } ],", 1, ); 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"]); if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign two-regime e2e"); return; } assert_eq!(code, Some(0), "stdout/stderr: {out}"); let member_lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect(); assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}"); let expect_for = |regime_tag: &str, want_length: i64, want_k: f64| { let matches: Vec<&&str> = member_lines .iter() .filter(|line| line.contains(regime_tag)) .collect(); assert!(!matches.is_empty(), "at least one member line tagged {regime_tag}: {out}"); for line in matches { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); let params = v["report"]["manifest"]["params"] .as_array() .expect("manifest.params is an array"); let get = |name: &str| params.iter().find(|p| p[0].as_str() == Some(name)); assert_eq!( get("stop_length").and_then(|p| p[1]["I64"].as_i64()), Some(want_length), "{regime_tag} stamps its own stop_length, not the other regime's: {line}" ); assert_eq!( get("stop_k").and_then(|p| p[1]["F64"].as_f64()), Some(want_k), "{regime_tag} stamps its own stop_k, not the other regime's: {line}" ); } }; expect_for("-r0-", 5, 3.5); expect_for("-r1-", 8, 1.2); } /// RED (#219 / #212): the campaign trace-persist re-run must reproduce the /// nominee it recorded, INCLUDING when the cell ran under a non-default risk /// regime. `run_blueprint_member` stamps the regime-resolved stop into the /// member manifest (`stop_length`/`stop_k`), but `persist_campaign_traces` /// rebuilds the re-run's `point` only from the SIGNAL blueprint's param space /// (the SMA lengths) and hardcodes the DEFAULT `Vol { R_SMA_STOP_LENGTH, /// R_SMA_STOP_K }` (3, 2.0) for the stop (campaign_run.rs). For a non-default /// regime the re-run's stop differs from the recorded run's stop, its metrics /// diverge, and the C1 drift alarm returns a FALSE "trace re-run diverged from /// the recorded nominee (C1 violation)" refusal — exit 1 on a run that is in /// fact perfectly deterministic. The record boundary (`CellRealization`) drops /// the regime; the fix threads it through so the re-run binds the cell's OWN /// stop. Every other persist e2e in this file is regime-less (default risk), /// which is exactly why the bug shipped unnoticed. Gated on the local GER40 /// archive like its siblings; skips on a data-less host. #[test] fn campaign_persist_non_default_regime_does_not_false_fail_c1() { let (dir, _fixture) = fresh_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("regime-persist.process.json")), ScratchPath::File(dir.join("regime-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-regime-seed"); let proc_id = register_process_doc(&dir, "regime-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"equity\", \"r_equity\"", "\"family_table\"", ); // A non-default regime: length != 3 or k != 2.0. Vol { 5, 3.0 } is a 50% // wider stop over a longer ATR window — its exits, R rows, and metrics // differ from the default the persist re-run hardcodes. let with_regime = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.0 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign regime-persist e2e"); return; } // The symptom, pinned exactly: the persist re-run hardcodes the default // stop and false-fails the C1 drift alarm for the non-default regime. assert!( !out.contains("trace re-run diverged from the recorded nominee (C1 violation)"), "the persist re-run must bind the cell's own regime stop, not false-fail C1: {out}" ); assert_eq!( code, Some(0), "a deterministic non-default-regime campaign persists cleanly: {out}" ); // The nominee's taps actually landed (not merely that the refusal was // suppressed): traces///equity.json exists. 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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); assert!( cell_dir.join("equity.json").is_file(), "the non-default-regime nominee's equity tap persists at {}", cell_dir.display() ); } /// Property (#234 Task 5, quality-review Minor finding): a costed campaign's /// trace-persist re-run must not false-fail the C1 drift alarm, exercised /// through the REAL production path — `campaign run` -> `run_campaign_returning` /// -> `CliMemberRunner` (net-mode member run) -> `persist_campaign_traces` /// (non-reduce re-run binding a `CostLeg` via `cost_nodes_for`) — not the /// hand-mirrored arithmetic pinned by /// `persist_side_nonreduce_rerun_matches_reduce_mode_net_metrics_under_cost` in /// `main.rs`'s unit tests. Mirrors the sibling non-default-regime e2e above /// (same #219-class divergence risk, cost edition): a costed campaign /// document, `persist_taps` non-empty so the re-run actually executes, must /// exit 0 with its nominee's cost stamp on disk and no C1 violation. Gated on /// the local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm() { let (dir, _fixture) = fresh_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("cost-persist.process.json")), ScratchPath::File(dir.join("cost-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-cost-seed"); let proc_id = register_process_doc(&dir, "cost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"equity\", \"r_equity\"", "\"family_table\"", ); let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 0.0005 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign cost-persist e2e"); return; } assert!( !out.contains("C1 violation"), "the costed persist-side re-run must bind the SAME cost model the nominee ran \ under, not false-fail C1: {out}" ); assert_eq!(code, Some(0), "a deterministic costed campaign persists cleanly: {out}"); // Non-vacuity: every emitted member actually stamped the cost component // it ran under (else the C1 comparison above never engaged a non-empty // CostLeg and this test would pass for the wrong reason). let member_lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect(); assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}"); for line in &member_lines { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); let params = v["report"]["manifest"]["params"] .as_array() .expect("manifest.params is an array"); assert!( params.iter().any(|p| p[0].as_str() == Some("cost[0].cost_per_trade")), "the costed member stamps its cost component: {line}" ); } // The nominee's taps actually landed under the costed re-run. 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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); assert!( cell_dir.join("r_equity.json").is_file(), "the costed nominee's r_equity tap persists at {}", cell_dir.display() ); } /// Property (#234): a costed campaign requesting `net_r_equity` must actually /// land a `net_r_equity.json` trace on disk carrying the cost-adjusted curve /// — the write path (`TapChannel::Net` -> `rx_net` -> `net_rows` -> /// `ColumnarTrace`) had no positive e2e coverage: the costed siblings above /// request only equity/r_equity, and the skip e2e only proves `net_r_equity` /// is ABSENT on a cost-FREE document. Exercised through the same real /// production path as /// `campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm`, /// requesting `net_r_equity` alongside `r_equity` on an identically-shaped /// costed document: the net file must exist, carry the same row count as its /// `r_equity` sibling (both drained off the identical per-cycle firing off /// `exec.output("cum_realized_r"/"unrealized_r")` — `wrap_r`, main.rs), and /// its values must diverge from the gross r_equity curve at least once /// (non-vacuity — a net curve byte-identical to gross would pass even if the /// LinComb(4) cost subtraction were never wired). Gated on the local GER40 /// archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_net_r_equity_tap_persists_the_cost_adjusted_curve() { let (dir, _fixture) = fresh_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("net-persist.process.json")), ScratchPath::File(dir.join("net-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-net-seed"); let proc_id = register_process_doc(&dir, "net-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"net_r_equity\", \"r_equity\"", "\"family_table\"", ); let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 0.0005 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign net-persist e2e"); return; } assert!( !out.contains("C1 violation"), "the costed persist-side re-run must bind the SAME cost model the nominee ran \ under, not false-fail C1: {out}" ); assert_eq!(code, Some(0), "a deterministic costed campaign persists cleanly: {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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); // The headline finding: net_r_equity.json actually lands on disk. let net_path = cell_dir.join("net_r_equity.json"); assert!( net_path.is_file(), "the costed nominee's net_r_equity tap persists at {}", cell_dir.display() ); let req_path = cell_dir.join("r_equity.json"); assert!( req_path.is_file(), "the sibling gross r_equity tap persists at {}", cell_dir.display() ); let net_trace: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(&net_path).expect("net_r_equity.json readable"), ) .expect("net_r_equity.json parses as JSON"); let req_trace: serde_json::Value = serde_json::from_str( &std::fs::read_to_string(&req_path).expect("r_equity.json readable"), ) .expect("r_equity.json parses as JSON"); let net_col = net_trace["columns"][0].as_array().expect("net_r_equity has a value column"); let req_col = req_trace["columns"][0].as_array().expect("r_equity has a value column"); assert!(!net_col.is_empty(), "the net curve carries at least one recorded row: {net_trace}"); assert_eq!( net_col.len(), req_col.len(), "the net and gross curves are drained off the identical per-cycle firing, same row count" ); // Non-vacuity: at least one row's net value differs from its gross // sibling — else this test would pass even if the LinComb cost // subtraction (net = gross - cum_cost - open_cost) were never wired and // net silently mirrored gross. let any_row_differs = net_col.iter().zip(req_col.iter()).any(|(n, r)| { n.as_f64().expect("net cell is f64") != r.as_f64().expect("gross cell is f64") }); assert!( any_row_differs, "the costed net curve must diverge from the gross r_equity curve at least once \ (else the cost subtraction never engaged): net={net_col:?} gross={req_col:?}" ); } /// Property (#234): a `vol_slippage` cost component — the structurally /// distinct leg from `constant` (it reactivates the vol-proxy RollingMax/ /// RollingMin/Sub chain feeding a `VolSlippageCost` node, not a flat per-close /// charge) — nets a real campaign member through the SAME production path as /// its `constant` sibling /// (`campaign_run_real_e2e_costed_campaign_persists_without_drift_alarm`): /// `campaign run` -> `CliMemberRunner` (net-mode) -> `persist_campaign_traces` /// (non-reduce re-run binding a `CostLeg`). The re-run must bind the identical /// vol-slippage model the nominee ran under, so it must not false-fail the C1 /// drift alarm; the emitted member must stamp `cost[0].slip_vol_mult`; and the /// vol-driven charge must actually move net off gross (non-vacuity — a /// zero-effect charge would let this pass even if the vol proxy were never /// wired). Gated on the local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_vol_slippage_cost_persists_without_drift_alarm() { let (dir, _fixture) = fresh_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("volcost-persist.process.json")), ScratchPath::File(dir.join("volcost-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-volcost-seed"); let proc_id = register_process_doc(&dir, "volcost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"r_equity\"", "\"family_table\"", ); let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"vol_slippage\": { \"slip_vol_mult\": 0.5 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign vol-slippage-persist e2e"); return; } assert!( !out.contains("C1 violation"), "the vol-slippage persist-side re-run must bind the SAME cost model the nominee \ ran under, not false-fail C1: {out}" ); assert_eq!(code, Some(0), "a deterministic vol-slippage-costed campaign persists cleanly: {out}"); let member_lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect(); assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}"); let mut any_net_moved = false; for line in &member_lines { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); let params = v["report"]["manifest"]["params"] .as_array() .expect("manifest.params is an array"); assert!( params.iter().any(|p| p[0].as_str() == Some("cost[0].slip_vol_mult")), "the costed member stamps its vol_slippage component: {line}" ); if let Some(r) = v["report"]["metrics"]["r"].as_object() { let gross = r["expectancy_r"].as_f64().expect("expectancy_r is a number"); let net = r["net_expectancy_r"].as_f64().expect("net_expectancy_r is a number"); if gross != net { any_net_moved = true; } } } assert!( any_net_moved, "at least one member's vol-slippage charge must move net off gross \ (else the vol proxy never engaged): {out}" ); } /// RED (#212 — the trace-dir collision facet of the same #219 record gap): two /// risk regimes over the SAME (strategy, instrument, window) cell must persist /// their traces into DISTINCT directories. `campaign_cell_key` names dirs /// `strategy8-instrument-w{window_ordinal}` with NO regime discriminator /// (campaign_run.rs), so both regimes' nominees resolve to the identical /// `strategy8-GER40-w0` member dir and the second write clobbers the first. /// NOTE (masking): today this fails EARLIER — at the same false C1 refusal as /// its sibling above, because the persist re-run's hardcoded default stop /// diverges from each non-default regime's recorded nominee. Once the regime is /// threaded so the re-run stops false-failing, THIS assertion (two distinct /// on-disk trace dirs, one per regime) becomes the live guard that the cell key /// gained its `-r{ordinal}` discriminator — without it the two regimes collide /// into one dir. Gated on the local GER40 archive; skips on a data-less host. #[test] fn campaign_persist_two_regimes_land_in_distinct_trace_dirs() { let (dir, _fixture) = fresh_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("tworegimes-persist.process.json")), ScratchPath::File(dir.join("tworegimes-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-two-regimes-seed"); let proc_id = register_process_doc( &dir, "tworegimes-persist.process.json", SWEEP_ONLY_PROCESS_DOC, ); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"equity\"", "\"family_table\"", ); let with_regimes = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"risk\": [ { \"vol\": { \"length\": 5, \"k\": 3.0 } }, \ { \"vol\": { \"length\": 8, \"k\": 1.2 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the two-regime persist e2e"); return; } // Precondition (facet A): neither regime's persist re-run may false-fail C1. assert!( !out.contains("trace re-run diverged from the recorded nominee (C1 violation)"), "the persist re-run must bind each cell's own regime stop, not false-fail C1: {out}" ); assert_eq!(code, Some(0), "a two-regime campaign persists both cells cleanly: {out}"); // The guard (facet B): each regime's equity tap lands in its OWN dir under // traces//, so there are TWO distinct tap-bearing dirs, not one. 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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); let family_dir = runs_dir.join("traces").join(trace_name); let mut tap_dirs: Vec = std::fs::read_dir(&family_dir) .expect("trace family dir exists") .filter_map(|e| e.ok()) .filter(|e| e.path().join("equity.json").is_file()) .map(|e| e.file_name().to_string_lossy().into_owned()) .collect(); tap_dirs.sort(); tap_dirs.dedup(); assert_eq!( tap_dirs.len(), 2, "two regimes over one cell persist into two distinct trace dirs, got {tap_dirs:?}" ); } /// Property: an UNPRODUCIBLE tap requested alongside a producible one degrades /// gracefully, never a crash or a silently-dropped whole request. `net_r_equity` /// is in the closed tap vocabulary (`validate_campaign` accepts it) but has no /// drained channel on the campaign runner's cost-free wrap (the doc carries /// no cost block, #234); the one-time stderr note names exactly that tap, the /// summary line's arity counts only what was actually written (1 tap, not 2), /// and no `net_r_equity.json` file is ever created — only the producible /// `equity` tap lands on disk. Uses `SWEEP_ONLY_PROCESS_DOC` (cheapest /// executable shape) over the same GER40 Sept-2024 window as the gated wf e2e /// above, so the underlying member run needs real data; skips on a data-less /// host like its siblings. #[test] fn campaign_run_real_e2e_skips_unproducible_tap_gracefully() { let (dir, _fixture) = fresh_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("mixedtap.process.json")), ScratchPath::File(dir.join("mixedtap.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-mixedtap-seed"); let proc_id = register_process_doc(&dir, "mixedtap.process.json", SWEEP_ONLY_PROCESS_DOC); write_doc( &dir, "mixedtap.campaign.json", &campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"net_r_equity\", \"equity\"", "", ), ); let (out, code) = run_code_in(&dir, &["campaign", "run", "mixedtap.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); assert!( out.contains( "aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped" ), "the unproducible tap gets a named, one-time note with the remedy: {out}" ); assert!( out.contains(&format!("aura: traces persisted: {trace_name} (1 tap(s) x 1 cell(s))")), "the summary counts only the producible tap actually written: {out}" ); let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); assert!( cell_dir.join("equity.json").is_file(), "the producible tap still persists: {}", cell_dir.display() ); assert!( !cell_dir.join("net_r_equity.json").exists(), "an unproducible tap is never written to disk" ); } /// #234 (archive-gated): the net-R campaign path end to end. The SAME document /// runs twice — without and with a `cost` block (the only textual difference). /// Cost only ADDS: every member's gross field family is identical across the /// two runs and net == gross without cost; with cost, every member's /// net_expectancy_r sits strictly below its gross expectancy_r. The costed run /// persists `net_r_equity` when requested (no skip notice), while the /// cost-less run skips it with the remedy notice; and the costed family /// reproduces bit-identically (components re-derived from the member /// manifests). Skips on a data-less host like its siblings. #[test] fn campaign_run_real_e2e_cost_block_nets_members_and_persists_net_r_equity() { let (dir, _fixture) = fresh_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("costnet.process.json")), ScratchPath::File(dir.join("costless.campaign.json")), ScratchPath::File(dir.join("costnet.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-costnet-seed"); let proc_id = register_process_doc(&dir, "costnet.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"net_r_equity\", \"equity\"", "\"family_table\"", ); // The same document, plus a cost block — the ONLY difference. 2.0 price // units per close on GER40 (index ~18500, M1 vol-stop distances of a few // points) yields a clearly visible per-trade cost-in-R. let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"constant\": { \"cost_per_trade\": 2.0 } }, { \"vol_slippage\": { \"slip_vol_mult\": 0.5 } } ],", 1, ); assert_ne!(with_cost, base, "replacen must actually match the fixture's seed field"); write_doc(&dir, "costless.campaign.json", &base); write_doc(&dir, "costnet.campaign.json", &with_cost); // Per-member (axis-params key -> (expectancy_r, net_expectancy_r)), // parsed from the emitted family_table member lines. Cost stamps are // excluded from the key so the two runs' members align. let members = |out: &str| -> Vec<(String, f64, f64)> { out.lines() // A member line starts with the family_id key itself; the always-on // final `{"campaign_run":...}` summary line also embeds a nested // "family_id" (per selection stage) and must NOT be swept in here. .filter(|l| l.starts_with("{\"family_id\":")) .map(|l| { let v: serde_json::Value = serde_json::from_str(l).expect("member line parses"); let key = v["report"]["manifest"]["params"] .as_array() .expect("params array") .iter() .filter(|p| !p[0].as_str().unwrap_or_default().starts_with("cost[")) .map(|p| p.to_string()) .collect::>() .join(","); let r = &v["report"]["metrics"]["r"]; ( key, r["expectancy_r"].as_f64().expect("expectancy_r"), r["net_expectancy_r"].as_f64().expect("net_expectancy_r"), ) }) .collect() }; // (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"]); if code_gross == Some(3) && (out_gross.contains("no recorded geometry") || out_gross.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the cost e2e"); return; } assert_eq!(code_gross, Some(0), "cost-less baseline run: {out_gross}"); assert!( out_gross.contains( "aura: tap \"net_r_equity\" needs a cost model; add a cost block to the campaign document; skipped" ), "the cost-less run skips the net tap with the remedy: {out_gross}" ); let gross_members = members(&out_gross); assert_eq!(gross_members.len(), 4, "2x2 axes -> four member lines: {out_gross}"); for (key, e, ne) in &gross_members { assert_eq!(e, ne, "cost-less member {key} must have net == gross"); } // (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"]); assert_eq!(code_net, Some(0), "costed run: {out_net}"); assert!( !out_net.contains("skipped"), "a costed run must not skip any requested tap: {out_net}" ); let net_members = members(&out_net); assert_eq!(net_members.len(), 4, "the same four members: {out_net}"); for (key, e, ne) in &net_members { let (_, gross_e, _) = gross_members .iter() .find(|(gk, _, _)| gk == key) .unwrap_or_else(|| panic!("costed member {key} has no cost-less twin")); assert_eq!(e, gross_e, "cost only ADDS: member {key} gross must be identical"); assert!(ne < e, "member {key}: net {ne} must sit strictly below gross {e}"); } let record_line = out_net .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 trace_name = v["campaign_run"]["trace_name"] .as_str() .expect("persist_taps non-empty => the record carries trace_name"); let cell_key = format!("{}-GER40-w0", &bp_id[..8]); let cell_dir = runs_dir.join("traces").join(trace_name).join(&cell_key); assert!( cell_dir.join("net_r_equity.json").is_file(), "the net curve persists at {}", cell_dir.display() ); assert!( cell_dir.join("equity.json").is_file(), "the sibling tap still persists: {}", cell_dir.display() ); // (c) the costed family reproduces bit-identically — the components are // re-derived from the member manifests (#233 pattern, cost edition). let family_id = out_net .lines() .find(|l| l.starts_with("{\"family_id\":")) .map(|l| { serde_json::from_str::(l).expect("member line parses")["family_id"] .as_str() .expect("family_id is a string") .to_string() }) .expect("a costed member line carries the family id"); let (rep_out, rep_code) = run_code_in(&dir, &["reproduce", &family_id]); assert_eq!(rep_code, Some(0), "reproduce: {rep_out}"); assert!( rep_out.contains("reproduced 4/4 members bit-identically"), "the costed family re-derives: {rep_out}" ); } /// Property (#234): a `carry` cost component — the third and structurally /// distinct cost leg (a per-held-cycle accrual charged while a position stays /// open, via `CarryCost`'s own extra input, unlike `constant`'s flat per-close /// charge or `vol_slippage`'s volatility-proxy-scaled charge) — nets a real /// campaign member through the SAME production path as its `constant`/ /// `vol_slippage` siblings (`campaign run` -> `CliMemberRunner` (net-mode) -> /// `persist_campaign_traces`, non-reduce re-run binding a `CostLeg`). The /// re-run must bind the identical carry model the nominee ran under, so it /// must not false-fail the C1 drift alarm; the emitted member must stamp /// `cost[0].carry_per_cycle`; and the accrual must actually move at least one /// member's net off its gross (non-vacuity — a zero-effect charge would let /// this pass even if the per-cycle accrual were never wired). Gated on the /// local GER40 archive; skips on a data-less host. #[test] fn campaign_run_real_e2e_carry_cost_persists_without_drift_alarm() { let (dir, _fixture) = fresh_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("carrycost-persist.process.json")), ScratchPath::File(dir.join("carrycost-persist.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-persist-carrycost-seed"); let proc_id = register_process_doc(&dir, "carrycost-persist.process.json", SWEEP_ONLY_PROCESS_DOC); let base = campaign_doc_json( &bp_id, &proc_id, (1725148800000, 1727740799999), "\"r_equity\"", "\"family_table\"", ); let with_cost = base.replacen( "\"seed\": 7,", "\"seed\": 7,\n \"cost\": [ { \"carry\": { \"carry_per_cycle\": 0.001 } } ],", 1, ); 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"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (out.contains("no recorded geometry") || out.contains("no data for instrument")) { eprintln!("skip: no local GER40 data for the campaign carry-cost-persist e2e"); return; } assert!( !out.contains("C1 violation"), "the carry persist-side re-run must bind the SAME cost model the nominee ran \ under, not false-fail C1: {out}" ); assert_eq!(code, Some(0), "a deterministic carry-costed campaign persists cleanly: {out}"); let member_lines: Vec<&str> = out.lines().filter(|l| l.starts_with("{\"family_id\":")).collect(); assert!(!member_lines.is_empty(), "family_table member lines emitted: {out}"); let mut any_net_moved = false; for line in &member_lines { let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON"); let params = v["report"]["manifest"]["params"] .as_array() .expect("manifest.params is an array"); assert!( params.iter().any(|p| p[0].as_str() == Some("cost[0].carry_per_cycle")), "the costed member stamps its carry component: {line}" ); if let Some(r) = v["report"]["metrics"]["r"].as_object() { let gross = r["expectancy_r"].as_f64().expect("expectancy_r is a number"); let net = r["net_expectancy_r"].as_f64().expect("net_expectancy_r is a number"); if gross != net { any_net_moved = true; } } } assert!( any_net_moved, "at least one member's carry accrual must move net off gross \ (else the per-held-cycle charge never engaged): {out}" ); } /// Gated real-data e2e, the OTHER mc bootstrap input shape (#200 d1): with no /// walk_forward in the pipeline (`sweep -> monte_carlo`), the terminal /// annotator's dual-input dispatch takes the `PerSurvivor` arm, not /// `PooledOos` — one bootstrap per surviving sweep member, keyed by the /// member's ordinal into the sweep family, rather than one pooled bootstrap /// over walk-forward OOS windows. `campaign_run_real_e2e_sweep_gate_walkforward` /// above only exercises `PooledOos`; this is the only e2e coverage of /// `PerSurvivor` with real data (the addressing-mode tests use it only as a /// data-free refusal fixture). #[test] fn campaign_run_real_e2e_sweep_monte_carlo_per_survivor() { let (dir, _fixture) = fresh_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-persurvivor-seed"); let proc_id = register_process_doc(&dir, "mc.process.json", MC_PROCESS_DOC); // Same GER40 Sept-2024 window as the gated wf e2e above. write_doc( &dir, "mc.campaign.json", &campaign_doc_json(&bp_id, &proc_id, (1725148800000, 1727740799999), "", ""), ); let (out, code) = run_code_in(&dir, &["campaign", "run", "mc.campaign.json"]); // Skip on a data-less machine: the member-data refusal, never a panic. if code == Some(3) && (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"); let stages = cells[0]["stages"].as_array().expect("stages array"); assert_eq!(stages.len(), 2, "sweep + monte_carlo realized: {record_line}"); assert_eq!( stages[1]["block"].as_str(), Some("std::monte_carlo"), "the annotator stage is realized last: {record_line}" ); let per_survivor = stages[1]["bootstrap"]["per_survivor"] .as_array() .expect("no walk_forward precedes: the PerSurvivor arm, not pooled_oos"); assert_eq!( per_survivor.len(), 4, "one bootstrap per surviving sweep member (4 axis combinations, no gate): {record_line}" ); let (ordinal, bootstrap) = (per_survivor[0][0].as_u64(), &per_survivor[0][1]); assert_eq!(ordinal, Some(0), "each entry is keyed by ordinal into the sweep family: {record_line}"); assert_eq!( bootstrap["n_resamples"].as_u64(), Some(100), "the bootstrap carries the doc's resamples verbatim: {record_line}" ); } /// `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: with the [1, 2] /// 1970 window, both runs pass the (now mc-permitting) preflight and the one /// cell fails at the member-data seam — no recorded geometry on a data-less /// host, no overlapping archive window on a data-ful one — #272: the failed /// cell is contained, so the campaign run still completes (exit 3, "completed /// with failed cells") and byte-identical prose either way (the message /// depends only on env/instrument/window). #[test] fn campaign_run_by_content_id_matches_file_sugar_refusal() { let (dir, _fixture) = fresh_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(3), "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(3), "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!( file_line.contains("no recorded geometry") || file_line.contains("no data for instrument"), "the refusal is the member-data seam (mc processes execute now): {file_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 /// member-data seam (the mc process is executable in v2; the [1, 2] 1970 /// window guarantees a deterministic data refusal), so no real data is /// needed. #[test] fn campaign_run_tolerates_content_prefix_on_target() { let (dir, _fixture) = fresh_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!( bare_line.contains("no recorded geometry") || bare_line.contains("no data for instrument"), "the refusal is the member-data seam (mc processes execute now): {bare_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 (dir, _fixture) = fresh_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 closed_bp = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR")); let (reg_out, reg_code) = run_code_in(&dir, &["graph", "register", &closed_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(3), "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 (dir, _fixture) = fresh_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" ); } /// #272: a two-window campaign over GAPSYM where one window falls inside the /// March-April hole records that cell as failed (no_data) while the covered /// window's cell completes and persists — exit 3, both cells in one run record. #[test] fn campaign_over_a_gapped_archive_records_the_uncovered_cell_and_continues() { let (dir, _fixture) = fresh_project_with_gapped_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("gap.process.json")), ScratchPath::File(dir.join("gap.campaign.json")), ]); let bp_id = seed_blueprint(&dir, "campaign-run-gap-seed"); let proc_id = register_process_doc(&dir, "gap.process.json", SWEEP_ONLY_PROCESS_DOC); // Window 0 (2024-01, Unix-ms [1704067200000, 1706745600000)) is fully // archived by `fresh_project_with_gapped_data` (GAPSYM 2024-01..02); // window 1 (2024-03, [1709251200000, 1711929600000)) falls entirely // inside the deliberate March-April hole. let doc = format!( r#"{{ "format_version": 1, "kind": "campaign", "name": "gap-cells", "data": {{ "instruments": ["GAPSYM"], "windows": [ {{ "from_ms": 1704067200000, "to_ms": 1706745600000 }}, {{ "from_ms": 1709251200000, "to_ms": 1711929600000 }} ] }}, "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": [] }} }}"#, ); write_doc(&dir, "gap.campaign.json", &doc); let (out, code) = run_code_in(&dir, &["campaign", "run", "gap.campaign.json"]); assert_eq!(code, Some(3), "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(), 2, "one cell per window, both in one run record: {record_line}"); let jan_cell = &cells[0]; let mar_cell = &cells[1]; assert!(jan_cell["fault"].is_null(), "the January cell completes: {record_line}"); assert!( !jan_cell["stages"].as_array().expect("stages").is_empty(), "the January cell realizes a walk-through stage set: {record_line}" ); assert!( jan_cell["coverage"].is_object(), "the covered window's cell is still only trading-hours-covered inside \ a calendar-month request, so it carries a coverage annotation: {record_line}" ); assert_eq!( mar_cell["fault"]["kind"].as_str(), Some("no_data"), "the March cell (entirely inside the gap) fails as no_data: {record_line}" ); assert!( out.contains("1 failed"), "the campaign-run summary names the one failed cell: {out}" ); }