test(cli): RED — chart cannot resolve a --trace family by its chosen name (#238)

--trace <NAME> lands the name in the generated campaign document, but no
lookup surface consumes it: the trace store knows only the content-derived
handle {campaign8}-{run}, so the user cannot address their own family by
the name they chose.

refs #238
This commit is contained in:
2026-07-11 17:20:14 +02:00
parent 2a63c5d889
commit c70b4cc2d2
+119
View File
@@ -650,6 +650,125 @@ fn chart_opens_a_sweep_trace_family_by_its_printed_handle() {
let _ = std::fs::remove_dir_all(&cwd);
}
/// Property (#238): a sweep / walk-forward `--trace <NAME>` family is chartable by
/// the very NAME the user chose — not only by the content-derived handle the run
/// prints. `--trace <NAME>` lands `NAME` in the generated campaign document's
/// `name` field (content-addressed via `put_campaign`), while the trace directory
/// stays keyed by the content-derived handle `{campaign8}-{run}` that
/// `append_campaign_run` re-derives (C18, untouched). Today `aura chart <NAME>`
/// classifies `NAME` as an unknown trace handle and exits 1, so the chosen name
/// has no charting effect. It must instead resolve `NAME` against the recorded
/// campaign runs: the ONE record whose stored campaign document carries
/// `name == NAME` and a persisted `trace_name` charts that family by its handle —
/// exit 0 with the same chart page charting the handle directly renders.
///
/// The state is fabricated in-process, no archive / sweep run: a campaign
/// document carrying a distinctive `name` is stored via `put_campaign`; a
/// campaign-run record with a `Some` trace_name sentinel is appended (which
/// re-derives the handle `{campaign8}-0`); a family is written under that exact
/// handle via `TraceStore` — mirroring the handle-charted sibling above (the
/// #224 depth-2 fan-out). The registry roots where `env.registry()` does
/// (`<cwd>/runs/runs.jsonl`), so the running binary reads the same store. On
/// today's tree `chart <NAME>` hits `emit_chart`'s NotFound arm (no registry
/// query), so it exits 1 with the "no recorded run or family" message — RED for
/// the right reason (the name-resolution is absent, not a fabrication error).
#[test]
fn chart_resolves_a_trace_family_by_its_chosen_campaign_name() {
use aura_core::{Scalar, ScalarKind, Timestamp};
use aura_engine::{ColumnarTrace, RunManifest};
use aura_registry::{derive_trace_name, CampaignRunRecord, Registry, TraceStore};
let cwd = temp_cwd("chart-by-campaign-name");
// The chosen family name (`--trace <NAME>`) — distinctive, not a trace handle
// (no such directory under runs/traces, so `name_kind` classifies NotFound).
const CAMPAIGN_NAME: &str = "mom-ger40-sept24";
// (1) Store a campaign document carrying that name (a valid CampaignDoc, so a
// resolver can parse it by any strategy — Value lookup or full deserialize).
// `put_campaign` self-keys by content, returning the id the campaign-run
// record must reference. The registry roots where `env.registry()` does:
// `<cwd>/runs/runs.jsonl` (campaign docs at `<cwd>/runs/campaigns/<id>.json`,
// campaign_runs at `<cwd>/runs/campaign_runs.jsonl`).
let reg = Registry::open(cwd.join("runs").join("runs.jsonl"));
let campaign_json = r#"{
"format_version": 1,
"kind": "campaign",
"name": "__NAME__",
"data": { "instruments": ["GER40"],
"windows": [ { "from_ms": 1, "to_ms": 2 } ] },
"strategies": [
{ "ref": { "content_id": "9f3a" },
"axes": { "fast": { "kind": "I64", "values": [8, 12] } } }
],
"process": { "ref": { "content_id": "4e2d" } },
"seed": 1,
"presentation": { "persist_taps": ["equity"], "emit": ["family_table"] }
}"#
.replace("__NAME__", CAMPAIGN_NAME);
let campaign_id = reg.put_campaign(&campaign_json).expect("store campaign document");
// (2) Append a campaign-run record whose trace_name is Some (the executor's
// claim sentinel; `append_campaign_run` assigns the per-campaign run counter
// and re-derives the stored trace_name to `{campaign8}-{run}`).
let record = CampaignRunRecord {
campaign: campaign_id.clone(),
process: "4e2d".to_string(),
run: 0, // ignored — append assigns the counter
seed: 1,
cells: vec![],
generalizations: vec![],
trace_name: Some("claim".to_string()),
};
let run = reg.append_campaign_run(&record).expect("append campaign run");
let handle = derive_trace_name(&campaign_id, run);
// (3) Write a family under that exact handle via TraceStore — the same
// fabrication the handle-charted sibling uses (#224 depth-2: two members under
// one cell). Charting `handle` directly is thus known to render a family chart.
let manifest = || RunManifest {
commit: "c0ffee".to_string(),
params: vec![("fast".to_string(), Scalar::f64(2.0))],
window: (Timestamp(1), Timestamp(2)),
seed: 0,
broker: "sim-optimal(pip_size=1)".to_string(),
selection: None,
instrument: Some("GER40".to_string()),
topology_hash: None,
project: None,
};
let taps = |a: f64, b: f64| {
vec![ColumnarTrace::from_rows(
"equity",
&[ScalarKind::F64],
&[(Timestamp(1), vec![Scalar::f64(a)]), (Timestamp(2), vec![Scalar::f64(b)])],
)]
};
let store = TraceStore::open(cwd.join("runs"));
store.write(&format!("{handle}/cell0/m0"), &manifest(), &taps(0.0, 0.4)).expect("write member m0");
store.write(&format!("{handle}/cell0/m1"), &manifest(), &taps(0.0, 0.7)).expect("write member m1");
// Chart by the chosen NAME (not the handle): resolution must go through the
// campaign-run query and land on `handle`, rendering the same family chart.
let out = Command::new(BIN).args(["chart", CAMPAIGN_NAME]).current_dir(&cwd).output().expect("spawn chart <name>");
assert_eq!(
out.status.code(),
Some(0),
"charting a --trace family by its chosen campaign name must exit 0; got {:?}, stderr: {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr),
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("window.AURA_TRACES") && stdout.contains("<title>aura chart"),
"the chosen name must render the same family chart as its handle \
(AURA_TRACES + chart title); stdout len {}",
stdout.len(),
);
let _ = std::fs::remove_dir_all(&cwd);
}
#[test]
fn no_args_prints_usage_and_exits_two() {
let out = Command::new(BIN).output().expect("spawn aura");