use super::*;
const CONFIG: &str = r#"model_provider = "relay"
model = "gpt-test"
sandbox_mode = "danger-full-access"
approval_policy = "never"
[model_providers.relay]
base_url = "https://relay.invalid"
wire_api = "responses"
requires_openai_auth = true
experimental_bearer_token = "sk-bearer-secret"
"#;
fn homes() -> (tempfile::TempDir, tempfile::TempDir) {
(tempfile::tempdir().unwrap(), tempfile::tempdir().unwrap())
}
fn mode(path: &Path) -> u32 {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}
#[test]
fn copies_config_and_api_key_privately_without_printing_secrets() {
let (source, destination) = homes();
std::fs::write(source.path().join("config.toml"), CONFIG).unwrap();
let auth = r#"{"auth_mode":"apikey","OPENAI_API_KEY":"sk-auth-secret"}"#;
std::fs::write(source.path().join("auth.json"), auth).unwrap();
let notes = import_codex(source.path(), destination.path()).unwrap();
for file in ["config.toml", "auth.json"] {
let copied = destination.path().join(file);
assert_eq!(
std::fs::read_to_string(&copied).unwrap(),
std::fs::read_to_string(source.path().join(file)).unwrap()
);
assert_eq!(mode(&copied), 0o600);
}
let notes = notes.join("\n");
assert!(notes.contains(r#"model_provider "relay", model "gpt-test""#));
assert!(notes.contains(r#"sandbox_mode "danger-full-access" and approval_policy "never""#));
assert!(notes.contains("API-key sign-in"));
assert!(!notes.contains("secret"));
}
#[test]
fn never_copies_a_chatgpt_session() {
let (source, destination) = homes();
std::fs::write(source.path().join("config.toml"), "model = \"m\"\n").unwrap();
std::fs::write(
source.path().join("auth.json"),
r#"{"auth_mode":"chatgpt","OPENAI_API_KEY":null,"tokens":{"refresh_token":"rt"}}"#,
)
.unwrap();
let notes = import_codex(source.path(), destination.path()).unwrap();
assert!(destination.path().join("config.toml").is_file());
assert!(!destination.path().join("auth.json").exists());
assert!(notes.join("\n").contains("scv agents login codex"));
}
#[test]
fn env_key_providers_are_flagged() {
let (source, destination) = homes();
std::fs::write(
source.path().join("config.toml"),
"[model_providers.a]\nenv_key = \"OPENAI_API_KEY\"\n\
[model_providers.b]\nenv_key = \"RELAY_KEY\"\n",
)
.unwrap();
let notes = import_codex(source.path(), destination.path())
.unwrap()
.join("\n");
assert!(notes.contains("$OPENAI_API_KEY, which SCV removes"));
assert!(notes.contains("$RELAY_KEY; the SCV daemon's environment must provide it"));
}
const DSH: KeyStore = KeyStore::DshRefs {
path: ".dsh/.credentials.yaml",
variable: "DEEPSEEK_API_KEY",
};
const PI: KeyStore = KeyStore::Pi { dir: ".pi/agent" };
const GROK: KeyStore = KeyStore::Grok {
auth: ".grok/auth.json",
config: ".grok/config.toml",
};
const GROK_CONFIG: &str = r#"[cli]
installer = "internal"
# The relay profile.
[model.relay]
model = "grok-4.5"
base_url = "https://relay.invalid"
api_key = "xai-profile-secret"
api_backend = "responses"
[model."relay-4.7"]
model = "grok-4.7"
base_url = "https://relay.invalid"
api_key = "xai-profile-secret"
[models]
default = "relay-4.7"
"#;
fn grok_status_of(config: &str) -> (bool, String) {
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join(".grok")).unwrap();
std::fs::write(home.path().join(".grok/config.toml"), config).unwrap();
let StoredStatus { ready, lines } = stored_status(GROK, home.path()).unwrap();
(ready, lines.join("\n"))
}
#[test]
fn grok_config_keys_count_as_signed_in_without_printing_them() {
let (ready, lines) = grok_status_of(GROK_CONFIG);
assert!(ready);
assert_eq!(lines, r#"signed in (API key in config, model "relay-4.7")"#);
let (ready, _) =
grok_status_of(&GROK_CONFIG.replace(r#"default = "relay-4.7""#, r#"default = "grok-4.5""#));
assert!(ready);
let keyless = "[model.m]\nmodel = \"grok-4.7\"\n[models]\ndefault = \"m\"\n";
let (ready, lines) = grok_status_of(keyless);
assert!(!ready);
assert!(lines.contains(r#"default model "m" has no api_key in config"#));
assert!(!grok_status_of("[models]\ndefault = \"grok-9\"\n").0);
assert!(!grok_status_of("[cli]\ninstaller = \"internal\"\n").0);
let removed = "[model.m]\nenv_key = [\"XAI_API_KEY\"]\n[models]\ndefault = \"m\"\n";
let (ready, lines) = grok_status_of(removed);
assert!(!ready);
assert!(lines.contains("$XAI_API_KEY"), "{lines}");
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join(".grok")).unwrap();
std::fs::write(
home.path().join(".grok/config.toml"),
"api_key = xai-secret",
)
.unwrap();
let error = stored_status(GROK, home.path()).unwrap_err().to_string();
assert!(!error.contains("secret"), "{error}");
}
#[test]
fn grok_import_merges_by_table_privately_without_printing_keys() {
let (source, scv) = homes();
let destination = scv.path().join(".grok");
std::fs::create_dir(&destination).unwrap();
std::fs::write(source.path().join("config.toml"), GROK_CONFIG).unwrap();
std::fs::write(
source.path().join("auth.json"),
r#"{"a":{"key":"xai-token-secret"}}"#,
)
.unwrap();
std::fs::write(
destination.join("config.toml"),
"[marketplace]\ndefault_skills_installs_purged = true\n\n[cli]\ninstaller = \"old\"\n",
)
.unwrap();
let notes = import_grok(source.path(), &destination).unwrap().join("\n");
let copied = destination.join("config.toml");
assert_eq!(mode(&copied), 0o600);
let text = std::fs::read_to_string(&copied).unwrap();
assert!(text.starts_with(GROK_CONFIG), "user formatting is kept");
let table: toml::Table = text.parse().unwrap();
assert_eq!(table["cli"]["installer"].as_str(), Some("internal"));
assert_eq!(
table["marketplace"]["default_skills_installs_purged"].as_bool(),
Some(true)
);
assert_eq!(table["models"]["default"].as_str(), Some("relay-4.7"));
assert!(!destination.join("auth.json").exists());
assert!(notes.contains(r#"default model "relay-4.7"; profiles "relay", "relay-4.7""#));
assert!(notes.contains(r#"Kept SCV-only settings: "marketplace""#));
assert!(notes.contains("Skipped auth.json"));
assert!(!notes.contains("secret"), "{notes}");
assert!(stored_status(GROK, scv.path()).unwrap().ready);
}
#[test]
fn grok_import_writes_nothing_when_either_file_is_invalid() {
let (source, scv) = homes();
let destination = scv.path().join(".grok");
std::fs::create_dir(&destination).unwrap();
let existing = "[marketplace]\nkept = true\n";
std::fs::write(destination.join("config.toml"), existing).unwrap();
std::fs::write(source.path().join("config.toml"), "api_key = xai-secret").unwrap();
let error = import_grok(source.path(), &destination)
.unwrap_err()
.to_string();
assert!(!error.contains("secret"), "{error}");
assert_eq!(
std::fs::read_to_string(destination.join("config.toml")).unwrap(),
existing
);
std::fs::write(source.path().join("config.toml"), GROK_CONFIG).unwrap();
std::fs::write(destination.join("config.toml"), "broken = [").unwrap();
assert!(import_grok(source.path(), &destination).is_err());
assert_eq!(
std::fs::read_to_string(destination.join("config.toml")).unwrap(),
"broken = ["
);
assert!(import_grok(&destination, &destination).is_err());
let empty = tempfile::tempdir().unwrap();
assert!(import_grok(empty.path(), &destination).is_err());
}
#[test]
fn grok_import_keeps_top_level_values_outside_the_users_tables() {
let (source, scv) = homes();
let destination = scv.path().join(".grok");
std::fs::create_dir(&destination).unwrap();
std::fs::write(source.path().join("config.toml"), GROK_CONFIG).unwrap();
std::fs::write(destination.join("config.toml"), "version = 3\n").unwrap();
import_grok(source.path(), &destination).unwrap();
let table: toml::Table = std::fs::read_to_string(destination.join("config.toml"))
.unwrap()
.parse()
.unwrap();
assert_eq!(table["version"].as_integer(), Some(3));
assert!(table["models"].get("version").is_none());
assert_eq!(table["models"]["default"].as_str(), Some("relay-4.7"));
}
fn endpoint() -> Endpoint {
Endpoint {
base_url: "https://relay.invalid/v1/".into(),
api: WireApi::Responses,
model: "gpt-test".into(),
}
}
#[test]
fn dsh_keys_are_stored_privately_in_its_native_file() {
let home = tempfile::tempdir().unwrap();
assert!(!stored_status(DSH, home.path()).unwrap().ready);
let notes = store_key(DSH, home.path(), "sk-dsh-secret").unwrap();
assert!(!notes.join("\n").contains("secret"));
let path = home.path().join(".dsh/.credentials.yaml");
assert_eq!(
std::fs::read_to_string(&path).unwrap(),
"version: 1\n\nrefs:\n DEEPSEEK_API_KEY: \"sk-dsh-secret\"\n"
);
assert_eq!(mode(&path), 0o600);
assert_eq!(mode(&home.path().join(".dsh")), 0o700);
let StoredStatus { ready, lines } = stored_status(DSH, home.path()).unwrap();
assert!(ready);
assert!(!lines.join("\n").contains("secret"));
for invalid in ["", "two words", "quote\"d", "line\nbreak"] {
assert!(store_key(DSH, home.path(), invalid).is_err(), "{invalid:?}");
}
assert_eq!(
remove_stored(DSH, home.path()).unwrap(),
["Removed .dsh/.credentials.yaml"]
);
assert!(!path.exists());
assert!(!stored_status(DSH, home.path()).unwrap().ready);
}
#[test]
fn grok_login_entries_report_sign_in_without_values() {
let home = tempfile::tempdir().unwrap();
let store = GROK;
assert!(!stored_status(store, home.path()).unwrap().ready);
std::fs::create_dir(home.path().join(".grok")).unwrap();
std::fs::write(home.path().join(".grok/auth.json"), "{}").unwrap();
assert!(!stored_status(store, home.path()).unwrap().ready);
std::fs::write(
home.path().join(".grok/auth.json"),
r#"{"https://auth.x.ai::id":{"key":"xai-token-secret"}}"#,
)
.unwrap();
let StoredStatus { ready, lines } = stored_status(store, home.path()).unwrap();
assert!(ready);
assert!(!lines.join("\n").contains("secret"));
std::fs::write(home.path().join(".grok/auth.json"), "xai-token-secret").unwrap();
let error = stored_status(store, home.path()).unwrap_err().to_string();
assert!(!error.contains("secret"), "{error}");
}
#[test]
fn pi_endpoints_merge_into_pi_files_as_the_private_default() {
let home = tempfile::tempdir().unwrap();
let dir = home.path().join(".pi/agent");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("models.json"),
r#"{"providers":{"ollama":{"baseUrl":"http://localhost:11434/v1","api":"openai-completions","models":[{"id":"q"}]}}}"#,
)
.unwrap();
std::fs::write(dir.join("settings.json"), r#"{"theme":"dark"}"#).unwrap();
let notes = configure_pi_endpoint(&dir, &endpoint(), "sk-pi-secret").unwrap();
let notes = notes.join("\n");
assert!(
notes.contains("openai-responses at relay.invalid"),
"{notes}"
);
assert!(!notes.contains("secret"));
let read = |file: &str| -> serde_json::Value {
let path = dir.join(file);
assert_eq!(mode(&path), 0o600, "{file}");
serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap()
};
let models = read("models.json");
assert_eq!(
models["providers"]["scv"],
serde_json::json!({
"baseUrl": "https://relay.invalid/v1",
"api": "openai-responses",
"models": [{"id": "gpt-test"}],
"compat": {"sessionAffinityFormat": "openai-nosession"},
})
);
assert_eq!(models["providers"]["ollama"]["models"][0]["id"], "q");
assert_eq!(
read("auth.json")["scv"],
serde_json::json!({"type": "api_key", "key": "sk-pi-secret"})
);
let settings = read("settings.json");
assert_eq!(settings["defaultProvider"], "scv");
assert_eq!(settings["defaultModel"], "gpt-test");
assert_eq!(settings["theme"], "dark");
let StoredStatus { ready, lines } = stored_status(PI, home.path()).unwrap();
let lines = lines.join("\n");
assert!(ready);
assert!(
lines.contains(
r#"default provider "scv" (openai-responses at relay.invalid), model "gpt-test""#
),
"{lines}"
);
assert!(!lines.contains("secret"));
let removed = remove_stored(PI, home.path()).unwrap().join("\n");
assert!(removed.contains("auth.json"), "{removed}");
assert!(!dir.join("auth.json").exists());
let models = read("models.json");
assert!(models["providers"].get("scv").is_none());
assert!(models["providers"].get("ollama").is_some());
let settings = read("settings.json");
assert!(settings.get("defaultProvider").is_none());
assert_eq!(settings["theme"], "dark");
assert!(!stored_status(PI, home.path()).unwrap().ready);
}
#[test]
fn pi_endpoint_input_is_validated_before_any_write() {
let home = tempfile::tempdir().unwrap();
let dir = home.path().join(".pi/agent");
for base_url in [
"ftp://relay.invalid",
"https://user:pass@relay.invalid",
"https://relay.invalid/v1?key=x",
"https://",
] {
let endpoint = Endpoint {
base_url: base_url.into(),
..endpoint()
};
assert!(
configure_pi_endpoint(&dir, &endpoint, "sk").is_err(),
"{base_url}"
);
}
let endpoint = Endpoint {
model: "--flag".into(),
..endpoint()
};
assert!(configure_pi_endpoint(&dir, &endpoint, "sk").is_err());
assert!(configure_pi_endpoint(&dir, &super::tests::endpoint(), "").is_err());
assert!(!dir.exists());
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("models.json"), "not json").unwrap();
assert!(configure_pi_endpoint(&dir, &super::tests::endpoint(), "sk").is_err());
assert!(!dir.join("auth.json").exists());
assert!(!dir.join("settings.json").exists());
}
#[test]
fn invalid_input_writes_nothing() {
let (source, destination) = homes();
std::fs::write(source.path().join("config.toml"), "model = \"m\"\n").unwrap();
std::fs::write(source.path().join("auth.json"), "not json").unwrap();
assert!(import_codex(source.path(), destination.path()).is_err());
std::fs::write(source.path().join("config.toml"), "model = ").unwrap();
std::fs::remove_file(source.path().join("auth.json")).unwrap();
assert!(import_codex(source.path(), destination.path()).is_err());
assert_eq!(std::fs::read_dir(destination.path()).unwrap().count(), 0);
let empty = tempfile::tempdir().unwrap();
assert!(import_codex(empty.path(), destination.path()).is_err());
assert!(import_codex(destination.path(), destination.path()).is_err());
}
#[test]
fn the_nested_scv_gets_a_private_copy_of_the_provider() {
let home = tempfile::tempdir().unwrap();
let path = home.path().join("config.toml");
std::fs::write(&path, "[agent]\nmax_steps = 7\n").unwrap();
let headers =
std::collections::HashMap::from([("X-Team".to_owned(), scv_client::Secret::from("core"))]);
let key = "sk-nested-secret-0123456789";
let lines = configure_scv_child(
home.path(),
&ScvChildProvider {
wire_api: "responses",
model: "gpt-test",
base_url: "https://relay.invalid/v1/",
timeout_seconds: 600,
headers: &headers,
hosted_web_search: true,
},
key,
)
.unwrap();
assert!(lines.iter().all(|line| !line.contains(key)), "{lines:?}");
assert!(lines[0].contains("gpt-test") && lines[0].contains("relay.invalid"));
assert_eq!(mode(&path), 0o600);
let table: toml::Table = std::fs::read_to_string(&path).unwrap().parse().unwrap();
assert_eq!(
table["agent"]["max_steps"].as_integer(),
Some(7),
"other settings kept"
);
assert_eq!(
table["provider"]["active"].as_str(),
Some(SCV_CHILD_PROVIDER)
);
let profile = &table["providers"][SCV_CHILD_PROVIDER];
assert_eq!(
profile["base_url"].as_str(),
Some("https://relay.invalid/v1")
);
assert_eq!(profile["api_key"].as_str(), Some(key));
assert_eq!(profile["headers"]["X-Team"].as_str(), Some("core"));
assert_eq!(table["web"]["search"].as_str(), Some("provider"));
let store = KeyStore::Scv {
config: "config.toml",
};
let StoredStatus {
ready,
lines: status,
} = stored_status(store, home.path()).unwrap();
assert!(ready);
assert!(status.iter().all(|line| !line.contains(key)), "{status:?}");
assert!(status[0].contains("API key stored"), "{status:?}");
assert!(store_key(store, home.path(), key).is_err());
remove_stored(store, home.path()).unwrap();
let StoredStatus {
ready,
lines: status,
} = stored_status(store, home.path()).unwrap();
assert!(!ready);
assert!(status[0].contains("scv agents import scv"), "{status:?}");
}
#[test]
fn a_broken_nested_scv_config_is_not_overwritten() {
let home = tempfile::tempdir().unwrap();
let path = home.path().join("config.toml");
std::fs::write(&path, "not = [valid").unwrap();
let headers = std::collections::HashMap::new();
let provider = ScvChildProvider {
wire_api: "responses",
model: "gpt-test",
base_url: "https://relay.invalid",
timeout_seconds: 60,
headers: &headers,
hosted_web_search: false,
};
assert!(configure_scv_child(home.path(), &provider, "sk-key-0123456789").is_err());
assert_eq!(std::fs::read_to_string(&path).unwrap(), "not = [valid");
let bad_url = ScvChildProvider {
base_url: "ftp://relay.invalid",
..provider
};
std::fs::remove_file(&path).unwrap();
assert!(configure_scv_child(home.path(), &bad_url, "sk-key-0123456789").is_err());
assert!(!path.exists());
}
#[test]
fn claude_api_key_status_accepts_a_gateway_key_shape() {
use std::os::unix::fs::PermissionsExt;
let home = tempfile::tempdir().unwrap();
let key = home.path().join(".anthropic-api-key");
let write = |contents: &str, permissions: u32| {
std::fs::write(&key, contents).unwrap();
std::fs::set_permissions(&key, std::fs::Permissions::from_mode(permissions)).unwrap();
};
write("sk-gateway-key-not-anthropic\n", 0o600);
let ready = claude_status(AgentAuth::ClaudeApiKey, home.path()).unwrap();
assert!(ready.ready, "{ready:?}");
assert_eq!(ready.lines, ["signed in (API key)"]);
write("", 0o600);
let empty = claude_status(AgentAuth::ClaudeApiKey, home.path()).unwrap();
assert!(!empty.ready, "{empty:?}");
assert!(
empty
.lines
.iter()
.any(|line| line.contains("invalid shape"))
);
write(" \n", 0o600);
assert!(
!claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
write("sk-gateway key", 0o600);
let spaced = claude_status(AgentAuth::ClaudeApiKey, home.path()).unwrap();
assert!(!spaced.ready, "{spaced:?}");
assert!(
spaced
.lines
.iter()
.any(|line| line.contains("invalid shape"))
);
write("sk-gate\u{0007}way", 0o600);
assert!(
!claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
write(&"k".repeat(513), 0o600);
assert!(
!claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
write(&"k".repeat(512), 0o600);
assert!(
claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
write("sk-gateway-key\n", 0o644);
let open = claude_status(AgentAuth::ClaudeApiKey, home.path()).unwrap();
assert!(!open.ready, "{open:?}");
assert!(
open.lines.iter().any(|line| line.contains("chmod 600")),
"{open:?}"
);
}
#[test]
fn claude_stores_fail_closed_for_api_and_subscription_identities() {
let home = tempfile::tempdir().unwrap();
let key = home.path().join(".anthropic-api-key");
assert!(
!claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
std::fs::write(&key, "sk-ant-api03-test").unwrap();
std::fs::set_permissions(&key, std::fs::Permissions::from_mode(0o600)).unwrap();
assert!(
claude_status(AgentAuth::ClaudeApiKey, home.path())
.unwrap()
.ready
);
std::fs::create_dir_all(home.path().join(".claude")).unwrap();
std::fs::write(
home.path().join(".claude/.credentials.json"),
r#"{"claudeAiOauth":{"refreshToken":"refresh","scopes":["user:inference"],"subscriptionType":"max"}}"#,
)
.unwrap();
let blocked = claude_status(AgentAuth::ClaudeApiKey, home.path()).unwrap();
assert!(!blocked.ready);
assert!(
blocked.lines.iter().any(|line| {
line.contains("agents/claude-ai")
&& line.contains("HOME=$SCV_HOME/agents/claude claude auth logout")
}),
"{:?}",
blocked.lines
);
assert!(
blocked
.lines
.iter()
.all(|line| !line.contains("scv agents logout claude")),
"{:?}",
blocked.lines
);
assert!(
claude_status(AgentAuth::ClaudeSubscription, home.path())
.unwrap()
.ready
);
}
#[test]
fn claude_subscription_status_accepts_oauth_and_rejects_ambiguous_json() {
let home = tempfile::tempdir().unwrap();
let credentials = home.path().join(".claude/.credentials.json");
std::fs::create_dir_all(credentials.parent().unwrap()).unwrap();
std::fs::write(
&credentials,
r#"{"claudeAiOauth":{"refreshToken":"refresh","scopes":["user:inference"],"subscriptionType":"max"}}"#,
)
.unwrap();
assert_eq!(
claude_status(AgentAuth::ClaudeSubscription, home.path())
.unwrap()
.lines[0],
"signed in (Claude subscription, max)"
);
std::fs::write(home.path().join(".claude.json"), "not json").unwrap();
assert!(
!claude_status(AgentAuth::ClaudeSubscription, home.path())
.unwrap()
.ready
);
}
#[test]
fn a_large_claude_json_is_checked_for_a_console_key_without_failing_status() {
let home = tempfile::tempdir().unwrap();
let credentials = home.path().join(".claude/.credentials.json");
std::fs::create_dir_all(credentials.parent().unwrap()).unwrap();
std::fs::write(
&credentials,
r#"{"claudeAiOauth":{"refreshToken":"refresh","scopes":["user:inference"],"subscriptionType":"pro"}}"#,
)
.unwrap();
let config = home.path().join(".claude.json");
let padding = "a".repeat(MAX_IMPORT_BYTES as usize);
std::fs::write(&config, format!("{{\"padding\":\"{padding}\"}}")).unwrap();
let open = claude_status(AgentAuth::ClaudeSubscription, home.path()).unwrap();
assert!(open.ready, "{open:?}");
std::fs::write(
&config,
format!("{{\"primaryApiKey\":\"\",\"padding\":\"{padding}\"}}"),
)
.unwrap();
let empty = claude_status(AgentAuth::ClaudeSubscription, home.path()).unwrap();
assert!(empty.ready, "{empty:?}");
std::fs::write(
&config,
format!("{{\"padding\":\"{padding}\",\"primaryApiKey\":\"sk-ant-api03-console\"}}"),
)
.unwrap();
let blocked = claude_status(AgentAuth::ClaudeSubscription, home.path()).unwrap();
assert!(!blocked.ready, "{blocked:?}");
assert!(
blocked
.lines
.iter()
.any(|line| line.contains("Console API key")),
"{blocked:?}"
);
std::fs::write(&config, r#"{"nested":{"primaryApiKey":"sk-ant"}}"#).unwrap();
assert!(
claude_status(AgentAuth::ClaudeSubscription, home.path())
.unwrap()
.ready
);
let huge = std::fs::File::create(&config).unwrap();
huge.set_len(MAX_CLAUDE_CONFIG_BYTES + 1).unwrap();
let too_large = claude_status(AgentAuth::ClaudeSubscription, home.path()).unwrap();
assert!(!too_large.ready, "{too_large:?}");
assert!(
too_large
.lines
.iter()
.any(|line| line.contains("too large")),
"{too_large:?}"
);
}
const GEMINI: KeyStore = KeyStore::Gemini { dir: ".gemini" };
fn gemini_status_of(files: &[(&str, &str)]) -> (bool, String) {
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join(".gemini")).unwrap();
for (name, contents) in files {
std::fs::write(home.path().join(".gemini").join(name), contents).unwrap();
}
let StoredStatus { ready, lines } = stored_status(GEMINI, home.path()).unwrap();
(ready, lines.join("\n"))
}
const GOOGLE: &str = r#"{"security":{"auth":{"selectedType":"oauth-personal"}}}"#;
const API_KEY: &str =
r#"{"security":{"auth":{"selectedType":"gemini-api-key"}},"ui":{"theme":"x"}}"#;
const OAUTH_CREDS: &str = r#"{"access_token":"ya29.secret","refresh_token":"1//secret","token_type":"Bearer","expiry_date":1}"#;
#[test]
fn gemini_status_follows_the_auth_method_without_printing_credentials() {
assert_eq!(gemini_status_of(&[]), (false, "not signed in".to_owned()));
let (ready, lines) =
gemini_status_of(&[("settings.json", GOOGLE), ("oauth_creds.json", OAUTH_CREDS)]);
assert!(ready);
assert_eq!(lines, "signed in (Google account)");
assert!(!gemini_status_of(&[("settings.json", GOOGLE), ("oauth_creds.json", "{}")]).0);
assert!(!gemini_status_of(&[("oauth_creds.json", OAUTH_CREDS)]).0);
let (ready, lines) = gemini_status_of(&[
("settings.json", API_KEY),
("gemini-credentials.json", "0f:1e:c0ffee"),
]);
assert!(ready);
assert_eq!(lines, "signed in (Gemini API key, stored encrypted)");
let (ready, lines) = gemini_status_of(&[("settings.json", API_KEY)]);
assert!(!ready);
assert!(lines.contains("SCV removes $GEMINI_API_KEY"), "{lines}");
let (ready, lines) = gemini_status_of(&[
("settings.json", API_KEY),
(".env", "# for SCV\nexport GEMINI_API_KEY=\"AIza-secret\"\n"),
]);
assert!(ready);
assert_eq!(lines, "signed in (Gemini API key in .gemini/.env)");
assert!(
!gemini_status_of(&[
("settings.json", API_KEY),
(".env", "GEMINI_API_KEY=\nGEMINI_MODEL=pro\n"),
])
.0
);
let (ready, lines) = gemini_status_of(&[(
"settings.json",
r#"{"security":{"auth":{"selectedType":"vertex-ai"}}}"#,
)]);
assert!(!ready);
assert!(lines.contains("GOOGLE_CLOUD_PROJECT"), "{lines}");
let (ready, lines) = gemini_status_of(&[(
"settings.json",
r#"{"security":{"auth":{"selectedType":"compute-default-credentials"}}}"#,
)]);
assert!(ready);
assert!(lines.contains("\"compute-default-credentials\""), "{lines}");
let commented = "{\n // chosen at /auth\n \"security\": {\"auth\": {\"selectedType\": \"oauth-personal\"}}\n}";
let (ready, lines) = gemini_status_of(&[
("settings.json", commented),
("oauth_creds.json", OAUTH_CREDS),
]);
assert!(ready);
assert!(lines.contains("not plain JSON"), "{lines}");
assert!(!lines.contains("chosen at"), "{lines}");
assert!(!gemini_status_of(&[("settings.json", commented)]).0);
let home = tempfile::tempdir().unwrap();
std::fs::create_dir(home.path().join(".gemini")).unwrap();
std::fs::write(home.path().join(".gemini/oauth_creds.json"), "ya29.secret").unwrap();
let error = stored_status(GEMINI, home.path()).unwrap_err().to_string();
assert!(!error.contains("secret"), "{error}");
}
#[test]
fn gemini_logout_removes_its_sign_ins_and_keeps_its_settings() {
let home = tempfile::tempdir().unwrap();
let dir = home.path().join(".gemini");
std::fs::create_dir(&dir).unwrap();
assert_eq!(
remove_stored(GEMINI, home.path()).unwrap(),
["Nothing stored"]
);
for (name, contents) in [
("settings.json", GOOGLE),
("oauth_creds.json", OAUTH_CREDS),
("gemini-credentials.json", "0f:1e:c0ffee"),
(
"google_accounts.json",
r#"{"active":"me@example.com","old":[]}"#,
),
] {
std::fs::write(dir.join(name), contents).unwrap();
}
assert!(stored_status(GEMINI, home.path()).unwrap().ready);
let notes = remove_stored(GEMINI, home.path()).unwrap();
assert_eq!(
notes,
[
"Removed .gemini/oauth_creds.json (Google sign-in)",
"Removed .gemini/gemini-credentials.json (stored API key and tokens)",
"Removed .gemini/google_accounts.json (signed-in account)",
]
);
assert!(dir.join("settings.json").exists());
assert!(!stored_status(GEMINI, home.path()).unwrap().ready);
std::fs::write(dir.join(".env"), "GEMINI_API_KEY=AIza-secret\n").unwrap();
let notes = remove_stored(GEMINI, home.path()).unwrap();
assert_eq!(
notes,
[
"Nothing stored",
"Note: .gemini/.env still sets GEMINI_API_KEY; remove it there to sign out",
]
);
assert!(dir.join(".env").exists());
assert!(store_key(GEMINI, home.path(), "AIza-key-0123456789").is_err());
}