fix(cli): sweep-entry ergonomics — honest refusals, no store litter (#210 c0110 fieldtest)
Three test-first fixes from the cycle-0110 fieldtest findings: - Axis-name preflight on the dissolved sweep arm: every --axis name is checked against the wrapped probe namespace (the --list-axes names) BEFORE the #203 strip; an unknown or raw-form name is refused house-style echoing exactly what the user typed, with a --list-axes pointer — no more stripped-then-mangled fragments from the deep referential refusal. Data-free, fires before the archive is touched. - Validate-before-register in the sweep sugar: intrinsic doc checks + executor preflight + the pure bind_axes coverage check (reused, not re-inlined; now pub(crate)) all run in-memory before register_generated — a refused invocation leaves no generated document in the content-addressed store and no campaign_runs.jsonl line (previously P2/P3 refusals littered runs/campaigns, one doc malformed). - blueprint_slot_prose: the sweep/walkforward/mc loaded-blueprint slots share one dispatch-boundary presenter — an op-script array gets a targeted 'run aura graph build first' hint, every load error goes through blueprint_load_prose + the missing-project hint, and the raw {e:?} Debug leak (#184's pattern, deliberately left on these three slots back then) is gone. Two stale Debug-leak pins flipped, two new refusal tests. Suite 1060/0, clippy -D warnings clean. refs #210
This commit is contained in:
@@ -1615,6 +1615,96 @@ fn sweep_real_blueprint_member_lines_pin_the_dissolved_contract() {
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (#210 c0110 fieldtest, "the wrapped-name refusal mangles the axis
|
||||
/// the user typed"): the dissolved real-data blueprint sweep types `--axis`
|
||||
/// against the WRAPPED probe namespace (`sma_signal.fast.length`, what
|
||||
/// `--list-axes` prints), not the raw campaign-document namespace
|
||||
/// (`fast.length`). A user who types the raw form must be refused up front,
|
||||
/// echoing exactly what they typed — never a stripped-then-mangled fragment
|
||||
/// (the old failure mode: `fast.length` -> strip one segment -> the
|
||||
/// nonsensical bare `"length"`). NOT gated: the preflight is data-free (it
|
||||
/// only reloads the blueprint to probe its param space), so the refusal
|
||||
/// fires before the archive is ever touched — this test needs no local data.
|
||||
#[test]
|
||||
fn aura_sweep_real_blueprint_refuses_a_raw_form_axis_name_before_data_access() {
|
||||
let dir = temp_cwd("sweep_real_rawname_refusal");
|
||||
let fixture = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = Command::new(BIN)
|
||||
.args([
|
||||
"sweep", &fixture,
|
||||
"--real", "GER40",
|
||||
"--from", GER40_SEPT2024_FROM_MS,
|
||||
"--to", GER40_SEPT2024_TO_MS,
|
||||
"--axis", "fast.length=2,4",
|
||||
"--name", "c0110-rawname",
|
||||
])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura sweep (raw-form axis)");
|
||||
assert_eq!(out.status.code(), Some(2), "a raw-form axis name must be refused, not run: status={:?}", out.status);
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
stderr.contains("axis \"fast.length\""),
|
||||
"the refusal echoes exactly the name the user typed: {stderr}"
|
||||
);
|
||||
assert!(
|
||||
!stderr.contains("axis \"length\""),
|
||||
"must never mangle the typed axis into a stripped fragment: {stderr}"
|
||||
);
|
||||
// Data-free: no archive/store access at all.
|
||||
assert!(!dir.join("runs").exists(), "a refused axis name must not start a real run");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// Property (#210 c0110 fieldtest, "refused dissolved sweeps still register
|
||||
/// their generated campaign document" — store litter, probe P3): binding only
|
||||
/// a SUBSET of the blueprint's open axes (here: `fast.length`, leaving
|
||||
/// `slow.length` unbound) is a referential refusal at member-run time — but
|
||||
/// the sugar must validate the generated documents (including the
|
||||
/// axis-binding shape) BEFORE `register_generated`, so the refusal leaves
|
||||
/// `runs/processes/` and `runs/campaigns/` untouched and appends no
|
||||
/// `campaign_runs.jsonl` record. Gated on the local GER40 archive (the
|
||||
/// referential refusal is reached only after the archive is probed for the
|
||||
/// window, same as the sibling dissolved-sweep pin above).
|
||||
#[test]
|
||||
fn aura_sweep_real_blueprint_subset_axes_refuses_without_store_litter() {
|
||||
if !local_data_present() {
|
||||
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
|
||||
return;
|
||||
}
|
||||
let dir = temp_cwd("sweep_real_subset_refusal");
|
||||
let fixture = format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
||||
let out = 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",
|
||||
"--name", "c0110-subset",
|
||||
])
|
||||
.current_dir(&dir)
|
||||
.output()
|
||||
.expect("spawn aura sweep (subset axes)");
|
||||
assert_ne!(
|
||||
out.status.code(),
|
||||
Some(0),
|
||||
"leaving slow.length unbound must refuse, not run: stdout={}",
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
);
|
||||
let count = |sub: &str| {
|
||||
std::fs::read_dir(dir.join("runs").join(sub)).map(|d| d.count()).unwrap_or(0)
|
||||
};
|
||||
assert_eq!(count("processes"), 0, "a refused sweep must not register a generated process document");
|
||||
assert_eq!(count("campaigns"), 0, "a refused sweep must not register a generated campaign document");
|
||||
let runs_log = dir.join("runs").join("campaign_runs.jsonl");
|
||||
assert!(
|
||||
!runs_log.exists() || std::fs::read_to_string(&runs_log).unwrap().is_empty(),
|
||||
"no realization recorded for a refused sweep"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// REJECTED (usage on stderr, exit 2) at the binary boundary. The real path is
|
||||
@@ -3934,8 +4024,10 @@ fn aura_walkforward_over_a_blueprint_rejects_no_axis() {
|
||||
assert!(stderr.contains("requires >= 1 --axis"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#173, safety): a malformed blueprint is rejected at the dispatch boundary
|
||||
/// (exit 2, UnknownNodeType) rather than panicking.
|
||||
/// E2E (#173/#184, safety): a malformed blueprint is rejected at the dispatch
|
||||
/// boundary (exit 2) rather than panicking, phrased house-style (#184's
|
||||
/// `blueprint_load_prose` convention) — never the raw `UnknownNodeType` Debug
|
||||
/// leak (c0110 fieldtest finding, mirroring `aura_run_rejects_an_unknown_node_blueprint`).
|
||||
#[test]
|
||||
fn aura_walkforward_over_a_blueprint_rejects_malformed() {
|
||||
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@@ -3945,7 +4037,8 @@ fn aura_walkforward_over_a_blueprint_rejects_malformed() {
|
||||
.expect("spawn aura walkforward (malformed)");
|
||||
assert_eq!(out.status.code(), Some(2), "malformed must be rejected, not panic");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("UnknownNodeType"), "stderr: {stderr}");
|
||||
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
|
||||
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
|
||||
assert!(!stderr.contains("panicked"), "must reject cleanly: {stderr}");
|
||||
}
|
||||
|
||||
@@ -4182,9 +4275,11 @@ fn aura_sweep_list_axes_rejects_other_flags() {
|
||||
assert!(stderr.contains("--list-axes lists axes"), "stderr was: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#169, safety): `--list-axes` over a malformed blueprint rejects cleanly
|
||||
/// (exit 2, naming UnknownNodeType) rather than panicking — the axis probe reloads
|
||||
/// the doc, so the dispatch-boundary validation must run first, never a raw `.expect`.
|
||||
/// E2E (#169/#184, safety): `--list-axes` over a malformed blueprint rejects
|
||||
/// cleanly (exit 2), phrased house-style — never the raw `UnknownNodeType`
|
||||
/// Debug leak (c0110 fieldtest finding) — rather than panicking; the axis
|
||||
/// probe reloads the doc, so the dispatch-boundary validation must run first,
|
||||
/// never a raw `.expect`.
|
||||
#[test]
|
||||
fn aura_sweep_list_axes_rejects_malformed_blueprint() {
|
||||
let bp = format!("{}/tests/fixtures/unknown_node.json", env!("CARGO_MANIFEST_DIR"));
|
||||
@@ -4194,10 +4289,59 @@ fn aura_sweep_list_axes_rejects_malformed_blueprint() {
|
||||
.expect("spawn aura sweep --list-axes (malformed)");
|
||||
assert_eq!(out.status.code(), Some(2), "malformed blueprint must be rejected, not panic");
|
||||
let stderr = String::from_utf8(out.stderr).expect("utf-8");
|
||||
assert!(stderr.contains("UnknownNodeType"), "stderr was: {stderr}");
|
||||
assert!(stderr.contains(r#"unknown node type "Nope""#), "house-style prose names the type: {stderr}");
|
||||
assert!(!stderr.contains("UnknownNodeType"), "does not leak the Debug variant name: {stderr}");
|
||||
assert!(!stderr.contains("panicked"), "must reject cleanly, not panic: {stderr}");
|
||||
}
|
||||
|
||||
/// E2E (#210 c0110 fieldtest, "aura sweep <op-script> rejects the op-script
|
||||
/// array with a raw serde error"): `aura sweep`'s blueprint slot accepts only
|
||||
/// the built blueprint envelope; an un-built op-script (a JSON array) is
|
||||
/// shape-discriminated and refused with a targeted, house-style hint — never
|
||||
/// the leaked `Json(Error("invalid type: map, expected u32", ...))`.
|
||||
#[test]
|
||||
fn aura_sweep_rejects_an_op_script_array_with_a_build_first_hint() {
|
||||
let dir = temp_cwd("sweep_rejects_op_script");
|
||||
let op_script = dir.join("ops.json");
|
||||
std::fs::write(&op_script, r#"[{"op":"source","role":"price","kind":"F64"}]"#)
|
||||
.expect("write op-script fixture");
|
||||
let out = Command::new(BIN)
|
||||
.args(["sweep", op_script.to_str().unwrap(), "--list-axes"])
|
||||
.output()
|
||||
.expect("spawn aura sweep (op-script)");
|
||||
assert_eq!(out.status.code(), Some(2), "an op-script array must be refused, not panic");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
stderr.contains("op-script") && stderr.contains("aura graph build"),
|
||||
"names the op-script cause + the fix: {stderr}"
|
||||
);
|
||||
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde error: {stderr}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// E2E (#210 c0110 fieldtest, house-style loader prose): a genuinely malformed
|
||||
/// (syntactically invalid) JSON document in the sweep blueprint slot is
|
||||
/// refused with `blueprint_load_prose`'s wording, never the raw `{e:?}` Debug
|
||||
/// form the dispatch used to print.
|
||||
#[test]
|
||||
fn aura_sweep_rejects_malformed_json_house_style() {
|
||||
let dir = temp_cwd("sweep_rejects_malformed_json");
|
||||
let bad = dir.join("bad.json");
|
||||
std::fs::write(&bad, "{not valid json").expect("write malformed fixture");
|
||||
let out = Command::new(BIN)
|
||||
.args(["sweep", bad.to_str().unwrap(), "--list-axes"])
|
||||
.output()
|
||||
.expect("spawn aura sweep (malformed json)");
|
||||
assert_eq!(out.status.code(), Some(2), "malformed JSON must be refused, not panic");
|
||||
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
|
||||
assert!(
|
||||
stderr.contains("blueprint document is not valid JSON"),
|
||||
"house-style prose, not a raw Debug leak: {stderr}"
|
||||
);
|
||||
assert!(!stderr.contains("Json(Error"), "does not leak the raw serde Debug form: {stderr}");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// E2E (#169, the load-bearing "listed == swept" invariant): a name emitted by
|
||||
/// `--list-axes` is accepted VERBATIM by `--axis` — the discovered axis namespace
|
||||
/// and the swept axis namespace are the same set (shared-probe construction). The
|
||||
|
||||
Reference in New Issue
Block a user