use std::path::PathBuf;
use std::process::Command;
use std::sync::OnceLock;
fn current_profile_dir() -> PathBuf {
let test_exe = std::env::current_exe().expect("resolve current test executable");
test_exe
.parent()
.and_then(std::path::Path::parent)
.expect("resolve cargo profile dir")
.to_path_buf()
}
fn fallback_aicx_path() -> PathBuf {
let mut path = current_profile_dir().join("aicx");
if cfg!(windows) {
path.set_extension("exe");
}
path
}
fn ensure_aicx_binary_exists() -> PathBuf {
static BIN_PATH: OnceLock<PathBuf> = OnceLock::new();
BIN_PATH
.get_or_init(|| {
if let Some(env_path) = std::env::var_os("CARGO_BIN_EXE_aicx").map(PathBuf::from)
&& env_path.is_file()
{
return env_path;
}
let env_path = PathBuf::from(env!("CARGO_BIN_EXE_aicx"));
if env_path.is_file() {
return env_path;
}
let fallback = fallback_aicx_path();
if fallback.is_file() {
return fallback;
}
let cargo = std::env::var_os("CARGO")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("cargo"));
let output = Command::new(&cargo)
.args(["build", "--locked", "--bin", "aicx"])
.current_dir(env!("CARGO_MANIFEST_DIR"))
.output()
.expect("build fallback aicx binary");
assert!(output.status.success(), "fallback cargo build failed");
fallback
})
.clone()
}
fn unique_aicx_home(label: &str) -> PathBuf {
use std::time::{SystemTime, UNIX_EPOCH};
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock")
.as_nanos();
let dir = std::env::temp_dir().join(format!(
"aicx-mutwarn-{label}-{}-{suffix}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create temp aicx home");
dir
}
const WARN_TEXT_FRAGMENT: &str = "about to write to ~/.aicx/";
#[test]
fn migrate_emits_mutation_warning_on_bare_invocation() {
let bin = ensure_aicx_binary_exists();
let home = unique_aicx_home("migrate-bare");
let legacy = unique_aicx_home("migrate-legacy-src");
let output = Command::new(&bin)
.arg("migrate")
.arg("--legacy-root")
.arg(&legacy)
.arg("--store-root")
.arg(&home)
.env("AICX_HOME", &home)
.env("AICX_MUTATION_WARN_DELAY_SECONDS", "0")
.env_remove("AICX_NO_MUTATION_WARN")
.output()
.expect("run aicx migrate");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("aicx migrate: note:"),
"stderr should include mutation warning header; got:\n{stderr}"
);
assert!(
stderr.contains(WARN_TEXT_FRAGMENT),
"stderr should mention canonical aicx home; got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&home);
let _ = std::fs::remove_dir_all(&legacy);
}
#[test]
fn migrate_dry_run_skips_mutation_warning() {
let bin = ensure_aicx_binary_exists();
let home = unique_aicx_home("migrate-dry");
let legacy = unique_aicx_home("migrate-dry-legacy");
let output = Command::new(&bin)
.arg("migrate")
.arg("--dry-run")
.arg("--legacy-root")
.arg(&legacy)
.arg("--store-root")
.arg(&home)
.env("AICX_HOME", &home)
.env("AICX_MUTATION_WARN_DELAY_SECONDS", "0")
.env_remove("AICX_NO_MUTATION_WARN")
.output()
.expect("run aicx migrate --dry-run");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("aicx migrate: note:"),
"dry-run must NOT emit the mutation warning; got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&home);
let _ = std::fs::remove_dir_all(&legacy);
}
#[test]
fn aicx_no_mutation_warn_env_suppresses_warning() {
let bin = ensure_aicx_binary_exists();
let home = unique_aicx_home("noenv");
let legacy = unique_aicx_home("noenv-legacy");
let output = Command::new(&bin)
.arg("migrate")
.arg("--legacy-root")
.arg(&legacy)
.arg("--store-root")
.arg(&home)
.env("AICX_HOME", &home)
.env("AICX_NO_MUTATION_WARN", "1")
.env("AICX_MUTATION_WARN_DELAY_SECONDS", "0")
.output()
.expect("run aicx migrate with AICX_NO_MUTATION_WARN=1");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("aicx migrate: note:"),
"AICX_NO_MUTATION_WARN=1 must suppress the mutation warning; got:\n{stderr}"
);
assert!(
!stderr.contains(WARN_TEXT_FRAGMENT),
"suppression env must also suppress the body fragment; got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&home);
let _ = std::fs::remove_dir_all(&legacy);
}
#[test]
fn aicx_mutation_warn_delay_zero_skips_sleep_but_keeps_warning() {
let bin = ensure_aicx_binary_exists();
let home = unique_aicx_home("delay0");
let legacy = unique_aicx_home("delay0-legacy");
let start = std::time::Instant::now();
let output = Command::new(&bin)
.arg("migrate")
.arg("--legacy-root")
.arg(&legacy)
.arg("--store-root")
.arg(&home)
.env("AICX_HOME", &home)
.env("AICX_MUTATION_WARN_DELAY_SECONDS", "0")
.env_remove("AICX_NO_MUTATION_WARN")
.output()
.expect("run aicx migrate delay=0");
let elapsed = start.elapsed();
assert!(
elapsed.as_secs() < 3,
"delay=0 must skip the 3s sleep (elapsed: {:?})",
elapsed
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("aicx migrate: note:"),
"delay=0 must still emit the warning text; got:\n{stderr}"
);
assert!(
!stderr.contains("Ctrl-C within 0s"),
"delay=0 must not advertise a Ctrl-C window of 0 seconds; got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&home);
let _ = std::fs::remove_dir_all(&legacy);
}
#[test]
fn migrate_intent_schema_dry_run_skips_mutation_warning() {
let bin = ensure_aicx_binary_exists();
let home = unique_aicx_home("mis-dry");
let output = Command::new(&bin)
.arg("migrate-intent-schema")
.arg("--dry-run")
.arg("--store-root")
.arg(&home)
.env("AICX_HOME", &home)
.env("AICX_MUTATION_WARN_DELAY_SECONDS", "0")
.env_remove("AICX_NO_MUTATION_WARN")
.output()
.expect("run aicx migrate-intent-schema --dry-run");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("aicx migrate-intent-schema: note:"),
"dry-run must NOT emit the mutation warning; got:\n{stderr}"
);
let _ = std::fs::remove_dir_all(&home);
}