use crate::config::{Config, load_last_good_config, opencrabs_home, save_last_good_config};
struct HomeGuard {
prev_home: Option<std::ffi::OsString>,
prev_userprofile: Option<std::ffi::OsString>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl HomeGuard {
fn new(temp_home: &std::path::Path) -> Self {
let lock = crate::tests::HOME_ENV_LOCK
.lock()
.unwrap_or_else(|p| p.into_inner());
let prev_home = std::env::var_os("HOME");
let prev_userprofile = std::env::var_os("USERPROFILE");
unsafe {
std::env::set_var("HOME", temp_home);
std::env::set_var("USERPROFILE", temp_home);
}
Self {
prev_home,
prev_userprofile,
_lock: lock,
}
}
}
impl Drop for HomeGuard {
fn drop(&mut self) {
match self.prev_home.take() {
Some(v) => unsafe { std::env::set_var("HOME", v) },
None => unsafe { std::env::remove_var("HOME") },
}
match self.prev_userprofile.take() {
Some(v) => unsafe { std::env::set_var("USERPROFILE", v) },
None => unsafe { std::env::remove_var("USERPROFILE") },
}
}
}
const GOOD_CONFIG: &str = r#"
[agent]
approval_policy = "auto-always"
"#;
const FIXABLE_BROKEN: &str = r#"
[agent]
approval_policy = "auto-always"
[providers.custom.broken]
models = ["a", "b", "c"
"#;
const UNFIXABLE_BROKEN: &str = r#"
[agent]
approval_policy = "auto-always" / oops
"#;
fn temp_home_with(config_toml: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().expect("tempdir");
let opencrabs = dir.path().join(".opencrabs");
std::fs::create_dir_all(&opencrabs).expect("create .opencrabs");
std::fs::write(opencrabs.join("config.toml"), config_toml).expect("write config");
std::fs::write(opencrabs.join("keys.toml"), b"").expect("write keys");
dir
}
#[test]
fn broken_config_does_not_poison_last_good_snapshot() {
let temp = temp_home_with(GOOD_CONFIG);
let _guard = HomeGuard::new(temp.path());
save_last_good_config();
let good = load_last_good_config().expect("snapshot of a valid config must load");
assert_eq!(good.agent.approval_policy, "auto-always");
let config_path = opencrabs_home().join("config.toml");
std::fs::write(&config_path, UNFIXABLE_BROKEN).expect("overwrite with broken config");
save_last_good_config();
let still_good =
load_last_good_config().expect("snapshot must still be the prior VALID config");
assert_eq!(
still_good.agent.approval_policy, "auto-always",
"a broken config.toml must NOT clobber the last-good snapshot"
);
}
#[test]
fn fixable_broken_config_is_auto_repaired_in_place() {
let temp = temp_home_with(FIXABLE_BROKEN);
let _guard = HomeGuard::new(temp.path());
let config_path = opencrabs_home().join("config.toml");
let raw = std::fs::read_to_string(&config_path).unwrap();
assert!(toml::from_str::<toml::Value>(&raw).is_err());
let cfg = Config::load().expect("load must auto-repair and succeed");
assert_eq!(cfg.agent.approval_policy, "auto-always");
assert!(
Config::was_autofixed(),
"load should report it auto-repaired the file"
);
let healed = std::fs::read_to_string(&config_path).unwrap();
assert!(
toml::from_str::<toml::Value>(&healed).is_ok(),
"config.toml must be valid on disk after auto-repair"
);
assert!(
config_path.with_extension("toml.autofix.bak").exists(),
"the broken original must be backed up"
);
}
#[test]
fn unfixable_broken_config_recovers_from_last_good() {
let temp = temp_home_with(GOOD_CONFIG);
let _guard = HomeGuard::new(temp.path());
save_last_good_config();
let config_path = opencrabs_home().join("config.toml");
std::fs::write(&config_path, UNFIXABLE_BROKEN).expect("overwrite with unfixable config");
let recovered = Config::load().expect("load must recover from last-good, not error out");
assert_eq!(
recovered.agent.approval_policy, "auto-always",
"recovery must preserve the auto-always policy so yolo mode survives a broken edit"
);
assert!(
Config::was_recovered(),
"the load should report it fell back to last-known-good"
);
}