//! 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). use std::path::{Path, PathBuf}; use std::process::{Command, Output}; fn aura(args: &[&str], cwd: &Path) -> Output { Command::new(env!("CARGO_BIN_EXE_aura")) .args(args) .current_dir(cwd) .output() .expect("run aura") } fn tmp(tag: &str) -> PathBuf { let d = std::env::temp_dir().join(format!("aura-new-{tag}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&d); std::fs::create_dir_all(&d).unwrap(); d } #[test] fn new_scaffold_builds_and_runs_the_authoring_loop() { let base = tmp("loop"); let out = aura(&["new", "demo-lab"], &base); assert!( out.status.success(), "aura new failed: {}", String::from_utf8_lossy(&out.stderr) ); let proj = base.join("demo-lab"); for f in [ "Cargo.toml", "Aura.toml", ".gitignore", "src/lib.rs", "blueprints/signal.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) ); let a = aura(&["run", "blueprints/signal.json"], &proj); assert!( a.status.success(), "run failed: {}", String::from_utf8_lossy(&a.stderr) ); 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::Identity"), "project id listed"); let _ = std::fs::remove_dir_all(&base); } #[test] fn new_refuses_an_existing_destination() { let base = tmp("exists"); std::fs::create_dir_all(base.join("taken")).unwrap(); let out = aura(&["new", "taken"], &base); assert_eq!(out.status.code(), Some(1), "runtime refusal"); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("already exists"), "stderr: {err}"); let _ = std::fs::remove_dir_all(&base); } #[test] fn new_rejects_invalid_names_as_usage_faults() { let base = tmp("badname"); for bad in ["bad/name", "9lives"] { let out = aura(&["new", bad], &base); assert_eq!(out.status.code(), Some(2), "usage fault for {bad}"); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("invalid project name"), "stderr: {err}"); } 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::Identity"), "overridden namespace listed: {text}"); assert!(!text.contains("demo_lab::Identity"), "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 /// hazard (it bit the 0107 fieldtest corpus); the enclosing repo owns the /// scaffold's history. #[test] fn new_inside_a_work_tree_skips_the_nested_git() { let base = tmp("insidegit"); let init = Command::new("git") .arg("init") .current_dir(&base) .output() .expect("git init the enclosing work tree"); assert!( init.status.success(), "git init base failed: {}", String::from_utf8_lossy(&init.stderr) ); let out = aura(&["new", "demo-lab"], &base); assert!( out.status.success(), "aura new failed: {}", String::from_utf8_lossy(&out.stderr) ); 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", "CLAUDE.md", ] { assert!(proj.join(f).is_file(), "missing scaffolded file {f}"); } // ...but without a nested repo of its own. assert!( !proj.join(".git").exists(), "scaffold nested a .git inside an existing work tree (gitlink hazard)" ); let _ = std::fs::remove_dir_all(&base); } /// Guard against over-removal of the git-init: OUTSIDE any work tree the /// scaffold still initializes its own repo. `std::env::temp_dir()` is not /// inside a git work tree (the e2e helpers here already rely on that), so the /// scaffold's parent is genuinely detached from this checkout's tree. #[test] fn new_outside_a_work_tree_initializes_git() { let base = tmp("outsidegit"); let out = aura(&["new", "demo-lab"], &base); assert!( out.status.success(), "aura new failed: {}", String::from_utf8_lossy(&out.stderr) ); let proj = base.join("demo-lab"); assert!( proj.join(".git").is_dir(), "scaffold outside a work tree must initialize its own repo" ); let _ = std::fs::remove_dir_all(&base); } #[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. let base = tmp("nested"); let out = aura(&["new", "outer"], &base); assert!(out.status.success()); let inner_cwd = base.join("outer"); let out = aura(&["new", "inner"], &inner_cwd); assert!( out.status.success(), "aura new inside an unbuilt project tree must succeed: {}", String::from_utf8_lossy(&out.stderr) ); assert!(inner_cwd.join("inner/Cargo.toml").is_file()); let _ = std::fs::remove_dir_all(&base); }