test(stage1-r-sweep): RED — grid all four knobs from CSV flags

Executable-spec for the strategy-research enabling change: expose the
stage1-r sweep's hardcoded timescales as CLI grids so a slow
time-series-momentum edge can be screened across timeframes.

- sweep_strategy_stage1_r_grids_all_four_knobs_from_csv_flags (RED):
  `aura sweep --strategy stage1-r --fast 240 --slow 960
  --stop-length 240 --stop-k 2.0` must yield a 1-member family whose
  member params are fast=240, slow=960, stop_length=240, stop_k=2.0.
  Fails today: the four flags are rejected (exit 2), gridding absent.
- sweep_strategy_stage1_r_no_flags_keeps_the_default_grid (guard,
  green today): a no-flags sweep still yields the {2,3}×{6,12} 4-member
  family with the stop pinned at 3 / 2.0, so existing goldens stay
  byte-green once the feature lands.

GREEN side follows via implement mini-mode.

refs #137
This commit is contained in:
2026-06-25 00:11:03 +02:00
parent 28e0331223
commit e94b79eb98
+128
View File
@@ -1826,3 +1826,131 @@ fn sweep_strategy_stage1_r_trace_persists_member_dirs_with_r_equity() {
);
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#137 — headline): `aura sweep --strategy stage1-r` accepts the four
/// optional comma-separated grid flags `--fast`, `--slow`, `--stop-length`,
/// `--stop-k`, which become the sweep's grid axes over the four stage1_r_graph
/// knobs (sma fast length, sma slow length, vol-stop length, vol-stop k). The
/// family is the cartesian product of the supplied lists; a single-value list per
/// axis is a 1-member family. This is the enabling change that lets the stop
/// timescale track a *slow* signal (the pinned 3-bar = 3-minute vol-stop is far too
/// fast for a 240/960-bar momentum cross) — so a slow time-series-momentum edge can
/// be screened across timeframes, stop included.
///
/// Mirrors `sweep_strategy_stage1_r_ranks_a_family_by_sqn`: drive the built binary,
/// read each stdout member line (the assigned `family_id` + the nested RunReport),
/// and inspect the manifest `params` array — whose on-disk shape is
/// `["fast.length",{"I64":N}]` / `["stop_k",{"F64":K}]` (verified against the live
/// CLI output). The single member must carry exactly fast=240, slow=960,
/// stop_length=240, stop_k=2.0. Today every one of these four flags is an unknown
/// token rejected with exit 2 (the grid only honours the hardcoded fast×slow), so
/// this is RED for the right reason: the flags + gridding are absent.
#[test]
fn sweep_strategy_stage1_r_grids_all_four_knobs_from_csv_flags() {
let cwd = temp_cwd("sweep-stage1-r-grid-flags");
let out = Command::new(BIN)
.args([
"sweep",
"--strategy",
"stage1-r",
"--fast",
"240",
"--slow",
"960",
"--stop-length",
"240",
"--stop-k",
"2.0",
"--name",
"g1",
])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy stage1-r with grid flags");
assert!(
out.status.success(),
"gridded stage1-r sweep must succeed (exit 0): {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let lines: Vec<&str> = stdout.lines().collect();
// a one-value-per-axis grid is a 1-member family (cartesian product of singletons).
assert_eq!(
lines.len(),
1,
"single-value lists on all four axes = a 1-member family: {stdout:?}"
);
let member = lines[0];
// the member is a real run (carries an R block), not a "0 ran" no-op.
assert!(member.contains("\"r\":{"), "member must carry an r block: {member}");
// the single member's four griddable knobs take the supplied values — the
// on-disk manifest-params shape (verified against live CLI output).
for needle in [
"[\"fast.length\",{\"I64\":240}]",
"[\"slow.length\",{\"I64\":960}]",
"[\"stop_length\",{\"I64\":240}]",
"[\"stop_k\",{\"F64\":2.0}]",
] {
assert!(
member.contains(needle),
"member must grid {needle} from the CSV flag: {member}"
);
}
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#137 — default preservation, the hard constraint): when the grid flags
/// are ABSENT, `aura sweep --strategy stage1-r` must produce *exactly today's*
/// family — the 2×2 fast×slow grid {2,3}×{6,12} (4 members) with the stop pinned at
/// stop_length=3, stop_k=2.0. This guards the byte-greenness of every existing
/// golden/sweep test once the four flags land: a no-flags invocation falls back to
/// the historical defaults, not to an empty/changed grid. It pins the contract that
/// the flag wiring must not move the no-flags baseline.
#[test]
fn sweep_strategy_stage1_r_no_flags_keeps_the_default_grid() {
let cwd = temp_cwd("sweep-stage1-r-default-grid");
let out = Command::new(BIN)
.args(["sweep", "--strategy", "stage1-r"])
.current_dir(&cwd)
.output()
.expect("spawn aura sweep --strategy stage1-r");
assert!(
out.status.success(),
"no-flags stage1-r sweep exit: {:?}; stderr: {}",
out.status,
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8(out.stdout).expect("utf-8");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 4, "no-flags grid = the historical 2×2 = 4 members: {stdout:?}");
// the four members are exactly the {2,3}×{6,12} fast×slow cartesian product,
// each with the historical pinned stop (stop_length=3, stop_k=2.0).
let mut fast_slow: Vec<(&str, &str)> = Vec::new();
for m in &lines {
assert!(
m.contains("[\"stop_length\",{\"I64\":3}]") && m.contains("[\"stop_k\",{\"F64\":2.0}]"),
"no-flags member must keep the historical pinned stop: {m}"
);
let fast = if m.contains("[\"fast.length\",{\"I64\":2}]") {
"2"
} else {
assert!(m.contains("[\"fast.length\",{\"I64\":3}]"), "fast must be 2 or 3: {m}");
"3"
};
let slow = if m.contains("[\"slow.length\",{\"I64\":6}]") {
"6"
} else {
assert!(m.contains("[\"slow.length\",{\"I64\":12}]"), "slow must be 6 or 12: {m}");
"12"
};
fast_slow.push((fast, slow));
}
fast_slow.sort_unstable();
assert_eq!(
fast_slow,
vec![("2", "12"), ("2", "6"), ("3", "12"), ("3", "6")],
"no-flags grid must be exactly {{2,3}}×{{6,12}}: {fast_slow:?}"
);
let _ = std::fs::remove_dir_all(&cwd);
}