From e94b79eb9822bd42aa9548d3e2ce1557859f4c62 Mon Sep 17 00:00:00 2001 From: Brummel Date: Thu, 25 Jun 2026 00:11:03 +0200 Subject: [PATCH] =?UTF-8?q?test(stage1-r-sweep):=20RED=20=E2=80=94=20grid?= =?UTF-8?q?=20all=20four=20knobs=20from=20CSV=20flags?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- crates/aura-cli/tests/cli_run.rs | 128 +++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 99c434f..068204a 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -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); +}