test(cli): characterization pins ahead of the sweep dissolution

Two green pins the sweep-dissolution cycle rests on (refs #210):

- sweep_real_blueprint_member_lines_pin_the_inline_contract: the inline
  real-data blueprint sweep prints one member line per grid point in
  odometer axis order, carries the swept bindings in manifest.params,
  persists exactly one Sweep family — and never stamps an instrument
  key (RunManifest::instrument stays None on this path).
- campaign_run_real_e2e_sweep_gate_walkforward (extended): every
  family_table member line emitted by the real campaign member-runner
  path is stamped with the instrument under test.

Together they pin the one behavioural delta between the two paths (the
additive instrument stamp) that the dissolution's parity contract is
phrased against.
This commit is contained in:
2026-07-04 16:31:30 +02:00
parent e7c7bded3e
commit 8235cee4d7
2 changed files with 110 additions and 1 deletions
+91
View File
@@ -1397,6 +1397,97 @@ fn sweep_real_is_byte_deterministic_across_runs() {
assert_eq!(run(), run(), "same real sweep twice must be byte-identical (C1)"); assert_eq!(run(), run(), "same real sweep twice must be byte-identical (C1)");
} }
// GER40 Sept-2024 window (inclusive Unix-ms) — the same gated calendar month
// used by `run_real_sidecar_index_pip_reaches_the_emitted_manifest` and the
// campaign-path e2e in `research_docs.rs`.
const GER40_SEPT2024_FROM_MS: &str = "1725148800000";
const GER40_SEPT2024_TO_MS: &str = "1727740799999";
/// Property (docs/specs/sweep-dissolution.md, Testing strategy 1 — the
/// characterization pin ahead of the sweep-dissolution re-cut): today's inline
/// `aura sweep <blueprint.json> --real SYM --axis …` path (`run_blueprint_sweep`)
/// prints one member line per grid point, IN AXIS ORDER (odometer: the first
/// `--axis` varies slowest), each line JSON with a `family_id` and a `report`
/// whose `manifest.params` carries every swept param binding — and, the
/// load-bearing pin the dissolution cycle must preserve unchanged, **`report.manifest`
/// carries NO `instrument` key at all** (the inline path leaves `RunManifest::instrument`
/// `None`, and `#[serde(skip_serializing_if = "Option::is_none")]` omits the key
/// entirely from the wire form — contrast the campaign substrate's stamped sibling,
/// `campaign_run_real_e2e_sweep_gate_walkforward` in `research_docs.rs`). Also pins
/// that the run persists exactly one new `FamilyKind::Sweep` family with the grid's
/// member count. Gated on the local GER40 archive (the project's skip-on-no-data
/// convention); skips cleanly when absent.
#[test]
fn sweep_real_blueprint_member_lines_pin_the_inline_contract() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let dir = temp_cwd("sweep_real_blueprint");
let fixture = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(BIN)
.args([
"sweep", &fixture,
"--real", "GER40",
"--from", GER40_SEPT2024_FROM_MS,
"--to", GER40_SEPT2024_TO_MS,
"--axis", "sma_signal.fast.length=2,4",
"--axis", "sma_signal.slow.length=8,16",
"--name", "brp",
])
.current_dir(&dir)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "one member line per 2x2 grid point: {stdout}");
// Odometer order: `sma_signal.fast.length` (first `--axis`, outer/slowest)
// x `sma_signal.slow.length` (second `--axis`, inner) -> (2,8),(2,16),(4,8),(4,16).
let expected: [[(&str, i64); 2]; 4] = [
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 2), ("sma_signal.slow.length", 16)],
[("sma_signal.fast.length", 4), ("sma_signal.slow.length", 8)],
[("sma_signal.fast.length", 4), ("sma_signal.slow.length", 16)],
];
for i in 0..4 {
let line = lines[i];
let v: serde_json::Value = serde_json::from_str(line).expect("member line parses as JSON");
assert!(v["family_id"].as_str().is_some(), "member line carries a family_id: {line}");
let manifest = &v["report"]["manifest"];
assert!(manifest.is_object(), "report.manifest is present: {line}");
let params = manifest["params"].as_array().expect("manifest.params is an array");
for (name, value) in expected[i] {
let bound = params
.iter()
.find(|p| p[0].as_str() == Some(name))
.and_then(|p| p[1]["I64"].as_i64());
assert_eq!(bound, Some(value), "manifest carries the swept binding {name}={value}: {line}");
}
// the load-bearing pin: the inline real-data path never stamps `instrument`.
assert!(
manifest.get("instrument").is_none(),
"the inline blueprint sweep must NOT stamp an instrument key: {line}"
);
}
// exactly one new Sweep family persisted, with the grid's four members.
let fams = std::process::Command::new(BIN)
.args(["runs", "families"])
.current_dir(&dir)
.output()
.unwrap();
assert!(fams.status.success(), "families exit: {:?}", fams.status);
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
assert_eq!(fams_out.lines().count(), 1, "exactly one family persisted: {fams_out}");
assert!(fams_out.contains("\"kind\":\"Sweep\""), "families: {fams_out}");
assert!(fams_out.contains("\"members\":4"), "2x2 grid -> four members: {fams_out}");
let _ = std::fs::remove_dir_all(&dir);
}
/// The synthetic seed-resweep `mc` is undefined over real bars (one realization -> /// The synthetic seed-resweep `mc` is undefined over real bars (one realization ->
/// identical members), so a bare `aura mc --real EURUSD` — with no R candidate — is /// identical members), so a bare `aura mc --real EURUSD` — with no R candidate — is
/// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is /// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is
+19 -1
View File
@@ -1493,10 +1493,28 @@ fn campaign_run_real_e2e_sweep_gate_walkforward() {
); );
// Emit-gated lines: per-member family_table lines (4 sweep members plus // Emit-gated lines: per-member family_table lines (4 sweep members plus
// the walk-forward OOS members) and at least one selection_report line. // 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!( assert!(
out.lines().filter(|l| l.starts_with("{\"family_id\":")).count() >= 4, member_lines.len() >= 4,
"family_table member lines emitted: {out}" "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!( assert!(
out.lines().any(|l| l.starts_with("{\"selection_report\":")), out.lines().any(|l| l.starts_with("{\"selection_report\":")),
"selection_report line emitted: {out}" "selection_report line emitted: {out}"