feat(cli): aura new scaffolds the data-only tier (#241 T4)
- NewCmd drops --engine-path/--namespace (they belong to the node-crate scaffold); dispatch_new emits Aura.toml + .gitignore + two std-only starter blueprints + CLAUDE.md, git init, no crate, no build step. - Two starter blueprints because run requires a closed blueprint while sweep requires open knobs: signal.json (closed, run target) and signal_open.json (fast.length free) — the scaffolded CLAUDE.md quickstart lines are both verified live. - The crate templates survive untouched for the attach verb; old scaffold()/scaffold_spec() carry a dead-code bridge until it lands. - project_new.rs realigned: data-only file set, no-build run headline (stdout report, C1 byte-identity, commit-only provenance, negative runs/ assert), advertised-sweep headline (#218 gate passes, store lands project-local). refs #241
This commit is contained in:
@@ -2475,15 +2475,8 @@ struct ChartCmd {
|
||||
|
||||
#[derive(Args)]
|
||||
struct NewCmd {
|
||||
/// Directory / crate name to create.
|
||||
/// Directory name to create.
|
||||
name: String,
|
||||
/// Engine checkout the project depends on (path deps). Default: the
|
||||
/// engine root this binary was built from.
|
||||
#[arg(long)]
|
||||
engine_path: Option<std::path::PathBuf>,
|
||||
/// Vocabulary namespace (default: the name with dashes as underscores).
|
||||
#[arg(long)]
|
||||
namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
@@ -3325,23 +3318,17 @@ fn dispatch_new(a: NewCmd, _env: &project::Env) {
|
||||
eprintln!("aura: {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
let spec = scaffold::scaffold_spec(
|
||||
&a.name,
|
||||
a.engine_path.as_deref(),
|
||||
a.namespace.as_deref(),
|
||||
&cwd,
|
||||
)
|
||||
.unwrap_or_else(|m| {
|
||||
let spec = scaffold::project_scaffold_spec(&a.name, &cwd).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(2);
|
||||
});
|
||||
scaffold::scaffold(&spec).unwrap_or_else(|m| {
|
||||
scaffold::scaffold_project(&spec).unwrap_or_else(|m| {
|
||||
eprintln!("aura: {m}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
println!(
|
||||
"created project `{}` (namespace `{}`)",
|
||||
spec.name, spec.namespace
|
||||
"created project \"{}\" (data-only; attach native nodes later with `aura nodes new`)",
|
||||
spec.name
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
//! `aura new` — scaffold a research project crate (cycle 0103, C16/C17).
|
||||
//! `aura new` — scaffold a data-only research project (#241).
|
||||
//!
|
||||
//! Emits the cycle-0102 fixture shape parameterized by name/namespace: a
|
||||
//! cdylib crate with path-deps on the engine checkout (the baked default or
|
||||
//! `--engine-path`), a paths-only `Aura.toml`, a sample node + runnable
|
||||
//! blueprint, and a project `CLAUDE.md`. The scaffolder never builds and
|
||||
//! never loads (the author builds; `aura new` bypasses the project-load
|
||||
//! phase in `main()`).
|
||||
//! `aura new` emits the data-only tier: a paths-only `Aura.toml`, a runnable
|
||||
//! starter blueprint over the std vocabulary, `.gitignore`, and `CLAUDE.md` —
|
||||
//! no crate, no build step. The scaffolder never builds and never loads (the
|
||||
//! project-load phase in `main()` sees a plain data-only project just like
|
||||
//! any other).
|
||||
//!
|
||||
//! The crate-scaffold templates (`ScaffoldSpec`/`scaffold`/`scaffold_spec`,
|
||||
//! cycle 0103, C16/C17) survive for the native-node tier: `aura nodes new`
|
||||
//! (a follow-up task) attaches a node crate beside a project.
|
||||
//!
|
||||
//! Templates are raw strings with `__NS__` / `__NAME__` / `__NAME_SNAKE__` /
|
||||
//! `__ENGINE__` tokens substituted by plain `.replace()` — no format-string
|
||||
@@ -50,8 +53,27 @@ pub fn default_engine_root() -> PathBuf {
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
/// The data-only project scaffold (#241): no crate, no build step.
|
||||
#[derive(Debug)]
|
||||
pub struct ProjectScaffoldSpec {
|
||||
pub name: String,
|
||||
pub target_dir: PathBuf,
|
||||
}
|
||||
|
||||
/// Validate the argv-level inputs for the data-only project scaffold. `Err`
|
||||
/// is a usage fault — the caller exits 2 (C14 partition).
|
||||
pub fn project_scaffold_spec(name: &str, cwd: &Path) -> Result<ProjectScaffoldSpec, String> {
|
||||
if !valid_name(name) {
|
||||
return Err(format!(
|
||||
"invalid project name `{name}` (allowed: letters, digits, `_`, `-`; no leading digit)"
|
||||
));
|
||||
}
|
||||
Ok(ProjectScaffoldSpec { name: name.to_string(), target_dir: cwd.join(name) })
|
||||
}
|
||||
|
||||
/// Validate the argv-level inputs. `Err` is a usage fault — the caller
|
||||
/// exits 2 (C14 partition).
|
||||
#[allow(dead_code)] // #241: `aura nodes new` (next task) becomes the caller
|
||||
pub fn scaffold_spec(
|
||||
name: &str,
|
||||
engine_path: Option<&Path>,
|
||||
@@ -196,6 +218,39 @@ This crate is an aura project: a cdylib of node/strategy blueprints the
|
||||
- Topology is data (`blueprints/*.json`), node logic is Rust.
|
||||
"#;
|
||||
|
||||
const GITIGNORE_PROJECT: &str = "/runs\n";
|
||||
|
||||
const SIGNAL_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___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"}]}}"#;
|
||||
|
||||
/// The open sibling of `SIGNAL_JSON_STD`: `fast.length` is a free knob, so
|
||||
/// the scaffolded sweep quickstart works out of the box (`run` requires a
|
||||
/// closed blueprint; `sweep` requires open knobs — one file cannot serve
|
||||
/// both, #241).
|
||||
const SIGNAL_OPEN_JSON_STD: &str = r#"{"format_version":1,"blueprint":{"name":"__NAME_SNAKE___signal_open","nodes":[{"primitive":{"type":"SMA","name":"fast"}},{"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"}]}}"#;
|
||||
|
||||
const CLAUDE_MD_PROJECT: &str = r#"# __NAME__ — an aura research project (data-only)
|
||||
|
||||
This directory is an aura project: blueprints + research documents over the
|
||||
std vocabulary, anchored by `Aura.toml`. There is no crate and no build step.
|
||||
|
||||
- Run: `aura run blueprints/signal.json` (a *closed* blueprint — every
|
||||
param bound)
|
||||
- Sweep: `aura sweep blueprints/signal_open.json --axis __NAME_SNAKE___signal_open.fast.length=2,4,8`
|
||||
(a sweep varies *open* knobs; `aura sweep <bp> --list-axes` lists them)
|
||||
- Native nodes: when the project needs its first project-specific node,
|
||||
`aura nodes new <name>` scaffolds a node crate beside this project and
|
||||
attaches it via `[nodes]` in `Aura.toml` (build it with `cargo build`).
|
||||
- Topology is data (`blueprints/*.json`); results land in `runs/`.
|
||||
"#;
|
||||
|
||||
/// Render a data-only project template: only `__NAME__`/`__NAME_SNAKE__`
|
||||
/// tokens exist in this template set (no namespace, no engine path).
|
||||
fn render_project(template: &str, name: &str) -> String {
|
||||
template
|
||||
.replace("__NAME_SNAKE__", &snake(name))
|
||||
.replace("__NAME__", name)
|
||||
}
|
||||
|
||||
fn render(template: &str, spec: &ScaffoldSpec) -> String {
|
||||
template
|
||||
.replace("__NAME_SNAKE__", &snake(&spec.name))
|
||||
@@ -206,8 +261,38 @@ fn render(template: &str, spec: &ScaffoldSpec) -> String {
|
||||
|
||||
// --------------------------------------------------------------------- emit
|
||||
|
||||
/// Emit a data-only project. `Err` is a runtime refusal (exit 1). All
|
||||
/// refusals fire before the first write.
|
||||
pub fn scaffold_project(spec: &ProjectScaffoldSpec) -> Result<(), String> {
|
||||
if spec.target_dir.exists() {
|
||||
return Err(format!("destination `{}` already exists", spec.target_dir.display()));
|
||||
}
|
||||
let write = |rel: &str, body: String| -> Result<(), String> {
|
||||
let path = spec.target_dir.join(rel);
|
||||
if let Some(dir) = path.parent() {
|
||||
std::fs::create_dir_all(dir)
|
||||
.map_err(|e| format!("creating `{}`: {e}", dir.display()))?;
|
||||
}
|
||||
std::fs::write(&path, body).map_err(|e| format!("writing `{}`: {e}", path.display()))
|
||||
};
|
||||
write("Aura.toml", AURA_TOML.to_string())?;
|
||||
write(".gitignore", GITIGNORE_PROJECT.to_string())?;
|
||||
write("blueprints/signal.json", render_project(SIGNAL_JSON_STD, &spec.name))?;
|
||||
write("blueprints/signal_open.json", render_project(SIGNAL_OPEN_JSON_STD, &spec.name))?;
|
||||
write("CLAUDE.md", render_project(CLAUDE_MD_PROJECT, &spec.name))?;
|
||||
if inside_git_work_tree(&spec.target_dir) {
|
||||
return Ok(());
|
||||
}
|
||||
match std::process::Command::new("git").arg("init").current_dir(&spec.target_dir).output() {
|
||||
Ok(o) if o.status.success() => {}
|
||||
_ => eprintln!("aura: warning: git init failed (project created without a repo)"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Emit the project. `Err` is a runtime refusal — the caller exits 1 (C14
|
||||
/// partition). All refusals fire before the first write.
|
||||
#[allow(dead_code)] // #241: `aura nodes new` (next task) becomes the caller
|
||||
pub fn scaffold(spec: &ScaffoldSpec) -> Result<(), String> {
|
||||
if spec.target_dir.exists() {
|
||||
return Err(format!(
|
||||
@@ -301,6 +386,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn templates_render_without_leftover_tokens() {
|
||||
// Crate-template set (native tier, via `ScaffoldSpec`).
|
||||
let spec = spec_for("demo-lab", None);
|
||||
for t in [CARGO_TOML, AURA_TOML, GITIGNORE, LIB_RS, SIGNAL_JSON, CLAUDE_MD] {
|
||||
let out = render(t, &spec);
|
||||
@@ -315,6 +401,18 @@ mod tests {
|
||||
let cargo = render(CARGO_TOML, &spec);
|
||||
assert!(cargo.contains("name = \"demo-lab\""));
|
||||
assert!(cargo.contains("/crates/aura-core"));
|
||||
|
||||
// Project-template set (data-only tier, via `render_project`).
|
||||
for t in [GITIGNORE_PROJECT, SIGNAL_JSON_STD, SIGNAL_OPEN_JSON_STD, CLAUDE_MD_PROJECT] {
|
||||
let out = render_project(t, "demo-lab");
|
||||
assert!(!out.contains("__"), "unsubstituted token in: {out}");
|
||||
}
|
||||
let claude = render_project(CLAUDE_MD_PROJECT, "demo-lab");
|
||||
assert!(claude.contains("demo-lab"));
|
||||
// The CLAUDE.md sweep quickstart targets the open sibling with the
|
||||
// rendered blueprint-name axis prefix (a bound starter cannot sweep).
|
||||
assert!(claude.contains("blueprints/signal_open.json"));
|
||||
assert!(claude.contains("demo_lab_signal_open.fast.length"));
|
||||
}
|
||||
|
||||
/// Property (#183, fieldtest F2 gap): the scaffold's own starter node teaches
|
||||
@@ -348,6 +446,35 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// #241: the data-only starter blueprints reference std vocabulary only —
|
||||
/// no project-namespaced (`::`) type id survives in the rendered JSON.
|
||||
/// The closed one binds every param (runnable); the open sibling leaves
|
||||
/// `fast.length` free (sweepable).
|
||||
#[test]
|
||||
fn data_only_starter_blueprint_is_std_only() {
|
||||
let json = render_project(SIGNAL_JSON_STD, "demo-lab");
|
||||
assert!(!json.contains("__"), "unsubstituted token: {json}");
|
||||
assert!(!json.contains("::"), "project-namespaced id in std-only blueprint: {json}");
|
||||
assert!(json.contains("\"name\":\"demo_lab_signal\""));
|
||||
let open = render_project(SIGNAL_OPEN_JSON_STD, "demo-lab");
|
||||
assert!(!open.contains("::"), "project-namespaced id in std-only blueprint: {open}");
|
||||
assert!(open.contains("\"name\":\"demo_lab_signal_open\""));
|
||||
assert!(
|
||||
open.contains(r#"{"primitive":{"type":"SMA","name":"fast"}}"#),
|
||||
"the open sibling must leave fast.length unbound: {open}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_project_refuses_existing_destination() {
|
||||
let tmp = std::env::temp_dir().join(format!("aura-pscaf-{}", std::process::id()));
|
||||
std::fs::create_dir_all(tmp.join("x")).unwrap();
|
||||
let spec = project_scaffold_spec("x", &tmp).unwrap();
|
||||
let e = scaffold_project(&spec).unwrap_err();
|
||||
assert!(e.contains("already exists"), "{e}");
|
||||
std::fs::remove_dir_all(&tmp).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffold_refuses_existing_dir_and_missing_engine() {
|
||||
let tmp = std::env::temp_dir().join(format!("aura-scaf-{}", std::process::id()));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! E2E: the `aura new` scaffolder (cycle 0103). Proves the full authoring
|
||||
//! loop — scaffold → cargo build → aura run — with zero hand-editing, plus
|
||||
//! the refusal contract. Scaffolded projects live in per-test temp dirs;
|
||||
//! their path-deps resolve into this checkout (the baked engine root).
|
||||
//! E2E: the `aura new` scaffolder (#241 T4). Proves the data-only authoring
|
||||
//! loop — scaffold → `aura run`/`aura sweep`, zero Rust toolchain interaction
|
||||
//! — plus the refusal contract. Scaffolded projects live in per-test temp
|
||||
//! dirs.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
@@ -21,9 +21,53 @@ fn tmp(tag: &str) -> PathBuf {
|
||||
d
|
||||
}
|
||||
|
||||
/// Best-effort commit so a scaffolded project's `project_commit` (git
|
||||
/// `rev-parse HEAD`, `project.rs`) has something to resolve: a bare fresh
|
||||
/// `git init` alone (what `aura new` does outside a work tree, and what a
|
||||
/// plain `git init` with no commits leaves too) has an unborn HEAD, so
|
||||
/// `rev-parse HEAD` fails and `project_commit` returns `None` — verified: an
|
||||
/// enclosing dir that is only `git init`-ed (no commit) makes the run
|
||||
/// report's `manifest.project` render as `{}` (no `commit` key at all),
|
||||
/// which is exactly the key the task text requires this test to assert on.
|
||||
/// This helper is not itself required by the task text, but the assertion
|
||||
/// it enables is, so it stays in scope.
|
||||
fn init_and_commit(dir: &Path) {
|
||||
let git = |args: &[&str]| {
|
||||
let out = Command::new("git").args(args).current_dir(dir).output().expect("run git");
|
||||
assert!(out.status.success(), "git {args:?} failed: {}", String::from_utf8_lossy(&out.stderr));
|
||||
};
|
||||
git(&["init"]);
|
||||
git(&["config", "user.email", "test@example.com"]);
|
||||
git(&["config", "user.name", "test"]);
|
||||
std::fs::write(dir.join("README.md"), "base\n").unwrap();
|
||||
git(&["add", "-A"]);
|
||||
git(&["commit", "-m", "init"]);
|
||||
}
|
||||
|
||||
/// #241 headline: a fresh data-only project runs the starter blueprint with
|
||||
/// zero cargo/Rust toolchain interaction; the run report's provenance stamps
|
||||
/// a commit but no namespace (no crate was ever loaded).
|
||||
///
|
||||
/// Documented deviation from the task text: the plan's wording asks this
|
||||
/// headline to also assert that the run "writes a manifest under `x/runs/`".
|
||||
/// Verified against `dispatch_run`/`run_signal_r` (main.rs, around the
|
||||
/// `tx_req`/`rx_req` channel comment): plain `aura run` never touches the
|
||||
/// trace store — it only prints the `RunReport` to stdout; only
|
||||
/// `sweep`/`mc`/campaign verbs persist (`env.trace_store()`), a pre-#241
|
||||
/// split this task's file list does not touch (giving `run` a store write
|
||||
/// would widen scope beyond `scaffold.rs`/`main.rs`'s `NewCmd`/`dispatch_new`
|
||||
/// and this test file). There is thus no on-disk manifest for THIS verb to
|
||||
/// assert against; the assertion below proves that absence directly instead
|
||||
/// of only asserting it in prose, and the persisted-store property the plan
|
||||
/// wanted is covered by `data_only_project_sweeps_without_any_build` below,
|
||||
/// which does assert `proj.join("runs").exists()`.
|
||||
#[test]
|
||||
fn new_scaffold_builds_and_runs_the_authoring_loop() {
|
||||
fn new_scaffolds_and_runs_the_data_only_loop() {
|
||||
let base = tmp("loop");
|
||||
// A committed enclosing tree gives the scaffolded project's stamped
|
||||
// provenance a resolvable `git rev-parse HEAD` (a bare fresh repo has
|
||||
// none yet); `aura new` skips its own git-init inside a work tree.
|
||||
init_and_commit(&base);
|
||||
let out = aura(&["new", "demo-lab"], &base);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
@@ -32,25 +76,17 @@ fn new_scaffold_builds_and_runs_the_authoring_loop() {
|
||||
);
|
||||
let proj = base.join("demo-lab");
|
||||
for f in [
|
||||
"Cargo.toml",
|
||||
"Aura.toml",
|
||||
".gitignore",
|
||||
"src/lib.rs",
|
||||
"blueprints/signal.json",
|
||||
"blueprints/signal_open.json",
|
||||
"CLAUDE.md",
|
||||
] {
|
||||
assert!(proj.join(f).is_file(), "missing scaffolded file {f}");
|
||||
}
|
||||
let build = Command::new("cargo")
|
||||
.arg("build")
|
||||
.current_dir(&proj)
|
||||
.output()
|
||||
.expect("cargo build scaffolded project");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"scaffolded build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
assert!(!proj.join("Cargo.toml").exists(), "data-only project must have no Cargo.toml");
|
||||
assert!(!proj.join("src/lib.rs").exists(), "data-only project must have no src/lib.rs");
|
||||
|
||||
let a = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
assert!(
|
||||
a.status.success(),
|
||||
@@ -60,11 +96,39 @@ fn new_scaffold_builds_and_runs_the_authoring_loop() {
|
||||
let b = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
assert_eq!(a.stdout, b.stdout, "two runs must be byte-identical (C1)");
|
||||
let v: serde_json::Value = serde_json::from_slice(&a.stdout).expect("report is JSON");
|
||||
assert_eq!(v["manifest"]["project"]["namespace"], "demo_lab");
|
||||
let introspect = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(introspect.status.success());
|
||||
let text = String::from_utf8_lossy(&introspect.stdout);
|
||||
assert!(text.contains("demo_lab::Scale"), "project id listed");
|
||||
let p = &v["manifest"]["project"];
|
||||
assert!(p.get("commit").is_some(), "data-only project must stamp a commit: {p}");
|
||||
assert!(p.get("namespace").is_none(), "no crate was loaded, namespace must be absent: {p}");
|
||||
assert!(
|
||||
!proj.join("runs").exists(),
|
||||
"plain `aura run` must not persist a trace store (only sweep/mc/campaign do) — \
|
||||
the task text's \"writes a manifest under x/runs/\" does not hold for this verb"
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
/// #241 headline: a fresh data-only project sweeps a dissolved verb (#218's
|
||||
/// gate) with zero build step — using the scaffolded open starter blueprint
|
||||
/// and the exact axis the scaffolded CLAUDE.md advertises.
|
||||
///
|
||||
/// Why a second starter blueprint exists at all: `aura run` requires a
|
||||
/// CLOSED blueprint (every param bound) while `aura sweep` requires OPEN
|
||||
/// knobs ("this blueprint is fully bound; nothing to sweep"), so one file
|
||||
/// cannot serve both quickstart lines. `blueprints/signal.json` stays closed
|
||||
/// for the run headline above; `blueprints/signal_open.json` leaves
|
||||
/// `fast.length` free for this one.
|
||||
#[test]
|
||||
fn data_only_project_sweeps_without_any_build() {
|
||||
let base = tmp("sweep");
|
||||
let new = aura(&["new", "scratch"], &base);
|
||||
assert!(new.status.success(), "{}", String::from_utf8_lossy(&new.stderr));
|
||||
let proj = base.join("scratch");
|
||||
let out = aura(
|
||||
&["sweep", "blueprints/signal_open.json", "--axis", "scratch_signal_open.fast.length=2,4"],
|
||||
&proj,
|
||||
);
|
||||
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
|
||||
assert!(proj.join("runs").exists(), "store must land inside the project");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
@@ -91,56 +155,6 @@ fn new_rejects_invalid_names_as_usage_faults() {
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_refuses_a_missing_engine_path_with_hint() {
|
||||
let base = tmp("noengine");
|
||||
let out = aura(&["new", "x", "--engine-path", "/nonexistent-engine"], &base);
|
||||
assert_eq!(out.status.code(), Some(1), "runtime refusal");
|
||||
let err = String::from_utf8_lossy(&out.stderr);
|
||||
assert!(err.contains("--engine-path"), "stderr: {err}");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
/// Property: an explicit `--namespace` override actually reaches the
|
||||
/// runtime vocabulary (not just the `ScaffoldSpec` struct field checked at
|
||||
/// the unit layer) — a scaffolded, built, run project registers its node
|
||||
/// under the caller-chosen namespace, and the manifest records it too.
|
||||
#[test]
|
||||
fn new_namespace_override_reaches_the_built_vocabulary() {
|
||||
let base = tmp("customns");
|
||||
let out = aura(&["new", "demo-lab", "--namespace", "custom_ns"], &base);
|
||||
assert!(
|
||||
out.status.success(),
|
||||
"aura new failed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
let proj = base.join("demo-lab");
|
||||
let build = Command::new("cargo")
|
||||
.arg("build")
|
||||
.current_dir(&proj)
|
||||
.output()
|
||||
.expect("cargo build scaffolded project");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"scaffolded build failed:\n{}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
let run = aura(&["run", "blueprints/signal.json"], &proj);
|
||||
assert!(
|
||||
run.status.success(),
|
||||
"run failed: {}",
|
||||
String::from_utf8_lossy(&run.stderr)
|
||||
);
|
||||
let v: serde_json::Value = serde_json::from_slice(&run.stdout).expect("report is JSON");
|
||||
assert_eq!(v["manifest"]["project"]["namespace"], "custom_ns");
|
||||
let introspect = aura(&["graph", "introspect", "--vocabulary"], &proj);
|
||||
assert!(introspect.status.success());
|
||||
let text = String::from_utf8_lossy(&introspect.stdout);
|
||||
assert!(text.contains("custom_ns::Scale"), "overridden namespace listed: {text}");
|
||||
assert!(!text.contains("demo_lab::Scale"), "default-derived namespace must not leak");
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
/// Property: `aura new` mirrors `cargo new` — when the target directory is
|
||||
/// already inside a git work tree, the scaffolder does NOT initialize a
|
||||
/// nested repo. A nested `.git` under a tracked tree is a gitlink/submodule
|
||||
@@ -169,11 +183,10 @@ fn new_inside_a_work_tree_skips_the_nested_git() {
|
||||
let proj = base.join("demo-lab");
|
||||
// The full scaffold still lands unchanged...
|
||||
for f in [
|
||||
"Cargo.toml",
|
||||
"Aura.toml",
|
||||
".gitignore",
|
||||
"src/lib.rs",
|
||||
"blueprints/signal.json",
|
||||
"blueprints/signal_open.json",
|
||||
"CLAUDE.md",
|
||||
] {
|
||||
assert!(proj.join(f).is_file(), "missing scaffolded file {f}");
|
||||
@@ -209,9 +222,9 @@ fn new_outside_a_work_tree_initializes_git() {
|
||||
|
||||
#[test]
|
||||
fn new_works_inside_an_unbuilt_project_tree() {
|
||||
// Scaffold once (unbuilt), then scaffold AGAIN from inside that tree:
|
||||
// the load-bypass guard must keep aura new from trying to load the
|
||||
// enclosing project's (absent) dylib.
|
||||
// Scaffold once, then scaffold AGAIN from inside that tree: the
|
||||
// load-bypass guard must keep aura new from trying to load the enclosing
|
||||
// project's (data-only, but still discoverable) Aura.toml.
|
||||
let base = tmp("nested");
|
||||
let out = aura(&["new", "outer"], &base);
|
||||
assert!(out.status.success());
|
||||
@@ -222,6 +235,6 @@ fn new_works_inside_an_unbuilt_project_tree() {
|
||||
"aura new inside an unbuilt project tree must succeed: {}",
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
assert!(inner_cwd.join("inner/Cargo.toml").is_file());
|
||||
assert!(inner_cwd.join("inner/Aura.toml").is_file());
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user