test(cli): dissolution-proof e2e for generalize (#210 T4)
`generalize_dissolves_through_the_campaign_path` asserts the verb now runs through the one campaign path: exactly one generated process document, one generated campaign document (carrying the --name handle and the stop as a non-empty risk regime), and one campaign-run record are durably auto-registered, plus the campaign-path family set (1 CrossInstrument grade family + 2 per-instrument Sweep families). The observable proof the inline path is gone. refs #210
This commit is contained in:
@@ -3637,6 +3637,193 @@ fn generalize_real_e2e_pins_the_exact_current_grade() {
|
||||
assert_eq!(per[1][1].as_f64(), Some(0.005795903617609842), "USDJPY expectancy R: {grade_line}");
|
||||
}
|
||||
|
||||
/// Property: `aura generalize` is now thin sugar over the one campaign path — a
|
||||
/// successful run durably auto-registers exactly one generated process document,
|
||||
/// one generated campaign document (carrying the `--name` handle and the stop as
|
||||
/// a non-empty single risk regime), and one campaign-run record. This is the
|
||||
/// observable proof the inline path is gone and the dissolution runs through the
|
||||
/// executor. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
|
||||
/// a data refusal.
|
||||
#[test]
|
||||
fn generalize_dissolves_through_the_campaign_path() {
|
||||
const FROM_MS: &str = "1725148800000";
|
||||
const TO_MS: &str = "1727740799999";
|
||||
let cwd = temp_cwd("generalize-dissolves");
|
||||
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura");
|
||||
if out.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40/USDJPY data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(out.status.code(), Some(0), "exit: {:?}", out.status);
|
||||
|
||||
let count = |sub: &str| {
|
||||
std::fs::read_dir(cwd.join("runs").join(sub))
|
||||
.map(|d| d.count())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
assert_eq!(count("processes"), 1, "one generated process document registered");
|
||||
assert_eq!(count("campaigns"), 1, "one generated campaign document registered");
|
||||
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
|
||||
.expect("campaign_runs.jsonl exists after a sugar run");
|
||||
assert_eq!(runs_log.lines().count(), 1, "one campaign run recorded");
|
||||
|
||||
let campaigns_dir = cwd.join("runs").join("campaigns");
|
||||
let campaign_doc_path = std::fs::read_dir(&campaigns_dir)
|
||||
.expect("campaigns dir exists")
|
||||
.next()
|
||||
.expect("exactly one campaign document")
|
||||
.expect("readable dir entry")
|
||||
.path();
|
||||
let campaign_doc_json = std::fs::read_to_string(&campaign_doc_path).expect("read campaign doc");
|
||||
assert!(
|
||||
campaign_doc_json.contains("\"name\":\"generalize\""),
|
||||
"the generated campaign document carries the --name handle: {campaign_doc_json}"
|
||||
);
|
||||
// The structural delta vs the sweep translator: the stop is a non-empty
|
||||
// single risk regime, not empty risk.
|
||||
assert!(
|
||||
campaign_doc_json.contains("\"risk\":[")
|
||||
&& campaign_doc_json.contains("\"length\":14")
|
||||
&& campaign_doc_json.contains("\"k\":2.0"),
|
||||
"the stop rides a non-empty risk regime: {campaign_doc_json}"
|
||||
);
|
||||
|
||||
// The campaign path's family set (#210 Q4): exactly one CrossInstrument grade
|
||||
// family (the generalize result, appended by the sugar) plus one
|
||||
// per-instrument Sweep family per cell (persisted by the executor).
|
||||
let fams = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args(["runs", "families"])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn families");
|
||||
let fams_out = String::from_utf8_lossy(&fams.stdout).into_owned();
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"CrossInstrument\"")).count(),
|
||||
1,
|
||||
"one CrossInstrument grade family: {fams_out}"
|
||||
);
|
||||
assert_eq!(
|
||||
fams_out.lines().filter(|l| l.contains("\"kind\":\"Sweep\"")).count(),
|
||||
2,
|
||||
"one per-instrument Sweep family per cell: {fams_out}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Property (#210, dissolution — content-addressed idempotency): re-running the
|
||||
/// IDENTICAL `aura generalize` invocation in the same project does not litter the
|
||||
/// store with a second generated process/campaign document — `register_generated_g`
|
||||
/// writes under a content-id filename, so two runs of the same candidate collapse
|
||||
/// onto the same one process doc and one campaign doc, while each invocation still
|
||||
/// records its OWN campaign-run line (a run is an event, a document is content).
|
||||
/// Were the sugar to salt its generated documents with anything non-deterministic
|
||||
/// (e.g. a timestamp), this would regress from 1 to 2 stored documents per repeat
|
||||
/// invocation — the store-litter regression the campaign-path dissolution must not
|
||||
/// introduce. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on
|
||||
/// a data refusal.
|
||||
#[test]
|
||||
fn generalize_repeated_identical_invocation_does_not_litter_the_store() {
|
||||
const FROM_MS: &str = "1725148800000";
|
||||
const TO_MS: &str = "1727740799999";
|
||||
let cwd = temp_cwd("generalize-repeat-idempotent");
|
||||
let invoke = || {
|
||||
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
|
||||
"--fast", "3", "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura")
|
||||
};
|
||||
let first = invoke();
|
||||
if first.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&first.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40/USDJPY data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
|
||||
let second = invoke();
|
||||
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
|
||||
|
||||
let count = |sub: &str| {
|
||||
std::fs::read_dir(cwd.join("runs").join(sub))
|
||||
.map(|d| d.count())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
assert_eq!(count("processes"), 1, "two identical invocations share one process document");
|
||||
assert_eq!(count("campaigns"), 1, "two identical invocations share one campaign document");
|
||||
let runs_log = std::fs::read_to_string(cwd.join("runs").join("campaign_runs.jsonl"))
|
||||
.expect("campaign_runs.jsonl exists");
|
||||
assert_eq!(runs_log.lines().count(), 2, "each invocation still records its own campaign run");
|
||||
}
|
||||
|
||||
/// Property (#210, dissolution — content-addressed distinctness): the idempotency
|
||||
/// above is not a bug masquerading as a feature — two DIFFERENT `--fast`
|
||||
/// invocations (differing candidate content) generate and persist TWO distinct
|
||||
/// campaign documents (and two distinct process documents, since the `--metric`
|
||||
/// text is unchanged so the process content matches — pinned via the campaign
|
||||
/// count only, the axis differs on the campaign, not the process). Together with
|
||||
/// `generalize_repeated_identical_invocation_does_not_litter_the_store`, this rules
|
||||
/// out a store that always overwrites one fixed filename regardless of content —
|
||||
/// the failure mode that would make the idempotency test above pass for the wrong
|
||||
/// reason. Gated on the shared GER40/USDJPY Sept-2024 archive; skips cleanly on a
|
||||
/// data refusal.
|
||||
#[test]
|
||||
fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
|
||||
const FROM_MS: &str = "1725148800000";
|
||||
const TO_MS: &str = "1727740799999";
|
||||
let cwd = temp_cwd("generalize-distinct-content");
|
||||
let invoke = |fast: &str| {
|
||||
std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
||||
.args([
|
||||
"generalize", "--strategy", "r-sma", "--real", "GER40,USDJPY",
|
||||
"--fast", fast, "--slow", "12", "--stop-length", "14", "--stop-k", "2.0",
|
||||
"--from", FROM_MS, "--to", TO_MS,
|
||||
])
|
||||
.current_dir(&cwd)
|
||||
.output()
|
||||
.expect("spawn aura")
|
||||
};
|
||||
let first = invoke("3");
|
||||
if first.status.code() == Some(1) {
|
||||
let stderr = String::from_utf8_lossy(&first.stderr);
|
||||
assert!(
|
||||
stderr.contains("no local data") || stderr.contains("no recorded geometry"),
|
||||
"exit 1 must be a data refusal, got: {stderr}"
|
||||
);
|
||||
eprintln!("skip: no local GER40/USDJPY data");
|
||||
return;
|
||||
}
|
||||
assert_eq!(first.status.code(), Some(0), "first run exit: {:?}", first.status);
|
||||
let second = invoke("4");
|
||||
assert_eq!(second.status.code(), Some(0), "second run exit: {:?}", second.status);
|
||||
|
||||
let count = |sub: &str| {
|
||||
std::fs::read_dir(cwd.join("runs").join(sub))
|
||||
.map(|d| d.count())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
assert_eq!(count("campaigns"), 2, "two distinct candidates persist two distinct campaign documents");
|
||||
}
|
||||
|
||||
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes —
|
||||
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
|
||||
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
|
||||
|
||||
Reference in New Issue
Block a user