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 environment = extended
.command
.get_envs()
.filter_map(|(name, value)| {
Some((
name.to_string_lossy().into_owned(),
value?.to_string_lossy().into_owned(),
))
})
.collect::<std::collections::HashMap<_, _>>();
assert_eq!(
environment.get("ANTHROPIC_BASE_URL").map(String::as_str),
Some("http://router.test/api/services/anthropic")
);
assert_eq!(
environment
.get("CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY")
.map(String::as_str),
Some("1")
);
assert_eq!(
environment
.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
.map(String::as_str),
Some("0")
);
for cleared in [
"ANTHROPIC_API_KEY",
"ANTHROPIC_MODEL",
"ANTHROPIC_DEFAULT_OPUS_MODEL",
"ANTHROPIC_DEFAULT_SONNET_MODEL",
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
"CLAUDE_CODE_SUBAGENT_MODEL",
] {
assert_eq!(
environment.get(cleared).map(String::as_str),
Some(""),
"{cleared}"
);
}
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 zai_only_claude_launch_maps_default_families_subagents_and_resume() {
let models = [
RouterModel {
id: "future-first-2099".to_string(),
owned_by: crate::clients::ZAI_MODEL_OWNER.to_string(),
},
RouterModel {
id: "future-explicit-2099".to_string(),
owned_by: crate::clients::ZAI_MODEL_OWNER.to_string(),
},
];
let resumed = TemporaryClient::prepare(&Preparation {
client: ClientKind::ClaudeCode,
base_url: "http://router.test",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: false,
profile_root: None,
})
.expect("prepare a resumed z.ai-only Claude session");
let resumed_env = resumed
.command
.get_envs()
.filter_map(|(key, value)| {
Some((
key.to_string_lossy().into_owned(),
value?.to_string_lossy().into_owned(),
))
})
.collect::<std::collections::HashMap<_, _>>();
for key in crate::clients::CLAUDE_MODEL_ENV {
assert_eq!(
resumed_env.get(key).map(String::as_str),
Some("future-first-2099"),
"{key}"
);
}
let explicit = TemporaryClient::prepare(&Preparation {
client: ClientKind::ClaudeCode,
base_url: "http://router.test",
token: "task-token",
model_override: Some("future-explicit-2099"),
models: &models,
isolated_config: false,
one_shot: true,
profile_root: None,
})
.expect("prepare an explicit z.ai Claude model");
let explicit_env = explicit
.command
.get_envs()
.filter_map(|(key, value)| {
Some((
key.to_string_lossy().into_owned(),
value?.to_string_lossy().into_owned(),
))
})
.collect::<std::collections::HashMap<_, _>>();
for key in crate::clients::CLAUDE_MODEL_ENV {
assert_eq!(
explicit_env.get(key).map(String::as_str),
Some("future-explicit-2099"),
"explicit model must win for {key}"
);
}
}
#[test]
fn codex_overlays_routing_without_repointing_user_configuration() {
let models = [RouterModel {
id: "gpt-5.6-sol".to_string(),
owned_by: "codex".to_string(),
}];
assert!(
extends_user_configuration(ClientKind::Codex, false),
"ordinary Codex runs can layer routing through CLI configuration"
);
assert!(
!extends_user_configuration(ClientKind::Codex, true),
"explicit isolation must still replace the client configuration"
);
let prepared = TemporaryClient::prepare(&Preparation {
client: ClientKind::Codex,
base_url: "http://router.test/path?tenant=one",
token: "task-token",
model_override: None,
models: &models,
isolated_config: false,
one_shot: true,
profile_root: None,
})
.expect("prepare Codex overlay");
let environment = prepared
.command
.get_envs()
.map(|(name, value)| (name.to_string_lossy().into_owned(), value))
.collect::<std::collections::HashMap<_, _>>();
assert!(!environment.contains_key("HOME"), "{environment:?}");
assert!(!environment.contains_key("CODEX_HOME"), "{environment:?}");
assert_eq!(
environment
.get("LINK_ASSISTANT_TOKEN")
.and_then(|value| *value)
.map(|value| value.to_string_lossy()),
Some(std::borrow::Cow::Borrowed("task-token"))
);
let arguments = prepared
.command
.get_args()
.map(|argument| argument.to_string_lossy().into_owned())
.collect::<Vec<_>>();
assert_eq!(
arguments[0..3],
["-c", "model_provider=\"link-assistant\"", "-c"]
);
let catalog_path = arguments[3]
.strip_prefix("model_catalog_json=")
.and_then(|value| serde_json::from_str::<String>(value).ok())
.expect("process-local model catalog argument");
assert!(Path::new(&catalog_path).starts_with(prepared.directory.path()));
let catalog: serde_json::Value = serde_json::from_slice(
&std::fs::read(&catalog_path).expect("read process-local model catalog"),
)
.expect("parse process-local model catalog");
assert_eq!(catalog["models"][0]["slug"], "gpt-5.6-sol");
assert_eq!(catalog["models"].as_array().unwrap().len(), 1);
assert_eq!(
arguments[4..],
[
"-c",
"model_providers.link-assistant.name=\"Link.Assistant.Router\"",
"-c",
"model_providers.link-assistant.base_url=\"http://router.test/path?tenant=one/api/services/codex/v1\"",
"-c",
"model_providers.link-assistant.env_key=\"LINK_ASSISTANT_TOKEN\"",
"-c",
"model_providers.link-assistant.wire_api=\"responses\"",
]
);
let isolated = TemporaryClient::prepare(&Preparation {
client: ClientKind::Codex,
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 Codex");
let isolated_home = isolated
.command
.get_envs()
.find_map(|(name, value)| (name == "HOME").then_some(value?))
.expect("isolated Codex sets HOME");
assert_eq!(Path::new(isolated_home), isolated.directory.path());
assert!(
isolated
.command
.get_envs()
.any(|(name, value)| name == "CODEX_HOME" && value.is_none()),
"isolation must prevent an inherited CODEX_HOME from escaping"
);
assert!(isolated.command.get_args().next().is_none());
assert!(
isolated
.directory
.path()
.join(".codex/config.toml")
.is_file()
);
}
#[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"
);
}