test(cli): synthetic per-test M1 archive — no-window generalize tests go hostless

The two generalize E2E tests whose property is no-window span
resolution (shared-window fallback, intersection semantics) no longer
stream the 6.6 GB host archive: fresh_project_with_data() generates a
deterministic two-symbol M1 archive (SYMA 2024-01..08, SYMB 2024-03..06
strictly inside it, so the intersection differs from symbols[0]'s
window on both bounds) in the exact zip/48-byte-record format
data-server reads, plus geometry sidecars, under the per-test project's
paths.data. Both tests drop their local_data_present gates — they now
run on any host, data mount or not.

~35s and ~50s become 0.74s and 1.14s; the full cli_run binary lands at
~25s (211s at the branch base). Full workspace suite: 247s -> 45.7s.

refs #250
This commit is contained in:
2026-07-13 15:24:00 +02:00
parent 25b8354959
commit c31e946bb7
4 changed files with 226 additions and 52 deletions
+25 -34
View File
@@ -5,7 +5,7 @@ use std::path::Path;
use std::process::Command;
mod common;
use common::fresh_project;
use common::{fresh_project, fresh_project_with_data};
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
@@ -3594,30 +3594,24 @@ fn generalize_distinct_invocations_persist_distinct_campaign_documents() {
/// independent full window). A wrong intersection (or a panic on the
/// per-symbol probe) in that fallback would only surface when `--from`/`--to`
/// are absent — every other generalize e2e pins the explicit-window path,
/// leaving this one unexercised. Gated on local GER40/USDJPY data.
/// leaving this one unexercised. The property is the no-window RESOLUTION
/// mechanics, not any particular archive's span, so it runs against
/// `fresh_project_with_data`'s tiny synthetic two-symbol archive (#250) —
/// hostless, no data-refusal skip arm needed.
#[test]
fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
let (cwd, _g) = fresh_project();
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "GER40,USDJPY",
"generalize", &fixture, "--real", "SYMA,SYMB",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--stop-length", "14", "--stop-k", "2.0",
])
.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);
assert_eq!(out.status.code(), Some(0), "exit: {:?}, stderr: {}", out.status, String::from_utf8_lossy(&out.stderr));
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(stdout.contains("\"generalize\":"), "aggregate object: {stdout}");
assert!(stdout.contains("\"n_instruments\":2"), "two instruments graded: {stdout}");
@@ -3633,24 +3627,21 @@ fn generalize_without_explicit_window_resolves_a_shared_window_and_completes() {
/// instrument conflates the instrument axis with the period axis, so the common
/// window is the only one that isolates what the verb measures. Today's fallback
/// picks `symbols[0]`'s full window alone (the superseded shape test above):
/// listing the WIDER symbol first (`AAPL.US`, archive from 2006) keeps AAPL's
/// early start, whereas the intersection with the later-starting `GER40` (archive
/// from 2014) must start at GER40's first bar and end at whichever archive ends
/// first — so on the current tree the resolved window differs from the
/// listing the WIDER synthetic symbol first (`SYMA`, spanning 2024-01..08) keeps
/// SYMA's early start, whereas the intersection with `SYMB` (spanning 2024-03..06,
/// strictly inside SYMA's span) must start at SYMB's first bar and end at SYMB's
/// last — so on the current tree the resolved window differs from the
/// intersection on both bounds. Each symbol's full window is resolved
/// independently here: a single-symbol `sweep` with no window records it into its
/// generated campaign document, in the same Unix-ms currency and via the same
/// `campaign_window_ms(full_window)` probe generalize itself resolves against —
/// so the expected intersection is derived from the live archive, never a
/// hard-coded span that shifts as the archive grows. Archive-gated; skips
/// cleanly on a data-less host.
/// hard-coded span. The archive's SPAN SHAPE is authored (#250,
/// `fresh_project_with_data`), not its size, so this runs hostless in
/// milliseconds instead of streaming two full host archives.
#[test]
fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols() {
if !local_data_present() {
eprintln!("skip: no local data at {}", data_server::DEFAULT_DATA_PATH);
return;
}
let (cwd, _g) = fresh_project();
let (cwd, _g) = fresh_project_with_data();
let fixture = format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
// The resolved shared window persists verbatim in the generated campaign
@@ -3694,24 +3685,24 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols()
window_of(name)
};
let aapl = probe("AAPL.US", "probe-aapl");
let ger40 = probe("GER40", "probe-ger40");
let syma = probe("SYMA", "probe-syma");
let symb = probe("SYMB", "probe-symb");
// Precondition: the archives genuinely differ, so "the intersection" is a
// distinct claim from "symbols[0]'s full window" (else the test is vacuous).
assert_ne!(
aapl, ger40,
"AAPL.US {aapl:?} and GER40 {ger40:?} must span different windows for the test to bite"
syma, symb,
"SYMA {syma:?} and SYMB {symb:?} must span different windows for the test to bite"
);
// The contract: the shared window is the intersection — latest start, earliest end.
let expected = (aapl.0.max(ger40.0), aapl.1.min(ger40.1));
let expected = (syma.0.max(symb.0), syma.1.min(symb.1));
// AAPL.US listed FIRST: today's `symbols[0]` fallback keeps AAPL's (wider)
// SYMA listed FIRST: today's `symbols[0]` fallback keeps SYMA's (wider)
// window, which differs from the intersection on both bounds — so a pass here
// can only mean the intersection semantics landed.
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
.args([
"generalize", &fixture, "--real", "AAPL.US,GER40",
"generalize", &fixture, "--real", "SYMA,SYMB",
"--axis", "sma_signal.fast.length=3", "--axis", "sma_signal.slow.length=12",
"--name", "gen-both",
])
@@ -3727,8 +3718,8 @@ fn generalize_without_explicit_window_resolves_the_intersection_of_all_symbols()
assert_eq!(
resolved, expected,
"no-window generalize must resolve the INTERSECTION {expected:?} of AAPL.US {aapl:?} \
and GER40 {ger40:?} not symbols[0]'s ({aapl:?}) full window (resolved {resolved:?})"
"no-window generalize must resolve the INTERSECTION {expected:?} of SYMA {syma:?} \
and SYMB {symb:?} not symbols[0]'s ({syma:?}) full window (resolved {resolved:?})"
);
}