Files
Aura/crates/aura-cli/tests/cli_run.rs
T
Brummel 3f4d756ed2 feat(aura): fan-in input distinguishability — construction constraint + source-derived render
A fan-in node (>1 input) whose colliding sources hide an unnamed configuration
axis is now illegal at construction, and the definition view renders each fan-in
input as a source-derived recursive-signature identifier instead of the
positional, meaningless #A/#B.

The root defect was sma_cross: two Sma into a Sub with unaliased length params,
rendering sma_cross() -> (cross) with two indistinguishable [SMA] and
[Sub(#A,#B)] — the author meant fast vs slow but the identity hid it. Now it
renders sma_cross(fast:i64, slow:i64) with [SMA(fast)]/[SMA(slow)] and
[Sub(#Sf,#Ss)].

Shape (single source of truth for "signature" shared by engine + CLI):
- aura-engine signature_of(): a node's recursive authoring identity — type
  initial + alias initials + each wired input's signature (recursing into
  interior leaves, stopping at named roles/composites at their initial).
- aura-cli leaf_label/fan_in_identifiers: shortest sibling-unique prefix of a
  source's signature, never below its base (type + alias initials); a role-fed
  slot renders its name verbatim (#price); equal-signature interchangeable
  inputs keep the positional letter.
- aura-engine IndistinguishableFanIn: a signature collision is a fault only when
  a colliding source carries an unaliased param (the unnamed axis); param-less
  interchangeable sources (fan_composite's Pass/Pass) stay legal.

Param-aware criterion (refined during design): keeps the blast radius to the
param-bearing alias-less fixtures (sma_cross CLI + engine, the nested fast_slow);
fan_composite and the hand-wired flat fixtures stay green.

Placement decision (deviates from the plan): the constraint runs as a structural
pre-pass (check_fan_in_distinguishability) at the head of compile_with_params,
BEFORE the param-arity gate — not inside inline_composite as the plan drafted.
Reason: the no-param compile() of a param-bearing composite would hit ParamArity
first and preempt the fault; the spec mandates a structural check needing no
param values, so the pre-pass is the spec-aligned home. Alias-validity ordering
(BadInteriorIndex before the fan-in fault) is preserved at the pre-pass head.

C23 untouched: the check is construction-phase; the compilat stays name-free
(compiled_view_golden byte-stable). Ledger C9 carries the refinement.

Known debt (non-gating): the alias-index validity check now exists in both
inline_composite and the pre-pass (the pre-pass reaches every composite first,
making inline_composite's copy effectively dead). Left for a separate tidy — a
5-line, correctness-neutral removal with its own test surface.

closes #44
2026-06-08 15:50:19 +02:00

186 lines
8.4 KiB
Rust

//! Integration test: drive the built `aura` binary as a downstream user would,
//! asserting the `run` subcommand's stdout/exit contract and the bad-args path.
use std::process::Command;
/// Path to the freshly-built `aura` binary (Cargo sets this env var for the test
/// crate; the binary is named `aura` in `Cargo.toml`).
const BIN: &str = env!("CARGO_BIN_EXE_aura");
#[test]
fn run_prints_json_and_exits_zero() {
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
// exactly one line (the JSON object + a trailing newline from println!).
assert_eq!(stdout.lines().count(), 1, "stdout was: {stdout:?}");
let line = stdout.trim_end();
// canonical cycle-0009 JSON shape: nested manifest + metrics, stable keys.
// The manifest leads with a `commit` field; its value is the build's git
// identity (asserted by `run_manifest_commit_carries_real_git_head`), so
// this shape check pins only the surrounding structure, not the value.
assert!(line.starts_with("{\"manifest\":{\"commit\":\""), "got: {line}");
assert!(line.contains("\"broker\":\"sim-optimal(pip_size=0.0001)\""), "got: {line}");
assert!(line.contains("\"window\":[1,7]"), "got: {line}");
assert!(line.contains("\"metrics\":{\"total_pips\":"), "got: {line}");
// the integer sign-flip count is stable across float renderings.
assert!(line.ends_with("\"exposure_sign_flips\":1}}"), "got: {line}");
}
/// Property: the RunManifest is self-identifying — its `commit` carries the
/// real code identity that produced the run, not a placeholder. C8/C18 audit
/// trail (this run = this commit): once runs are archived, `manifest.commit`
/// must point back at the source. The build captures the current git HEAD at
/// compile time; the `"unknown"` literal survives only when there is no git.
///
/// Structural assertion (not exact-equality) on purpose: the working tree may
/// be dirty when this runs (e.g. this very test file uncommitted), so a build
/// that appends a `-dirty` suffix would not equal `rev-parse HEAD` verbatim.
/// The honest contract is: the field CONTAINS the current HEAD sha AND is not
/// the bare `"unknown"` placeholder — true regardless of dirtiness/suffix.
#[test]
fn run_manifest_commit_carries_real_git_head() {
// The current HEAD sha, obtained the same way the build is expected to.
let head = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("spawn git rev-parse HEAD");
assert!(head.status.success(), "git rev-parse HEAD failed");
let head_sha = String::from_utf8(head.stdout).expect("utf-8 sha");
let head_sha = head_sha.trim();
assert!(!head_sha.is_empty(), "empty HEAD sha");
// Drive the binary and pull `manifest.commit` out of the single JSON line.
let out = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert!(out.status.success(), "exit status: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
let line = stdout.trim_end();
// Locate the `"commit":"<value>"` value without a JSON dependency (the
// sibling tests parse this output by substring too).
let key = "\"commit\":\"";
let start = line.find(key).expect("manifest has a commit field") + key.len();
let rest = &line[start..];
let end = rest.find('"').expect("commit field is a closed string");
let commit = &rest[..end];
assert_ne!(
commit, "unknown",
"manifest.commit is still the placeholder: {line}"
);
assert!(
commit.contains(head_sha),
"manifest.commit {commit:?} should contain the current HEAD sha {head_sha:?}; line: {line}"
);
}
/// Property: `aura run` is strict about its argument vector — a trailing
/// unexpected token (e.g. a typo'd flag like `aura run --sweep`) takes the
/// error path (usage on stderr, exit 2), never a silent full run. This is the
/// strict reading ratified in #16: keying only on the first token being `run`
/// and ignoring the rest would let a typo masquerade as a successful run.
/// The same test pins positive-preservation — bare `aura run` still emits the
/// JSON report on stdout and exits 0 — so the strict guard cannot regress the
/// happy path.
#[test]
fn run_with_trailing_token_is_strict_and_exits_two() {
// Negative: an unexpected trailing token is rejected like a bad-args call.
let out = Command::new(BIN)
.args(["run", "extra-garbage"])
.output()
.expect("spawn aura run extra-garbage");
assert_eq!(
out.status.code(),
Some(2),
"`aura run extra-garbage` must reject the trailing token: {:?}",
out.status
);
assert!(
out.stdout.is_empty(),
"stdout should be empty on the usage path, got: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(stderr.contains("usage"), "stderr was: {stderr:?}");
// Positive-preservation: bare `aura run` still runs and prints the report.
let ok = Command::new(BIN).arg("run").output().expect("spawn aura run");
assert_eq!(
ok.status.code(),
Some(0),
"bare `aura run` must still succeed: {:?}",
ok.status
);
let ok_stdout = String::from_utf8(ok.stdout).expect("utf-8 stdout");
assert!(
ok_stdout.trim_start().starts_with("{\"manifest\":"),
"bare `aura run` should print the JSON report, got: {ok_stdout:?}"
);
}
#[test]
fn no_args_prints_usage_and_exits_two() {
let out = Command::new(BIN).output().expect("spawn aura");
assert_eq!(out.status.code(), Some(2), "exit status: {:?}", out.status);
assert!(out.stdout.is_empty(), "stdout should be empty on the usage path");
let stderr = String::from_utf8(out.stderr).expect("utf-8 stderr");
assert!(stderr.contains("usage"), "stderr was: {stderr:?}");
}
/// Property: `--help`/`-h` is the success-path help affordance, not the error
/// path. A newcomer's reflex first command (C22 first-contact ergonomics, #20)
/// prints the usage to stdout and exits 0; the conventional `-h` alias behaves
/// identically. An *unknown* subcommand keeps the error path (exit 2) so the
/// help fix does not regress bad-args handling.
#[test]
fn help_flag_prints_usage_to_stdout_and_exits_zero() {
for flag in ["--help", "-h"] {
let out = Command::new(BIN).arg(flag).output().expect("spawn aura --help");
assert_eq!(out.status.code(), Some(0), "`aura {flag}` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
!stdout.trim().is_empty(),
"`aura {flag}` should print help to stdout, got: {stdout:?}"
);
// The usage names the only subcommand a newcomer can run.
assert!(
stdout.contains("run"),
"`aura {flag}` help should mention the `run` subcommand, got: {stdout:?}"
);
}
// Negative-preservation: an unknown subcommand is still the error path.
let out = Command::new(BIN).arg("frobnicate").output().expect("spawn aura frobnicate");
assert_eq!(
out.status.code(),
Some(2),
"unknown subcommand must stay exit 2: {:?}",
out.status
);
}
/// Property: the `aura graph` blueprint view renders a fan-in node's inputs as
/// source-derived signature identifiers, not positional `#A`/`#B`. The sample
/// `sma_cross` Sub combines a fast and a slow SMA; the two inputs must therefore
/// render as the distinct prefixes `#Sf` / `#Ss` (SMA + alias initial), so a
/// non-commutative Sub's argument order is legible at the boundary. Driven
/// through the built binary (output piped → Plain, byte-clean) so it pins the
/// observable CLI contract, not just the in-process renderer.
#[test]
fn graph_renders_source_derived_fan_in_identifiers() {
let out = Command::new(BIN).arg("graph").output().expect("spawn aura graph");
assert_eq!(out.status.code(), Some(0), "`aura graph` exit: {:?}", out.status);
let stdout = String::from_utf8(out.stdout).expect("utf-8 stdout");
assert!(
stdout.contains("[Sub(#Sf,#Ss)]"),
"fan-in inputs must be source-derived (#Sf/#Ss), not positional: {stdout}"
);
// the retired positional form must not reappear.
assert!(
!stdout.contains("[Sub(#A,#B)]"),
"positional #A/#B fan-in stubs must be gone: {stdout}"
);
}