use super::*;
#[test]
fn the_gemini_client_is_pointed_at_the_isolated_home() {
let root = tempfile::tempdir().expect("isolated root");
let manager = ClientManager::isolated(root.path());
let mut command = Command::new("gemini");
configure_isolation(
&mut command,
&manager,
root.path(),
ClientKind::GeminiCli,
true,
)
.expect("configure gemini isolation");
let environment: std::collections::HashMap<_, _> = command
.get_envs()
.filter_map(|(key, value)| Some((key.to_string_lossy().into_owned(), value?)))
.collect();
for name in ["HOME", "GEMINI_CLI_HOME"] {
assert_eq!(
environment.get(name).map(|value| value.to_string_lossy()),
Some(root.path().to_string_lossy()),
"{name} must name the isolated root, not the .gemini directory \
inside it — the CLI appends `.gemini` itself"
);
}
assert_eq!(
manager.config_path(ClientKind::GeminiCli),
root.path().join(".gemini/settings.json")
);
assert_eq!(
environment
.get("GEMINI_CLI_TRUST_WORKSPACE")
.map(|value| value.to_string_lossy()),
Some(std::borrow::Cow::Borrowed("true"))
);
}
#[test]
fn the_users_configuration_is_kept_by_default() {
let models = [RouterModel {
id: "test-model".to_string(),
owned_by: "test".to_string(),
}];
let extended = TemporaryClient::prepare(&Preparation {
client: ClientKind::ClaudeCode,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: None,
})
.expect("prepare with the default configuration handling");
let names: Vec<String> = extended
.command
.get_envs()
.map(|(name, _)| name.to_string_lossy().into_owned())
.collect();
assert!(
!names.iter().any(|name| name == "CLAUDE_CONFIG_DIR"),
"the user's configuration directory must not be repointed: {names:?}"
);
for required in ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_BASE_URL"] {
assert!(
names.iter().any(|name| name == required),
"{required} missing: {names:?}"
);
}
let isolated = TemporaryClient::prepare(&Preparation {
client: ClientKind::ClaudeCode,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: true,
one_shot: true,
profile_root: None,
})
.expect("prepare isolated");
assert!(
isolated
.command
.get_envs()
.any(|(name, _)| name == "CLAUDE_CONFIG_DIR"),
"--isolated-config must still give the client its own directory"
);
}
#[test]
fn a_file_configured_client_is_isolated_even_by_default() {
let models = [RouterModel {
id: "test-model".to_string(),
owned_by: "test".to_string(),
}];
assert!(
!extends_user_configuration(ClientKind::Opencode, false),
"opencode sets no base-url variable, so there is nothing to layer"
);
assert!(
extends_user_configuration(ClientKind::ClaudeCode, false),
"claude code sets both variables, so the default extends"
);
assert!(
!extends_user_configuration(ClientKind::ClaudeCode, true),
"--isolated-config wins over the default"
);
let profiles = tempfile::tempdir().expect("profile root");
TemporaryClient::prepare(&Preparation {
client: ClientKind::Opencode,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: Some(profiles.path()),
})
.expect("a file-configured client must still run");
}
#[test]
fn a_client_needing_a_written_file_is_isolated_despite_its_variables() {
let integration = ClientKind::GeminiCli.integration();
assert!(
integration.token_env.is_some() && integration.base_url_env.is_some(),
"the variables alone would otherwise qualify it for extending"
);
assert!(
!extends_user_configuration(ClientKind::GeminiCli, false),
"routing depends on a file only isolation makes reachable"
);
}
#[test]
fn a_prepared_gemini_run_leaves_settings_where_the_cli_reads_them() {
let models = [RouterModel {
id: "test-model".to_string(),
owned_by: "test".to_string(),
}];
let profiles = tempfile::tempdir().expect("profile root");
let temporary = TemporaryClient::prepare(&Preparation {
client: ClientKind::GeminiCli,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: Some(profiles.path()),
})
.expect("prepare gemini");
let root = temporary.directory.path();
let home = temporary
.command
.get_envs()
.find_map(|(name, value)| (name == "HOME").then_some(value?))
.expect("gemini run sets HOME");
let settings = Path::new(home).join(".gemini/settings.json");
assert!(
settings.is_file(),
"no settings at {}, which is where the CLI looks",
settings.display()
);
let written = fs::read_to_string(&settings).expect("read settings");
assert!(written.contains("gemini-api-key"), "{written}");
assert!(Path::new(home).starts_with(root), "HOME escaped the root");
}
#[test]
fn written_gemini_settings_replace_an_existing_file() {
let root = tempfile::tempdir().expect("isolated root");
let path = root.path().join(".gemini/settings.json");
fs::create_dir_all(path.parent().expect("parent")).expect("create directory");
fs::write(
&path,
r#"{"security":{"auth":{"selectedType":"oauth-personal"}}}"#,
)
.expect("seed a conflicting file");
write_gemini_settings(&path).expect("write settings");
let written = fs::read_to_string(&path).expect("read settings");
assert!(written.contains("gemini-api-key"), "{written}");
assert!(
!written.contains("oauth-personal"),
"the inherited value survived: {written}"
);
}
#[test]
fn gemini_settings_select_the_api_key_flow() {
let root = tempfile::tempdir().expect("isolated root");
let path = root.path().join(".gemini/settings.json");
write_gemini_settings(&path).expect("write settings");
let written: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&path).expect("read")).expect("valid JSON");
assert_eq!(
written["security"]["auth"]["selectedType"],
"gemini-api-key"
);
}
#[test]
fn a_client_that_cannot_be_extended_keeps_its_profile() {
let models = [RouterModel {
id: "test-model".to_string(),
owned_by: "test".to_string(),
}];
let profiles = tempfile::tempdir().expect("profile root");
for client in ClientKind::ALL {
if client == ClientKind::Cursor {
assert!(
TemporaryClient::prepare(&Preparation {
client,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: Some(profiles.path()),
})
.is_err()
);
continue;
}
let temporary = TemporaryClient::prepare(&Preparation {
client,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: Some(profiles.path()),
})
.unwrap_or_else(|error| panic!("{client} failed setup: {error}"));
let root = temporary.directory.path().to_path_buf();
assert_eq!(temporary.command.get_program(), client.command());
let environment = temporary
.command
.get_envs()
.filter_map(|(name, value)| value.map(|value| (name, value)))
.collect::<std::collections::HashMap<_, _>>();
if let Some(token_env) = client.token_env() {
assert_eq!(
environment.get(std::ffi::OsStr::new(token_env)).copied(),
Some(std::ffi::OsStr::new("task-token")),
"{client} did not receive its token environment"
);
}
for name in [
"HOME",
"CLAUDE_CONFIG_DIR",
"GEMINI_CLI_HOME",
"OPENCODE_CONFIG",
"OPENCODE_CONFIG_DIR",
] {
if let Some(value) = environment.get(std::ffi::OsStr::new(name)) {
assert!(
Path::new(value).starts_with(&root),
"{client} {name} escaped its root"
);
}
}
let keeps_a_profile = !extends_user_configuration(client, false);
drop(temporary);
assert_eq!(
root.exists(),
keeps_a_profile,
"{client}: a client routed through a written file must keep its profile, and \
one that only needs two environment variables must not leave a directory behind"
);
if keeps_a_profile {
assert!(
root.starts_with(profiles.path()),
"{client} profile must live under the router's own directory, not TMPDIR: \
{}",
root.display()
);
}
}
}
#[test]
fn two_runs_of_the_same_client_share_one_profile() {
let profiles = tempfile::tempdir().expect("profile root");
let root = Some(profiles.path());
let first = persistent_profile(ClientKind::Codex, root).expect("first profile");
let second = persistent_profile(ClientKind::Codex, root).expect("second profile");
assert_eq!(first, second);
assert!(first.is_dir());
assert_ne!(
first,
persistent_profile(ClientKind::GeminiCli, root).expect("another client")
);
}
#[test]
fn registry_order_matches_client_discriminants() {
for client in ClientKind::ALL {
assert_eq!(client.integration().kind, client);
}
}
#[test]
fn the_default_label_carries_no_directory_name() {
let label = format!("with-{}-{}", ClientKind::ClaudeCode, super::run_suffix());
assert!(label.starts_with("with-claude-"), "{label}");
let cwd = std::env::current_dir().expect("cwd");
let name = cwd
.file_name()
.expect("directory name")
.to_string_lossy()
.into_owned();
assert!(
!label.contains(&name),
"the working directory's name must not reach the router: {label} contains {name}"
);
assert_eq!(super::run_suffix().len(), 4, "a fixed-width run suffix");
assert_eq!(
super::run_suffix(),
super::run_suffix(),
"stable within one process, so one run has one label"
);
}