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:
2026-07-06 18:00:07 +02:00
parent 256ec7320c
commit f5bb38ed59
+187
View File
@@ -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