Files
Aura/crates/aura-cli/tests/research_docs.rs
T
claude bb0b0aeac2 feat(analysis,campaign,registry,cli): bootstrap net R through the process pipeline
The OOS bootstrap conduit RMetrics.trade_rs becomes net_trade_rs and
carries the COST-NETTED per-trade R (r − cost_in_r, trade order). Every
conduit consumer is thereby net-when-costed: the monte-carlo pooled-OOS
and per-survivor bootstraps, the walk-forward oos_r pooling, and the
deflation null-max — which previously compared a net observed statistic
against a gross-resampled null whenever a costed campaign selected on
net_expectancy_r. An uncosted run is bit-identical (empty cost stream
⇒ cost 0.0 per trade), so every existing golden pin stays green.

Design: one conduit, no knob — an explicit net: knob would let a costed
campaign silently produce a gross headline again (the exact misreading
of the issue's evidence). Per-member RMetrics scalar fields stay gross;
net_expectancy_r keeps its meaning. Wire shape unchanged (serde(skip)).
The net series is materialized as a separate expression in summarize_r;
the pinned-float expressions (net_sum, the SQN pair, the lockstep
r_metrics_from_rs copies) keep their tokens verbatim. Fork decisions
and rationales: issue #259 comments.

New coverage, both hostless over the synthetic SYMA archive: a costed
campaign twin shifts the pooled-OOS bootstrap (sweep→gate→wf→mc) and
every per-survivor bootstrap (sweep→mc) below its gross sibling, with
the trade population unchanged; a unit test pins the conduit as the
cost-netted series with the empty-stream degeneracy. Existing conduit
tests renamed with the field.

Verified: full workspace suite green (71 result blocks), clippy clean,
zero bare trade_rs tokens remain, ledger conduit prose updated in place.

closes #259
2026-07-13 21:21:41 +02:00

3412 lines
160 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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};
/// 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<i32>) {
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args(args)
.current_dir(dir)
.output()
.expect("binary runs");
let text = format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
(text, out.status.code())
}
fn run_code(args: &[&str]) -> (String, Option<i32>) {
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 <id> (<path>)`.
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}");
}
/// #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 } }; absent = one default regime"),
"the block description must name the regime shape: {out}"
);
assert!(
out.contains("optional, list of stop regimes { vol: { 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 } }; 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 } }; 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 } }; absent = one default regime)"
),
"an explicit empty risk array must still count as an open slot: {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 <id> (<path>)`.
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 <ID> | --unwired <FILE> | --content-id <FILE>"),
"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::<Vec<_>>().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)
}
/// 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 <id>` 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}"
);
}
/// 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 (Task-3-doomed) 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 refuse at 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) — exit 1 either way, and no
/// trace output (tracing never starts on a run that dies before members;
/// there is no "traces persisted" summary line). NOTE: the 0107
/// loud-deferral eprintln still fires in this task's tree and is
/// deliberately NOT asserted either way here — Task 3 deletes it and
/// tightens this test to pin its absence.
#[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(1), "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("traces persisted"),
"no trace summary on a run that never reached a member: {out}"
);
assert!(
!out.contains("persist_taps not yet honored"),
"the 0107 deferral line is retired; persist_taps is honored from 0109 on: {out}"
);
assert!(
!out.contains("traces persisted:"),
"a member-data refusal aborts inside execute, before any tracing: {out}"
);
}
/// 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/<name>/<cell_key>/`
/// (the persist re-run equality-asserted against the recorded nominee —
/// C1), and `aura chart <name>/<cell_key>` 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(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let record_line = out
.lines()
.find(|l| l.starts_with("{\"campaign_run\":"))
.expect("the always-on final campaign_run line");
let v: serde_json::Value =
serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
let cells = v["campaign_run"]["cells"].as_array().expect("cells array");
assert_eq!(cells.len(), 1, "one (strategy, instrument, window) cell: {record_line}");
let stages = cells[0]["stages"].as_array().expect("stages array");
assert_eq!(
stages.len(),
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/<name>/<cell_key>/ — the cell key is
// content-derived: strategy8-instrument-w<window_ordinal>.
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}"
);
}
}
/// 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(1)
&& (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(1)
&& (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(1)
&& (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(1)
&& (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/<name>/<cell_key>/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(1)
&& (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(1)
&& (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(1)
&& (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(1)
&& (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/<name>/, 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<String> = 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(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let record_line = out
.lines()
.find(|l| l.starts_with("{\"campaign_run\":"))
.expect("the always-on final campaign_run line");
let v: serde_json::Value =
serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
let 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::<Vec<_>>()
.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(1)
&& (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::<serde_json::Value>(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(1)
&& (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(1)
&& (out.contains("no recorded geometry") || out.contains("no data for instrument"))
{
eprintln!("skip: no local GER40 data for the campaign e2e");
return;
}
assert_eq!(code, Some(0), "stdout/stderr: {out}");
let record_line = out
.lines()
.find(|l| l.starts_with("{\"campaign_run\":"))
.expect("the always-on final campaign_run line");
let v: serde_json::Value =
serde_json::from_str(record_line).expect("campaign_run line parses as JSON");
let cells = v["campaign_run"]["cells"].as_array().expect("cells array");
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 refuse
/// at the member-data seam — no recorded geometry on a data-less host, no
/// overlapping archive window on a data-ful one — exit 1 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(1), "stdout/stderr: {file_out}");
let (reg_out, reg_code) = run_code_in(&dir, &["campaign", "register", "mc2.campaign.json"]);
assert_eq!(reg_code, Some(0), "register failed: {reg_out}");
let id = reg_out
.lines()
.find(|l| l.starts_with("registered campaign "))
.expect("register line")
.trim_start_matches("registered campaign ")
.split(' ')
.next()
.expect("id")
.trim_start_matches("content:")
.to_string();
let (id_out, id_code) = run_code_in(&dir, &["campaign", "run", &id]);
assert_eq!(id_code, Some(1), "stdout/stderr: {id_out}");
let file_line = file_out.lines().find(|l| l.starts_with("aura: ")).expect("file refusal line");
let id_line = id_out.lines().find(|l| l.starts_with("aura: ")).expect("id refusal line");
assert!(
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(1), "stdout/stderr: {out}");
assert!(
out.contains("no recorded geometry") || out.contains("no data for instrument"),
"referencing + axis-binding must succeed, reaching the member-data \
seam rather than a referential or bind refusal: {out}"
);
}
/// `campaign run`'s file-sugar branch calls `parse_valid_campaign` directly
/// (unwrapped), the SAME intrinsic validator `campaign register` calls but
/// wraps in its own "refusing to register:" prefix. This pins that `run`
/// does NOT inherit register's wrapper — an intrinsically invalid document
/// refuses with the bare "campaign document invalid:" prose — and, like
/// register, never touches the store for it. No project scaffolding, no
/// data, no seeded blueprint needed: the refusal fires before any of that is
/// read.
#[test]
fn campaign_run_invalid_file_refuses_before_touching_store() {
// Run inside the built project (campaign run needs one, per the project
// gate); the document fails intrinsic validation before any store or
// referential check is reached, so the project's own store is untouched.
let (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"
);
}