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}");
|
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 —
|
/// Property (#210 T3, dissolution): omitting `--from`/`--to` still completes —
|
||||||
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
|
/// the dispatch rewrite's window-resolution fallback (`dispatch_generalize` in
|
||||||
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
|
/// main.rs) resolves ONE shared campaign window from the FIRST listed symbol's
|
||||||
|
|||||||
Reference in New Issue
Block a user