3e1e7e21da
Reconciles the sweep-axis namespace (closes #328): the raw form <node>.<param> (splice paths keep their interior path) is now the only user-facing axis name — printed by --list-axes, accepted by --axis on BOTH sweep routes, recorded in the synthetic run manifest, discovered by graph introspect --params (bound params included, default= lexicon), and validated by campaign documents. The wrapped <blueprint>.<...> form is retired with translation refusals on both seams. Mechanics: - classify_axis_intake (aura-runner axes.rs): explicit raw-first acceptance predicate shared by both --axis intake routes. Deliberately not built on raw_matches_wrapped, whose equality branch would have accepted the retired form silently (skeptic finding, minuted on #328). Wrapped hit -> translation refusal naming the raw candidate (shared prose builder wrapped_axis_refusal, one literal for both routes). - The synthetic route resolution moves to the raw frame the real route (bind_axes) already uses: override_paths/wrapped_bound_overrides_of match via raw_matches_wrapped (also removing a latent slicing panic), blueprint_sweep_family translates raw names onto wrapped SweepBinder slots, manifest.params records raw keys (name-only reshaping, zip is positional). The two sweep routes now share one convention. - reproduce translates recorded manifest names raw-or-wrapped onto the wrapped space: old registered artifacts stay replayable (C29), new raw manifests round-trip. - introspect --params gains the bound default= lines via the same render_value the --list-axes bound pass uses - the two discovery surfaces are byte-identical by construction. - AxisNotInParamSpace carries raw_candidate; ref_fault_prose renders the did-you-mean iff present. Sweep-terminal bind errors (MissingKnob/KindMismatch) render the raw knob name (render seam only; acceptance criterion: no user-facing surface prints a wrapped axis name). - Downstream consequence fixes: scaffold quickstart example spoke wrapped; ~90 wrapped test literals converted to raw across six test files (deliberately-bogus refusal names and the untouched internal walkforward wrapped route kept, with comments). Fork decisions and the skeptic correction are minuted on #328 (comments 2026-07-24/25). Grounding-check PASS first attempt (12 assumptions ratified). Review (opus): translation ambiguity cleared (uniform single prefix per blueprint), reproduce both-shapes traced; repair pass added the synthetic-route refusal e2e + the shared prose builder. Verified: cargo test --workspace green (99 targets, 0 failures), clippy -D warnings clean. refs #246, refs #319, refs #331
488 lines
19 KiB
Rust
488 lines
19 KiB
Rust
//! E2E: the project-as-crate load boundary (cycle 0102). Builds the fixture
|
|
//! project once (cargo, same toolchain, path-dep on this workspace's
|
|
//! aura-core), then drives the aura binary from inside the fixture dir.
|
|
//! `aura run` persists nothing without --trace, so the tracked fixture stays
|
|
//! clean (target/, Cargo.lock, runs/ are fixture-gitignored).
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Output};
|
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
|
|
mod common;
|
|
use common::built_project;
|
|
|
|
fn fixture_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/demo-project")
|
|
}
|
|
|
|
fn badcharter_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/badcharter-project")
|
|
}
|
|
|
|
fn undescribed_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/undescribed-project")
|
|
}
|
|
|
|
fn restated_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/restated-project")
|
|
}
|
|
|
|
fn aura(args: &[&str], cwd: &Path) -> Output {
|
|
Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(args)
|
|
.current_dir(cwd)
|
|
.output()
|
|
.expect("run aura")
|
|
}
|
|
|
|
#[test]
|
|
fn project_run_resolves_demo_node_and_is_bit_identical() {
|
|
let dir = built_project();
|
|
let a = aura(&["run", "demo_signal.json"], dir);
|
|
assert!(
|
|
a.status.success(),
|
|
"run failed: {}",
|
|
String::from_utf8_lossy(&a.stderr)
|
|
);
|
|
let b = aura(&["run", "demo_signal.json"], dir);
|
|
assert!(b.status.success());
|
|
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
|
|
}
|
|
|
|
#[test]
|
|
fn project_run_manifest_carries_provenance() {
|
|
let dir = built_project();
|
|
let out = aura(&["run", "demo_signal.json"], dir);
|
|
assert!(out.status.success());
|
|
let v: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("run report is JSON");
|
|
let p = &v["manifest"]["project"];
|
|
assert_eq!(p["namespace"], "demo");
|
|
let sha = p["dylib_sha256"].as_str().expect("hash present");
|
|
assert_eq!(sha.len(), 64, "sha256 hex");
|
|
}
|
|
|
|
#[test]
|
|
fn introspect_vocabulary_lists_project_types() {
|
|
let dir = built_project();
|
|
let out = aura(&["graph", "introspect", "--vocabulary"], dir);
|
|
assert!(out.status.success());
|
|
let text = String::from_utf8_lossy(&out.stdout);
|
|
assert!(text.contains("demo::Identity"), "project id listed");
|
|
assert!(text.contains("SMA"), "std ids still listed");
|
|
}
|
|
|
|
/// (#184/#185) Outside any project, `demo::Identity` resolves nowhere: the base
|
|
/// failure is house-style prose (never the raw `UnknownNodeType` Debug leak),
|
|
/// and — because the unresolved id LOOKS project-namespaced (`::`) while no
|
|
/// `Aura.toml` was found up from cwd — the message carries a hint pointing at
|
|
/// the missing project, distinguishing "no project" from "genuinely unknown
|
|
/// std id" (the bare-id case in `cli_run.rs` gets no such hint).
|
|
#[test]
|
|
fn outside_a_project_the_demo_blueprint_is_unknown() {
|
|
// cwd = the aura-cli crate dir: no Aura.toml anywhere up the tree.
|
|
let cwd = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
let bp = fixture_dir().join("demo_signal.json");
|
|
let out = aura(&["run", bp.to_str().unwrap()], cwd);
|
|
assert!(!out.status.success());
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
err.contains(r#"unknown node type "demo::Identity""#),
|
|
"house-style prose names the type: {err}"
|
|
);
|
|
assert!(!err.contains("UnknownNodeType"), "does not leak the Debug variant name: {err}");
|
|
assert!(
|
|
err.contains("Aura.toml"),
|
|
"hints that no project was found for the namespaced id: {err}"
|
|
);
|
|
}
|
|
|
|
/// `Env::runs_root()` resolves `Aura.toml`'s `[paths] runs = "runs"` against
|
|
/// the DISCOVERED project root (the `Aura.toml` directory found by walking up
|
|
/// from cwd), not against the invocation cwd itself: a family recorded while
|
|
/// invoking `aura` from a project subdirectory must still land under
|
|
/// `<project-root>/runs/`, and no stray `runs/` must appear under the
|
|
/// subdirectory. This is the property that makes `[paths]` project-relative
|
|
/// rather than shell-relative (C17).
|
|
#[test]
|
|
fn project_registry_anchors_at_discovered_root_not_invocation_cwd() {
|
|
let dir = built_project();
|
|
let sub = dir.join("src");
|
|
let runs_dir = dir.join("runs");
|
|
std::fs::remove_dir_all(&runs_dir).ok();
|
|
let closed_bp =
|
|
format!("{}/examples/r_sma.json", env!("CARGO_MANIFEST_DIR"));
|
|
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args([
|
|
"sweep",
|
|
&closed_bp,
|
|
"--axis",
|
|
"fast.length=2,4",
|
|
"--axis",
|
|
"slow.length=8,16",
|
|
"--name",
|
|
"proj-anchor",
|
|
])
|
|
.current_dir(&sub)
|
|
.output()
|
|
.expect("spawn aura sweep");
|
|
assert!(
|
|
out.status.success(),
|
|
"sweep failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
assert!(
|
|
runs_dir.join("families.jsonl").is_file(),
|
|
"family record must land at the project root's runs/, not the invocation cwd's"
|
|
);
|
|
assert!(
|
|
!sub.join("runs").exists(),
|
|
"no stray runs/ must be created under the invocation subdirectory"
|
|
);
|
|
std::fs::remove_dir_all(&runs_dir).ok();
|
|
}
|
|
|
|
/// A project whose listed type id is not `<namespace>::`-prefixed is refused
|
|
/// (exit 1, cause named) through the REAL load path — `libloading` +
|
|
/// descriptor read + `check_charter` wired together in `project::load` — not
|
|
/// merely the pure `check_charter` function (already unit-tested in
|
|
/// `aura-cli::project`). Pins the "refuse-don't-guess" vocabulary charter as
|
|
/// an end-to-end guarantee: a regression that skipped the charter call in the
|
|
/// `load()` glue would pass every existing unit test but still be caught here.
|
|
#[test]
|
|
fn vocabulary_charter_violation_refuses_end_to_end() {
|
|
let dir = badcharter_dir();
|
|
let build = Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn cargo build for the badcharter fixture");
|
|
assert!(
|
|
build.status.success(),
|
|
"badcharter fixture build failed:\n{}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
let run = aura(&["run", "x.json"], &dir);
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(1),
|
|
"a charter violation is a runtime refusal (exit 1), not a usage error"
|
|
);
|
|
let err = String::from_utf8_lossy(&run.stderr);
|
|
assert!(err.contains("lacks the project prefix"), "stderr must name the charter cause: {err}");
|
|
}
|
|
|
|
/// C29 load seam end-to-end (#316): an extension node whose doc fails the
|
|
/// shape gate (the empty-string alibi the compiler cannot catch) refuses at
|
|
/// load with the entry and the rule named — through the real
|
|
/// `aura-cli::project::load` path (libloading + descriptor read), not merely
|
|
/// the pure `doc_gate` unit function. The described twin's load is pinned by
|
|
/// `project_run_resolves_demo_node_and_is_bit_identical`.
|
|
#[test]
|
|
fn undescribed_vocabulary_entry_refuses_end_to_end() {
|
|
let dir = undescribed_dir();
|
|
let build = Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn cargo build for the undescribed fixture");
|
|
assert!(
|
|
build.status.success(),
|
|
"undescribed fixture build failed:\n{}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
let run = aura(&["run", "x.json"], &dir);
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(1),
|
|
"an undescribed vocabulary entry is a runtime refusal (exit 1), \
|
|
not a usage error"
|
|
);
|
|
let err = String::from_utf8_lossy(&run.stderr);
|
|
assert!(
|
|
err.contains("vocabulary entry `und::Opaque` has an empty doc"),
|
|
"stderr must name the entry and the rule: {err}"
|
|
);
|
|
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
|
|
}
|
|
|
|
/// C29 load seam end-to-end, the sibling fault arm (#316): a doc that merely
|
|
/// restates its own type id (`DocGateFault::RestatesName`) refuses at load
|
|
/// exactly like the empty-doc case above, through the same real
|
|
/// `aura-cli::project::load` path. `RestatesName`'s rendered prose was
|
|
/// previously pinned only against the pure `Display` impl in
|
|
/// `aura-runner::project`'s unit tests (never reachable at all through the
|
|
/// CLI) — a regression that reordered `doc_gate`'s two fault arms, or that
|
|
/// skipped the gate call for this arm specifically, would pass every
|
|
/// existing test and still ship a self-description hole for exactly this
|
|
/// case: an alibi doc that types-checks but says nothing.
|
|
#[test]
|
|
fn restated_name_vocabulary_entry_refuses_end_to_end() {
|
|
let dir = restated_dir();
|
|
let build = Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn cargo build for the restated fixture");
|
|
assert!(
|
|
build.status.success(),
|
|
"restated fixture build failed:\n{}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
let run = aura(&["run", "x.json"], &dir);
|
|
assert_eq!(
|
|
run.status.code(),
|
|
Some(1),
|
|
"a name-restating vocabulary entry is a runtime refusal (exit 1), \
|
|
not a usage error"
|
|
);
|
|
let err = String::from_utf8_lossy(&run.stderr);
|
|
assert!(
|
|
err.contains("vocabulary entry `restated::Echo` has a doc that merely restates its name"),
|
|
"stderr must name the entry and the rule: {err}"
|
|
);
|
|
assert!(err.contains("C29"), "stderr must cite the contract: {err}");
|
|
}
|
|
|
|
/// Force a file's modification time to a fixed instant (explicit mtime, no
|
|
/// sleeping — mtime granularity would make a "touch then compare" race).
|
|
fn set_mtime(path: &Path, secs_since_epoch: u64) {
|
|
let t = UNIX_EPOCH + Duration::from_secs(secs_since_epoch);
|
|
let f = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.open(path)
|
|
.unwrap_or_else(|e| panic!("open {} to set mtime: {e}", path.display()));
|
|
f.set_modified(t)
|
|
.unwrap_or_else(|e| panic!("set mtime on {}: {e}", path.display()));
|
|
}
|
|
|
|
/// The role-2 authoring loop is edit -> `cargo build` -> run (C25). A forgotten
|
|
/// build is a silent-wrong-results footgun: the loader load-and-holds the
|
|
/// *previous* dylib and reports plausible, stale numbers. The property: when a
|
|
/// source file under the project is newer (mtime) than the built dylib, the run
|
|
/// still proceeds — a stale run is deterministic and manifest-stamped, so a
|
|
/// refusal is not warranted — but it surfaces the staleness on stderr, naming
|
|
/// both the dylib's and the source's modification times so the omission is
|
|
/// visible instead of silent.
|
|
#[test]
|
|
fn stale_dylib_warns_naming_both_timestamps_but_still_runs() {
|
|
let dir = built_project();
|
|
let src = dir.join("src/lib.rs");
|
|
let dylib = dir.join("target/debug").join(format!(
|
|
"{}demo_project{}",
|
|
std::env::consts::DLL_PREFIX,
|
|
std::env::consts::DLL_SUFFIX,
|
|
));
|
|
assert!(
|
|
dylib.is_file(),
|
|
"built fixture must have produced a dylib at {}",
|
|
dylib.display()
|
|
);
|
|
|
|
// Pin two far-apart, distinctive mtimes so any human-readable rendering of
|
|
// either timestamp carries a stable, non-colliding year token. Source
|
|
// (2033) is newer than the dylib (2001): stale.
|
|
const DYLIB_MTIME: u64 = 993_859_200; // 2001-06-30 UTC
|
|
const SOURCE_MTIME: u64 = 2_003_702_400; // 2033-06-30 UTC
|
|
set_mtime(&dylib, DYLIB_MTIME);
|
|
set_mtime(&src, SOURCE_MTIME);
|
|
|
|
let out = aura(&["run", "demo_signal.json"], dir);
|
|
|
|
// Restore sane mtimes before asserting, so a failed assertion does not
|
|
// leave the shared fixture wedged "stale" (or with a future mtime that
|
|
// makes every later `cargo build` re-fingerprint it).
|
|
let now = SystemTime::now();
|
|
let _ = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.open(&dylib)
|
|
.and_then(|f| f.set_modified(now));
|
|
let _ = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.open(&src)
|
|
.and_then(|f| f.set_modified(now));
|
|
|
|
// Warn, not refuse: the run still succeeds and still emits its JSON report.
|
|
assert!(
|
|
out.status.success(),
|
|
"a stale dylib warns but the run proceeds (exit 0): {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let report: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("stale run still emits its JSON report");
|
|
assert_eq!(
|
|
report["manifest"]["project"]["namespace"], "demo",
|
|
"the stale run behaves exactly like a fresh one"
|
|
);
|
|
|
|
// Names both timestamps: the dylib's (2001) and the newer source's (2033).
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
err.contains("2001"),
|
|
"stderr warning must name the dylib's modification time (2001): {err}"
|
|
);
|
|
assert!(
|
|
err.contains("2033"),
|
|
"stderr warning must name the newer source's modification time (2033): {err}"
|
|
);
|
|
}
|
|
|
|
fn dataonly_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/dataonly-project")
|
|
}
|
|
|
|
fn multicrate_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/multicrate-project")
|
|
}
|
|
|
|
fn nested_nodes_dir() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/nested-nodes-project")
|
|
}
|
|
|
|
/// Property (#241, tier selection at the load boundary): a project whose
|
|
/// `Aura.toml` has neither a `[nodes]` pointer nor a root `Cargo.toml` is the
|
|
/// data-only tier — `aura run` resolves entirely against the std vocabulary
|
|
/// (no crate to load, no dylib to build) and still succeeds. The widened
|
|
/// `ProjectProvenance` (Tier-1 optional fields) must reflect that: the
|
|
/// manifest's `project` object carries no `namespace`/`dylib_sha256` (there is
|
|
/// no loaded crate to stamp) while still being present as an object (a project
|
|
/// WAS discovered — `Aura.toml` exists).
|
|
#[test]
|
|
fn data_only_project_runs_and_stamps_commit_only_provenance() {
|
|
let dir = dataonly_dir();
|
|
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(["run", "signal.json"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura run");
|
|
assert!(
|
|
out.status.success(),
|
|
"data-only run failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let v: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("run report is JSON");
|
|
let p = &v["manifest"]["project"];
|
|
assert!(p.is_object(), "a project WAS discovered (Aura.toml exists): {p}");
|
|
assert!(
|
|
p.get("namespace").is_none(),
|
|
"no crate was loaded, so namespace must be absent, not null: {p}"
|
|
);
|
|
assert!(
|
|
p.get("dylib_sha256").is_none(),
|
|
"no crate was loaded, so dylib_sha256 must be absent, not null: {p}"
|
|
);
|
|
assert!(
|
|
p["commit"].as_str().is_some_and(|s| !s.is_empty()),
|
|
"a data-only project still stamps its own repo commit: {p}"
|
|
);
|
|
}
|
|
|
|
/// Property (#241, tier selection refuses ambiguity): more than one entry
|
|
/// under `[nodes].crates` refuses at the real CLI entrypoint — before any
|
|
/// subcommand-specific file I/O runs (main.rs resolves the project `Env`
|
|
/// ahead of dispatch) — naming both the count and the `[nodes]` source, not a
|
|
/// raw parse/index panic.
|
|
#[test]
|
|
fn multi_crate_pointer_refuses_end_to_end() {
|
|
let dir = multicrate_dir();
|
|
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(["run", "x.json"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura run");
|
|
assert_eq!(
|
|
out.status.code(),
|
|
Some(1),
|
|
"a multi-crate [nodes] config is a runtime refusal (exit 1): {:?}",
|
|
out.status
|
|
);
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(
|
|
err.contains("multi-crate loading is not yet supported"),
|
|
"stderr must name the cause: {err}"
|
|
);
|
|
}
|
|
|
|
/// Property (#241, [nodes] pointer indirection): the node crate need not be
|
|
/// collocated with `Aura.toml` at the project root — a single `[nodes].crates`
|
|
/// entry names a relative path to load instead. Driven through the REAL load
|
|
/// path (cargo metadata + libloading + charter, exactly like the root-crate
|
|
/// case in `project_run_manifest_carries_provenance`), not a pure unit call:
|
|
/// `aura run` from the project root resolves a node type that only the
|
|
/// pointed-to crate exports, and the manifest's provenance stamps that
|
|
/// crate's namespace.
|
|
#[test]
|
|
fn nodes_pointer_crate_resolves_and_stamps_its_namespace() {
|
|
let dir = nested_nodes_dir();
|
|
let crate_dir = dir.join("nodecrate");
|
|
let build = Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&crate_dir)
|
|
.output()
|
|
.expect("spawn cargo build for the nodecrate fixture");
|
|
assert!(
|
|
build.status.success(),
|
|
"nodecrate fixture build failed:\n{}",
|
|
String::from_utf8_lossy(&build.stderr)
|
|
);
|
|
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(["run", "pointer_signal.json"])
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn aura run");
|
|
assert!(
|
|
out.status.success(),
|
|
"pointer-crate run failed: {}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
let v: serde_json::Value =
|
|
serde_json::from_slice(&out.stdout).expect("run report is JSON");
|
|
assert_eq!(
|
|
v["manifest"]["project"]["namespace"], "ptr",
|
|
"provenance names the POINTED-TO crate's namespace, not the project root: {v}"
|
|
);
|
|
}
|
|
|
|
/// #241: inside a data-only project, a project-namespaced type id gets the
|
|
/// "binds no node crate" hint (not the outside-project "no Aura.toml" text).
|
|
#[test]
|
|
fn data_only_project_hints_attach_for_namespaced_ids() {
|
|
let tmp = Path::new(env!("CARGO_TARGET_TMPDIR")).join("aura-dataonly-hint");
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(&tmp).unwrap();
|
|
std::fs::write(tmp.join("Aura.toml"), "").unwrap();
|
|
let bp = tmp.join("bp.json");
|
|
std::fs::write(&bp, r#"{"format_version":1,"blueprint":{"name":"x","nodes":[{"primitive":{"type":"nosuch::Node"}}],"edges":[],"input_roles":[{"name":"price","targets":[{"node":0,"slot":0}],"source":"F64"}],"output":[{"node":0,"field":0,"name":"bias"}]}}"#).unwrap();
|
|
let out = std::process::Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args(["run", bp.to_str().unwrap()])
|
|
.current_dir(&tmp)
|
|
.output()
|
|
.expect("spawn aura");
|
|
assert_eq!(out.status.code(), Some(2), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(err.contains("binds no node crate"), "{err}");
|
|
assert!(err.contains("aura nodes new"), "{err}");
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
}
|
|
|
|
#[test]
|
|
fn missing_artifact_refuses_with_build_hint() {
|
|
// A minimal never-built project: valid cargo metadata, no dylib on disk.
|
|
let tmp = Path::new(env!("CARGO_TARGET_TMPDIR")).join("aura-nobuild");
|
|
let _ = std::fs::remove_dir_all(&tmp);
|
|
std::fs::create_dir_all(tmp.join("src")).unwrap();
|
|
std::fs::write(tmp.join("Aura.toml"), "").unwrap();
|
|
std::fs::write(tmp.join("src/lib.rs"), "").unwrap();
|
|
std::fs::write(
|
|
tmp.join("Cargo.toml"),
|
|
"[package]\nname = \"nobuild\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[workspace]\n",
|
|
)
|
|
.unwrap();
|
|
let out = aura(&["run", "x.json"], &tmp);
|
|
assert_eq!(out.status.code(), Some(1), "runtime refusal");
|
|
let err = String::from_utf8_lossy(&out.stderr);
|
|
assert!(err.contains("cargo build"), "build hint present: {err}");
|
|
std::fs::remove_dir_all(&tmp).ok();
|
|
}
|