use std::path::PathBuf;
use std::process::{Command, Output};
const BIN: &str = env!("CARGO_BIN_EXE_amph");
struct Home(PathBuf);
impl Home {
fn new(tag: &str) -> Self {
let dir = std::env::temp_dir().join(format!("amph-cli-{tag}"));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
Self(dir)
}
fn config(&self) -> PathBuf {
self.0.join("config.toml")
}
}
impl Drop for Home {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.0).ok();
}
}
fn amph(home: &Home, args: &[&str]) -> Output {
Command::new(BIN)
.args(args)
.env("AMPHETAMINE_CONFIG", home.config())
.output()
.expect("binary should run")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn version_matches_the_manifest() {
let h = Home::new("version");
let out = amph(&h, &["--version"]);
assert!(out.status.success());
assert!(stdout(&out).contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn help_names_every_subcommand() {
let h = Home::new("help");
let out = amph(&h, &["--help"]);
assert!(out.status.success());
let text = stdout(&out);
for cmd in [
"status", "boost", "focus", "restore", "setup", "add", "rm", "pick", "config",
] {
assert!(text.contains(cmd), "help does not mention `{cmd}`");
}
}
#[test]
fn an_unknown_flag_fails_with_the_usage_exit_code() {
let h = Home::new("badflag");
let out = amph(&h, &["--definitely-not-a-flag"]);
assert_eq!(out.status.code(), Some(2), "clap's usage-error exit code");
}
#[test]
fn config_path_honors_the_env_override() {
let h = Home::new("path");
let out = amph(&h, &["config", "path"]);
assert!(out.status.success());
assert_eq!(stdout(&out).trim(), h.config().to_string_lossy());
}
#[test]
fn status_json_is_valid_and_shaped() {
let h = Home::new("json");
let out = amph(&h, &["status", "--json", "--top", "3"]);
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&stdout(&out)).expect("valid JSON");
for key in ["memory", "swap", "power", "reclaimable", "top_apps"] {
assert!(v.get(key).is_some(), "missing key `{key}`");
}
assert!(
v["top_apps"].as_array().is_some_and(|a| a.len() <= 3),
"--top was not honored"
);
assert!(v["memory"]["total"].as_u64().is_some_and(|t| t > 0));
}
#[test]
fn config_init_writes_the_template_and_refuses_to_clobber() {
let h = Home::new("init");
assert!(amph(&h, &["config", "init"]).status.success());
let text = std::fs::read_to_string(h.config()).unwrap();
assert!(text.contains("[apps]") && text.contains("close = ["));
let again = amph(&h, &["config", "init"]);
assert!(!again.status.success(), "init must refuse to overwrite");
assert_eq!(
std::fs::read_to_string(h.config()).unwrap(),
text,
"the existing config was modified"
);
}
#[test]
fn first_boost_onboards_and_changes_nothing() {
let h = Home::new("onboard");
let out = amph(&h, &["boost"]);
assert!(out.status.success());
assert!(stdout(&out).contains("First run"));
let text = std::fs::read_to_string(h.config()).expect("onboarding writes the config");
let cfg: toml::Value = toml::from_str(&text).unwrap();
assert!(cfg["apps"]["close"].as_array().unwrap().is_empty());
assert!(cfg["focus"]["demote"].as_array().unwrap().is_empty());
}
#[test]
fn boost_dry_run_announces_itself_and_explains_the_empty_list() {
let h = Home::new("dry");
std::fs::write(h.config(), "[caches]\nenabled = false\n").unwrap();
let out = amph(&h, &["boost", "--dry-run"]);
assert!(out.status.success());
let text = stdout(&out);
assert!(text.contains("Dry run"));
assert!(text.contains("close list is empty"));
}
#[test]
fn adding_a_protected_app_is_refused_end_to_end() {
let h = Home::new("protected");
let out = amph(&h, &["add", "Cursor", "Docker"]);
assert!(out.status.success());
let text = stdout(&out);
assert!(text.contains("not added"), "refusal was not reported");
let cfg: toml::Value = toml::from_str(&std::fs::read_to_string(h.config()).unwrap()).unwrap();
assert!(cfg["apps"]["close"].as_array().unwrap().is_empty());
}
#[test]
fn setup_print_shows_only_the_restore_to_zero_rule() {
let h = Home::new("setup");
let out = amph(&h, &["setup", "--print"]);
assert!(out.status.success());
let rule = stdout(&out);
assert!(rule.contains("NOPASSWD"));
assert!(
rule.contains("/usr/bin/renice 0 -p"),
"the literal 0 is the whole security argument: {rule}"
);
}