61c8828fad
aura run leaked the raw LoadError Debug form (UnknownNodeType("..."))
while graph build already phrased the same fault house-style (#162
convention: engine error types are Display-free, the CLI presents them).
Route the run path through the shared blueprint_load_prose presenter (now
pub(crate), env-agnostic) so both surfaces phrase a LoadError identically
and neither leaks the Debug token (#184). Add unresolved_namespace_hint:
when the unresolved id looks project-namespaced (::) and no project was
loaded (env.provenance() is None), append a hint pointing at the missing
Aura.toml — the newcomer running the host from the wrong directory now
gets an orienting diagnosis instead of a bare type error (#185). The hint
is applied on both the run and graph-build paths. RED-first: the two
existing run-path pins were flipped to assert the prose + absence of the
Debug token before implementing; suite green (1043/0).
closes #184
closes #185
200 lines
7.5 KiB
Rust
200 lines
7.5 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::sync::OnceLock;
|
|
|
|
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 built_fixture() -> &'static PathBuf {
|
|
static BUILT: OnceLock<PathBuf> = OnceLock::new();
|
|
BUILT.get_or_init(|| {
|
|
let dir = fixture_dir();
|
|
let out = Command::new("cargo")
|
|
.arg("build")
|
|
.current_dir(&dir)
|
|
.output()
|
|
.expect("spawn cargo build for the fixture project");
|
|
assert!(
|
|
out.status.success(),
|
|
"fixture build failed:\n{}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
dir
|
|
})
|
|
}
|
|
|
|
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_fixture();
|
|
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_fixture();
|
|
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_fixture();
|
|
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_fixture();
|
|
let sub = dir.join("src");
|
|
let runs_dir = dir.join("runs");
|
|
std::fs::remove_dir_all(&runs_dir).ok();
|
|
let open_bp =
|
|
format!("{}/tests/fixtures/sma_signal_open.json", env!("CARGO_MANIFEST_DIR"));
|
|
let out = Command::new(env!("CARGO_BIN_EXE_aura"))
|
|
.args([
|
|
"sweep",
|
|
&open_bp,
|
|
"--axis",
|
|
"sma_signal.fast.length=2,4",
|
|
"--axis",
|
|
"sma_signal.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}");
|
|
}
|
|
|
|
#[test]
|
|
fn missing_artifact_refuses_with_build_hint() {
|
|
// A minimal never-built project: valid cargo metadata, no dylib on disk.
|
|
let tmp = std::env::temp_dir().join(format!("aura-nobuild-{}", std::process::id()));
|
|
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();
|
|
}
|