use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::config::{ApiProvider, Config};
use crate::provider_readiness::{ResolvedProviderReadiness, resolve_for_model};
struct HomeGuard {
prev_home: Option<std::ffi::OsString>,
prev_config_path: Option<std::ffi::OsString>,
}
impl Drop for HomeGuard {
fn drop(&mut self) {
unsafe {
match &self.prev_home {
Some(v) => std::env::set_var("CODEWHALE_HOME", v),
None => std::env::remove_var("CODEWHALE_HOME"),
}
match &self.prev_config_path {
Some(v) => std::env::set_var("CODEWHALE_CONFIG_PATH", v),
None => std::env::remove_var("CODEWHALE_CONFIG_PATH"),
}
}
}
}
fn sealed_home() -> &'static Path {
static HOME: std::sync::OnceLock<PathBuf> = std::sync::OnceLock::new();
HOME.get_or_init(|| {
let dir = tempfile::TempDir::new().expect("temp home").keep();
std::fs::write(
dir.join("config.toml"),
r#"provider = "deepseek"
[providers.deepseek]
api_key = "sk-test-scope-deepseek"
"#,
)
.expect("write config");
dir
})
}
fn sealed_env(config_path: Option<&Path>) -> HomeGuard {
let prev_home = std::env::var_os("CODEWHALE_HOME");
let prev_config_path = std::env::var_os("CODEWHALE_CONFIG_PATH");
unsafe {
std::env::set_var("CODEWHALE_HOME", sealed_home());
match config_path {
Some(path) => std::env::set_var("CODEWHALE_CONFIG_PATH", path),
None => std::env::remove_var("CODEWHALE_CONFIG_PATH"),
}
}
HomeGuard {
prev_home,
prev_config_path,
}
}
fn deepseek_readiness(config: &Config) -> ResolvedProviderReadiness {
resolve_for_model(
config,
ApiProvider::Deepseek,
"deepseek-v4-pro",
&crate::provider_readiness::ProviderReadinessSnapshot::default(),
)
}
fn workspace_with_config(dir: &Path, provider: &str) -> PathBuf {
let ws = dir.join(provider);
std::fs::create_dir_all(ws.join(".codewhale")).expect("workspace dir");
std::fs::write(
ws.join(".codewhale").join("config.toml"),
format!(
r#"provider = "{provider}"
[providers.{provider}]
# deliberately no api_key — this workspace selects a route it never
# authorized anywhere.
"#
),
)
.expect("write workspace config");
ws
}
#[test]
fn readiness_is_identical_across_unrelated_workspaces() {
let _lock = crate::test_support::lock_test_env();
let _home = sealed_env(None);
let base = tempfile::TempDir::new().expect("temp base");
let ws_a = base.path().join("project-a");
let ws_b = base.path().join("project-b");
std::fs::create_dir_all(&ws_a).expect("ws a");
std::fs::create_dir_all(&ws_b).expect("ws b");
let config_a = Config::load(None, None).expect("load from A");
let config_b = Config::load(None, None).expect("load from B");
let readiness_a = deepseek_readiness(&config_a);
let readiness_b = deepseek_readiness(&config_b);
assert_eq!(
readiness_a.label(),
readiness_b.label(),
"readiness must not depend on the launch folder: {} vs {}",
readiness_a.label(),
readiness_b.label()
);
assert!(
readiness_a.can_attempt(),
"the user-global key must make DeepSeek attemptable from A: {}",
readiness_a.label()
);
assert!(
readiness_b.can_attempt(),
"the user-global key must make DeepSeek attemptable from B: {}",
readiness_b.label()
);
}
#[test]
fn explicit_workspace_config_selects_its_route_without_locking_user_global() {
let _lock = crate::test_support::lock_test_env();
let base = tempfile::TempDir::new().expect("temp base");
let ws_a = workspace_with_config(base.path(), "zai");
let config_path = ws_a.join(".codewhale").join("config.toml");
let _home = sealed_env(Some(&config_path));
let config = Config::load(Some(config_path.clone()), None).expect("load workspace config");
assert_eq!(
config.api_provider(),
ApiProvider::Zai,
"the explicit workspace config selects zai"
);
let deepseek = deepseek_readiness(&config);
assert!(
deepseek.can_attempt(),
"authorization established once must stay visible: {}",
deepseek.label()
);
let zai = resolve_for_model(
&config,
ApiProvider::Zai,
"GLM-5.2",
&crate::provider_readiness::ProviderReadinessSnapshot::default(),
);
assert!(
!zai.can_attempt(),
"zai has no credential anywhere: {}",
zai.label()
);
let reason = zai.blocked_reason().map(|r| r.into_owned());
assert!(
reason.as_ref().is_some_and(|r| !r.trim().is_empty()),
"unavailable must carry a precise reason: {reason:?}"
);
}
fn write_nested_zai_config(base: &std::path::Path) -> std::path::PathBuf {
let nested = base.join("parent-repo").join("nested-repo");
std::fs::create_dir_all(nested.join(".codewhale")).expect("nested dir");
std::fs::write(
nested.join(".codewhale").join("config.toml"),
r#"provider = "zai"
[providers.zai]
"#,
)
.expect("nested config");
nested
}
fn assert_user_global_survives_workspace(label: &str, config_path: std::path::PathBuf) {
let _home = sealed_env(Some(&config_path));
let config = Config::load(Some(config_path.clone()), None)
.unwrap_or_else(|err| panic!("load from {label}: {err}"));
let deepseek = deepseek_readiness(&config);
assert!(
deepseek.can_attempt(),
"{label}: user-global authorization must survive: {}",
deepseek.label()
);
}
#[test]
fn nested_repo_does_not_change_readiness() {
let _lock = crate::test_support::lock_test_env();
let base = tempfile::TempDir::new().expect("temp base");
let nested = write_nested_zai_config(base.path());
assert_user_global_survives_workspace("nested", nested.join(".codewhale/config.toml"));
}
#[cfg(unix)]
#[test]
fn symlinked_worktree_does_not_change_readiness() {
let _lock = crate::test_support::lock_test_env();
let base = tempfile::TempDir::new().expect("temp base");
let nested = write_nested_zai_config(base.path());
let symlinked = base.path().join("symlink-worktree");
std::os::unix::fs::symlink(&nested, &symlinked).expect("symlink");
assert_user_global_survives_workspace("symlinked", symlinked.join(".codewhale/config.toml"));
}
#[test]
fn unavailable_truly_means_unavailable_with_a_reason() {
let _lock = crate::test_support::lock_test_env();
let _home = sealed_env(None);
let config = Config::load(None, None).expect("load config");
let moonshot = resolve_for_model(
&config,
ApiProvider::Moonshot,
"kimi-k2.6",
&crate::provider_readiness::ProviderReadinessSnapshot::default(),
);
assert!(!moonshot.can_attempt());
let reason = moonshot.blocked_reason().map(|r| r.into_owned());
assert!(
reason.as_ref().is_some_and(|r| !r.trim().is_empty()),
"unavailable must carry a precise reason: {reason:?}"
);
}
#[test]
fn repeated_readers_never_rewrite_configuration() {
let _lock = crate::test_support::lock_test_env();
let _home = sealed_env(None);
let config_path = sealed_home().join("config.toml");
let before = std::fs::read(&config_path).expect("read config before");
for _ in 0..2 {
let config = Config::load(Some(config_path.clone()), None).expect("load");
let readiness = deepseek_readiness(&config);
assert!(readiness.can_attempt());
}
let after = std::fs::read(&config_path).expect("read config after");
assert_eq!(before, after, "readers must never rewrite configuration");
}
#[test]
fn workspace_fleet_selection_affects_only_that_workspace() {
use crate::fleet::store::{FleetFile, FleetScope, save_fleet, selected_fleet, set_selected};
let _lock = crate::test_support::lock_test_env();
let home = tempfile::TempDir::new().expect("temp home");
std::fs::create_dir_all(home.path().join("fleets")).expect("fleets dir");
let prev = std::env::var_os("CODEWHALE_HOME");
unsafe { std::env::set_var("CODEWHALE_HOME", home.path()) };
let base = tempfile::TempDir::new().expect("temp base");
let ws_a = base.path().join("ws-a");
let ws_b = base.path().join("ws-b");
std::fs::create_dir_all(&ws_a).expect("ws a");
std::fs::create_dir_all(&ws_b).expect("ws b");
let fleet = FleetFile::new("Team A".to_string(), None).expect("fleet");
save_fleet(&fleet, FleetScope::Personal, &ws_a).expect("save personal");
set_selected("Team A", FleetScope::Workspace, &ws_a).expect("select in A");
assert!(
selected_fleet(&ws_a).is_some(),
"A has its folder selection"
);
assert!(selected_fleet(&ws_b).is_none(), "B must be unaffected");
let sel = selected_fleet(&ws_a).expect("selected in A");
assert_eq!(sel.scope, FleetScope::Personal);
assert!(
!ws_a.join(".codewhale/fleets/team-a.toml").exists(),
"a workspace selection must not copy the fleet file"
);
unsafe {
match prev {
Some(v) => std::env::set_var("CODEWHALE_HOME", v),
None => std::env::remove_var("CODEWHALE_HOME"),
}
}
}
#[test]
fn personal_fleet_selection_persists_across_restart() {
use crate::fleet::store::{FleetFile, FleetScope, save_fleet, selected_fleet, set_selected};
let _lock = crate::test_support::lock_test_env();
let home = tempfile::TempDir::new().expect("temp home");
std::fs::create_dir_all(home.path().join("fleets")).expect("fleets dir");
let prev = std::env::var_os("CODEWHALE_HOME");
unsafe { std::env::set_var("CODEWHALE_HOME", home.path()) };
let ws = tempfile::TempDir::new().expect("temp ws");
let fleet = FleetFile::new("My Default".to_string(), None).expect("fleet");
save_fleet(&fleet, FleetScope::Personal, ws.path()).expect("save personal");
set_selected("My Default", FleetScope::Personal, ws.path()).expect("select");
let sel = selected_fleet(ws.path()).expect("selection after restart");
assert_eq!(sel.name, "My Default");
assert_eq!(sel.scope, FleetScope::Personal);
unsafe {
match prev {
Some(v) => std::env::set_var("CODEWHALE_HOME", v),
None => std::env::remove_var("CODEWHALE_HOME"),
}
}
}
#[allow(dead_code)]
fn resolve_shared(config: Arc<Config>) -> ResolvedProviderReadiness {
resolve_for_model(
&config,
ApiProvider::Deepseek,
"deepseek-v4-pro",
&crate::provider_readiness::ProviderReadinessSnapshot::default(),
)
}