diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 690a321..b30cd22 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -4945,6 +4945,37 @@ mod tests { assert!(parse_blueprint_run_args(&["--params", "[{\"I64\":1}]", "--params", "[]"]).is_err()); // repeated } + /// Regenerates the committed demo signal blueprint the `aura run ` + /// E2E loads. Ignored by default; run with `--ignored` after a signal change. + #[test] + #[ignore = "regenerates the committed demo signal blueprint fixture"] + fn emit_demo_signal_fixture() { + let json = blueprint_to_json(&stage1_signal(Some(2), Some(4))).expect("serializes"); + std::fs::write("tests/fixtures/stage1_signal.json", json).expect("write fixture"); + } + + /// The cycle-1 keystone (C1): a signal serialized → loaded back through the + /// public `blueprint_from_json` path runs bit-identically to its Rust-built + /// twin — same metrics, same traces, same topology_hash. + #[test] + fn loaded_signal_runs_bit_identical_to_rust_built() { + let json = blueprint_to_json(&stage1_signal(Some(2), Some(4))).expect("serializes"); + let loaded = blueprint_from_json(&json, &|t| std_vocabulary(t)).expect("loads"); + let a = run_signal_stage1r(stage1_signal(Some(2), Some(4)), &[], RunData::Synthetic, 0); + let b = run_signal_stage1r(loaded, &[], RunData::Synthetic, 0); + assert_eq!(a.to_json(), b.to_json(), "loaded run is bit-identical incl. topology_hash"); + assert_eq!(a.manifest.topology_hash.as_deref().map(str::len), Some(64), "64-hex sha256 present"); + } + + /// `topology_hash` is deterministic per signal and distinguishes topologies — + /// the #158 reproducibility-anchor property. + #[test] + fn topology_hash_is_stable_and_distinguishes() { + let h = topology_hash(&stage1_signal(Some(2), Some(4))); + assert_eq!(h, topology_hash(&stage1_signal(Some(2), Some(4))), "same signal -> same hash"); + assert_ne!(h, topology_hash(&stage1_signal(Some(3), Some(4))), "different topology -> different hash"); + } + #[test] fn parse_mc_args_bare_and_name_route_to_synthetic() { assert_eq!(parse_mc_args(&[]), Ok(McArgs::Synthetic { name: "mc".to_string(), persist: false })); diff --git a/crates/aura-cli/tests/cli_run.rs b/crates/aura-cli/tests/cli_run.rs index 2dc5ff6..bb24114 100644 --- a/crates/aura-cli/tests/cli_run.rs +++ b/crates/aura-cli/tests/cli_run.rs @@ -317,6 +317,38 @@ fn run_real_nonvetted_symbol_resolves_pip_from_sidecar() { ); } +/// `aura run ` loads a serialized signal blueprint and runs it +/// end-to-end (#165): exit 0, a RunReport whose manifest carries the topology_hash. +#[test] +fn aura_run_loads_and_runs_a_blueprint_file() { + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["run", "tests/fixtures/stage1_signal.json"]) + .output() + .expect("spawn aura run blueprint"); + 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"); + assert!(stdout.contains("\"topology_hash\":\""), "manifest carries the topology hash: {stdout}"); +} + +/// A blueprint referencing a node type outside the closed vocabulary fails clean +/// at load (the #160 closed-set guard at the data-plane face; invariant 9). +#[test] +fn aura_run_rejects_an_unknown_node_blueprint() { + let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura")) + .args(["run", "tests/fixtures/unknown_node.json"]) + .output() + .expect("spawn aura run unknown"); + assert_ne!(out.status.code(), Some(0), "an unknown node type must fail the run"); + let stderr = String::from_utf8(out.stderr).expect("utf-8"); + assert!(stderr.contains("UnknownNodeType"), "names the cause: {stderr}"); +} + #[test] fn run_trace_persists_taps_and_plain_run_writes_no_traces() { let cwd = temp_cwd("run-trace"); diff --git a/crates/aura-cli/tests/fixtures/stage1_signal.json b/crates/aura-cli/tests/fixtures/stage1_signal.json new file mode 100644 index 0000000..b5bb17c --- /dev/null +++ b/crates/aura-cli/tests/fixtures/stage1_signal.json @@ -0,0 +1 @@ +{"format_version":1,"blueprint":{"name":"stage1_signal","nodes":[{"primitive":{"type":"SMA","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} \ No newline at end of file diff --git a/crates/aura-cli/tests/fixtures/unknown_node.json b/crates/aura-cli/tests/fixtures/unknown_node.json new file mode 100644 index 0000000..7516354 --- /dev/null +++ b/crates/aura-cli/tests/fixtures/unknown_node.json @@ -0,0 +1 @@ +{"format_version":1,"blueprint":{"name":"stage1_signal","nodes":[{"primitive":{"type":"Nope","name":"fast","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":2}}]}},{"primitive":{"type":"SMA","name":"slow","bound":[{"pos":0,"name":"length","kind":"I64","value":{"I64":4}}]}},{"primitive":{"type":"Sub"}},{"primitive":{"type":"Bias","name":"bias","bound":[{"pos":0,"name":"scale","kind":"F64","value":{"F64":0.5}}]}}],"edges":[{"from":0,"to":2,"slot":0,"from_field":0},{"from":1,"to":2,"slot":1,"from_field":0},{"from":2,"to":3,"slot":0,"from_field":0}],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0},{"node":1,"slot":0}],"source":"F64"}],"output":[{"node":3,"field":0,"name":"bias"}]}} \ No newline at end of file