#![allow(clippy::unwrap_used, clippy::expect_used)]
#![cfg(unix)]
use super::*;
use crate::claude::{LinkState, classify_credentials_link};
use crate::profile::{
AppConfig, AppState, ClaudeCredentials, DivergenceChoice, OAuthToken, Profile, profile_dir,
read_json_file, save_profile,
};
use crate::testutil::HomeSandbox;
fn creds(access: &str, refresh: &str) -> ClaudeCredentials {
ClaudeCredentials {
claude_ai_oauth: Some(OAuthToken {
access_token: access.to_string(),
refresh_token: Some(refresh.to_string()),
expires_at: None,
scopes: None,
subscription_type: None,
}),
}
}
fn stored_profile(name: &str, c: Option<ClaudeCredentials>) -> Profile {
let mut p = Profile::new(name.to_string(), None, None);
p.credentials = c;
save_profile(&p).expect("save profile");
p
}
fn seed_diverged(
active: &str,
active_stored: ClaudeCredentials,
live: &ClaudeCredentials,
target: &str,
target_stored: Option<ClaudeCredentials>,
) -> AppConfig {
let active_profile = stored_profile(active, Some(active_stored));
let target_profile = stored_profile(target, target_stored);
let live_path = crate::profile::claude_dir()
.expect("claude dir")
.join(".credentials.json");
std::fs::create_dir_all(live_path.parent().expect("parent")).expect("mkdir .claude");
std::fs::write(&live_path, serde_json::to_vec(live).expect("ser live")).expect("write live");
let mut config = AppConfig {
state: AppState::default(),
profiles: vec![active_profile, target_profile],
};
config.state.active_profile = Some(active.into());
config.state.profiles = vec![active.into(), target.into()];
config
}
#[test]
fn divergence_without_default_errors() {
let _home = HomeSandbox::new();
let live = creds("relogin-a", "relogin-r");
let mut config = seed_diverged(
"active",
creds("stored-a", "stored-r"),
&live,
"target",
Some(creds("target-a", "target-r")),
);
let err = switch_profile_noninteractive(&mut config, "target", None);
assert!(
err.is_err(),
"diverged active with no default must error, never prompt"
);
}
#[test]
fn divergence_new_profile_default_errors() {
let _home = HomeSandbox::new();
let live = creds("relogin-a", "relogin-r");
let mut config = seed_diverged(
"active",
creds("stored-a", "stored-r"),
&live,
"target",
Some(creds("target-a", "target-r")),
);
let err =
switch_profile_noninteractive(&mut config, "target", Some(DivergenceChoice::NewProfile));
assert!(
err.is_err(),
"'save as new profile' needs an interactive name prompt; headless must error",
);
}
#[test]
fn divergence_overwrite_captures_relogin_into_outgoing() {
let _home = HomeSandbox::new();
let live = creds("relogin-a", "relogin-r");
let mut config = seed_diverged(
"active",
creds("stored-a", "stored-r"),
&live,
"target",
Some(creds("target-a", "target-r")),
);
let (previous, active) =
switch_profile_noninteractive(&mut config, "target", Some(DivergenceChoice::Overwrite))
.expect("overwrite switch");
assert_eq!(previous.as_deref(), Some("active"));
assert_eq!(active, "target");
let on_disk: ClaudeCredentials =
read_json_file(&profile_dir("active").unwrap().join("credentials.json"))
.expect("read outgoing creds");
assert_eq!(
on_disk.refresh_token(),
Some("relogin-r"),
"overwrite must capture the live re-login into the outgoing profile",
);
let live_now: ClaudeCredentials = read_json_file(
&crate::profile::claude_dir()
.unwrap()
.join(".credentials.json"),
)
.expect("read live creds");
assert_eq!(
live_now.refresh_token(),
Some("target-r"),
"overwrite must end with the active link pointing at target",
);
}
#[test]
fn divergence_discard_drops_relogin_and_relinks_target() {
let _home = HomeSandbox::new();
let live = creds("relogin-a", "relogin-r");
let mut config = seed_diverged(
"active",
creds("stored-a", "stored-r"),
&live,
"target",
Some(creds("target-a", "target-r")),
);
let (previous, active) =
switch_profile_noninteractive(&mut config, "target", Some(DivergenceChoice::Discard))
.expect("discard switch succeeds — it force-relinks past the divergence guard");
assert_eq!(previous.as_deref(), Some("active"));
assert_eq!(active, "target");
let live_now: ClaudeCredentials = read_json_file(
&crate::profile::claude_dir()
.unwrap()
.join(".credentials.json"),
)
.expect("read live creds");
assert_eq!(
live_now.refresh_token(),
Some("target-r"),
"discard relinks the live file to target's stored creds",
);
let outgoing: ClaudeCredentials =
read_json_file(&profile_dir("active").unwrap().join("credentials.json"))
.expect("read outgoing creds");
assert_eq!(
outgoing.refresh_token(),
Some("stored-r"),
"discard must not capture the live login into the outgoing profile",
);
}
#[test]
fn non_diverged_switch_takes_plain_path() {
let _home = HomeSandbox::new();
let active_stored = creds("stored-a", "stored-r");
let active_profile = stored_profile("active", Some(active_stored.clone()));
let target_profile = stored_profile("target", Some(creds("target-a", "target-r")));
crate::claude::force_link_profile_credentials("active").expect("link active");
let mut config = AppConfig {
state: AppState {
active_profile: Some("active".into()),
profiles: vec!["active".into(), "target".into()],
..Default::default()
},
profiles: vec![active_profile, target_profile],
};
assert_eq!(
classify_credentials_link("active").expect("classify"),
LinkState::LinkedTo,
"precondition: active is cleanly linked, not diverged",
);
let (previous, active) =
switch_profile_noninteractive(&mut config, "target", None).expect("plain switch");
assert_eq!(previous.as_deref(), Some("active"));
assert_eq!(active, "target");
}