use serde_json::Value;
use std::{fs, path::Path, process::Command};
fn call(args: &[&str], cwd: &Path, success: bool) -> Value {
let out = Command::new(env!("CARGO_BIN_EXE_forge"))
.arg("--json")
.args(args)
.current_dir(cwd)
.output()
.unwrap();
assert_eq!(
out.status.success(),
success,
"{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).unwrap()
}
fn create(base: &Path) -> Value {
call(
&[
"demo",
"create",
base.join("example with spaces").to_str().unwrap(),
],
base,
true,
)
}
fn next(value: &Value, name: &str, base: &Path, success: bool) -> Value {
let args: Vec<&str> = value["commands"][name]
.as_array()
.unwrap()
.iter()
.skip(1)
.map(|v| v.as_str().unwrap())
.collect();
call(&args, base, success)
}
#[test]
fn native_demo_previews_are_read_only_and_real_execution_preserves_files() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let demo = create(&base);
let project = Path::new(demo["repo"].as_str().unwrap());
let state = Path::new(demo["state_dir"].as_str().unwrap());
assert_eq!(demo["executed"], false);
next(&demo, "inspect", &base, true);
next(&demo, "dry_run", &base, true);
assert!(!state.exists());
assert!(!project.join("hello.txt").exists());
assert!(!project.join(".ai-memory.toml").exists());
let result = next(&demo, "run", &base, true);
assert_eq!(result["run"]["status"], "passed");
assert_eq!(result["token_usage"]["calls"], 0);
assert_eq!(fs::read(project.join("preserved.txt")).unwrap(), b"keep\n");
assert!(state.join("state.sqlite").exists());
next(&demo, "report", &base, true);
fs::write(project.join("changed.txt"), b"new revision").unwrap();
let stale = next(&demo, "report", &base, false);
assert_eq!(stale["run"]["status"], "passed"); assert_eq!(stale["effective_status"], "blocked");
assert_eq!(stale["evidence_current"], false);
}
#[test]
fn native_demo_refuses_existing_directories_and_preserves_changed_artifacts() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let output = base.join("example with spaces");
let demo = create(&base);
fs::write(output.join("keep-extra.txt"), b"preserve").unwrap();
call(&["demo", "create", output.to_str().unwrap()], &base, false);
assert_eq!(
fs::read(output.join("keep-extra.txt")).unwrap(),
b"preserve"
);
let project = Path::new(demo["repo"].as_str().unwrap());
call(&["demo", "write"], project, true);
fs::write(project.join("hello.txt"), b"changed").unwrap();
call(&["demo", "write"], project, false);
call(&["demo", "verify", project.to_str().unwrap()], &base, false);
assert_eq!(fs::read(project.join("hello.txt")).unwrap(), b"changed");
}
#[test]
fn demo_cannot_pass_when_declared_evaluator_hash_is_invalid() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let demo = create(&base);
let path = Path::new(demo["manifest"].as_str().unwrap());
let mut manifest: Value = serde_json::from_slice(&fs::read(path).unwrap()).unwrap();
let inputs = manifest["gates"][0]["inputs"].as_object_mut().unwrap();
for digest in inputs.values_mut() {
*digest = Value::String("0".repeat(64));
}
fs::write(path, serde_json::to_vec(&manifest).unwrap()).unwrap();
let result = next(&demo, "run", &base, false);
assert_ne!(result["run"]["status"], "passed");
}
#[test]
fn init_memory_is_opt_in_and_existing_routing_is_preserved() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
fs::create_dir(base.join(".git")).unwrap();
let result = call(&["init"], &base, true);
assert_eq!(result["memory_config"], "not_requested");
assert!(!base.join(".ai-memory.toml").exists());
let content = b"workspace = \"existing\"\ncustom = true\n";
fs::write(base.join(".ai-memory.toml"), content).unwrap();
assert_eq!(call(&["init"], &base, true)["memory_config"], "preserved");
call(&["init", "--memory-workspace", "another"], &base, false);
assert_eq!(fs::read(base.join(".ai-memory.toml")).unwrap(), content);
let other = base.join("other");
fs::create_dir_all(other.join(".git")).unwrap();
call(&["init", "--memory-workspace", "my-team"], &other, true);
assert_eq!(
call(&["init", "--memory-workspace", "my-team"], &other, true)["memory_config"],
"preserved"
);
assert!(
fs::read_to_string(other.join(".ai-memory.toml"))
.unwrap()
.contains("my-team")
);
}
#[test]
fn init_preflights_existing_scaffolding_before_creating_memory() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
fs::create_dir(base.join(".git")).unwrap();
fs::create_dir(base.join(".forge")).unwrap();
fs::write(base.join(".forge/README.md"), "my existing file").unwrap();
call(&["init", "--memory-workspace", "my-team"], &base, false);
assert!(!base.join(".ai-memory.toml").exists());
assert_eq!(
fs::read_to_string(base.join(".forge/README.md")).unwrap(),
"my existing file"
);
}
#[test]
fn demo_rejects_inherited_git_routing_before_creating_files() {
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let destination = base.join("demo");
let out = Command::new(env!("CARGO_BIN_EXE_forge"))
.args(["--json", "demo", "create"])
.arg(&destination)
.env("GIT_DIR", base.join("unrelated-git"))
.output()
.unwrap();
assert!(!out.status.success());
assert!(!destination.exists());
assert!(String::from_utf8_lossy(&out.stdout).contains("GIT_DIR"));
}
#[cfg(unix)]
#[test]
fn demo_preserves_symlink_destinations_and_does_not_run_ambient_git_hooks() {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let existing = base.join("existing");
fs::create_dir(&existing).unwrap();
let link = base.join("link");
symlink(&existing, &link).unwrap();
call(&["demo", "create", link.to_str().unwrap()], &base, false);
assert!(link.is_symlink());
assert_eq!(fs::read_dir(&existing).unwrap().count(), 0);
let hooks = base.join("hooks");
fs::create_dir(&hooks).unwrap();
let marker = base.join("executed");
let hook = hooks.join("pre-commit");
fs::write(
&hook,
format!("#!/bin/sh\nprintf executed > '{}'\n", marker.display()),
)
.unwrap();
fs::set_permissions(hook, fs::Permissions::from_mode(0o700)).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_forge"))
.args(["--json", "demo", "create"])
.arg(base.join("demo"))
.env("GIT_CONFIG_COUNT", "1")
.env("GIT_CONFIG_KEY_0", "core.hooksPath")
.env("GIT_CONFIG_VALUE_0", hooks)
.env("GIT_TEMPLATE_DIR", base.join("hooks"))
.output()
.unwrap();
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stdout)
);
assert!(!marker.exists());
assert!(!base.join("demo/app/.git/hooks/pre-commit").exists());
}
#[cfg(unix)]
#[test]
fn doctor_inspects_both_names_and_never_executes_unknown_candidates() {
use std::os::unix::fs::{PermissionsExt, symlink};
let temp = tempfile::tempdir().unwrap();
let base = temp.path().canonicalize().unwrap();
let first = base.join("first");
let second = base.join("second");
fs::create_dir(&first).unwrap();
fs::create_dir(&second).unwrap();
let marker = base.join("executed");
fs::write(
first.join("forge"),
format!("#!/bin/sh\nprintf executed > '{}'\n", marker.display()),
)
.unwrap();
fs::set_permissions(first.join("forge"), fs::Permissions::from_mode(0o700)).unwrap();
symlink(env!("CARGO_BIN_EXE_forge"), second.join("forge")).unwrap();
symlink(env!("CARGO_BIN_EXE_forge"), second.join("illumitech-forge")).unwrap();
let out = Command::new(env!("CARGO_BIN_EXE_forge"))
.args(["--json", "installation", "doctor"])
.env(
"PATH",
std::env::join_paths([first.clone(), second]).unwrap(),
)
.output()
.unwrap();
assert!(out.status.success());
let value: Value = serde_json::from_slice(&out.stdout).unwrap();
let commands = value["path_diagnostics"]["commands"].as_array().unwrap();
assert_eq!(commands.len(), 2);
assert_eq!(
commands[0]["selected"]["path"],
first.join("forge").to_str().unwrap()
);
assert_eq!(commands[0]["selected"]["path_matches_running"], false);
assert!(commands[0]["selected"]["version"].is_null());
assert!(commands[0]["warning"].is_string());
assert_eq!(commands[1]["selected"]["path_matches_running"], true);
assert!(!marker.exists());
}