use super::refresh::{self, Place, Progress, Refreshing, Renewed, Stop};
use super::{Result, journal};
use crate::context::Context;
use crate::error::Error;
use crate::in_use::{self, InUse};
use crate::provider::Owner;
use crate::provider::{self, LoginKind, Provider, ProviderError, ProviderId};
use crate::service::Permit;
use crate::state::{self, Account, Key, State};
use crate::status::{Plan, Seen};
use crate::{audit, fault, store};
use serde_json::Value;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Found {
pub(crate) owner: Owner,
pub(crate) login: String,
pub(crate) known_at: i64,
}
impl Found {
pub(crate) fn recorded(self, named: Option<String>) -> InUse {
InUse {
owner: Some(self.owner),
login: self.login,
known_at: self.known_at,
named,
}
}
}
pub(crate) fn nobody(named: Option<&str>, at: i64) -> Option<InUse> {
named.is_none().then(|| InUse {
owner: None,
login: String::new(),
known_at: at,
named: None,
})
}
pub(crate) fn whose(
ctx: &Context,
state: &State,
tool: &'static dyn Provider,
document: &Value,
) -> std::result::Result<Found, ProviderError> {
if let Some(found) = recorded(state, tool, document) {
return Ok(found);
}
let owner = tool.identify(ctx, &provider::Credential::new(tool.id(), document.clone()))?;
Ok(Found {
owner,
login: tool.fingerprint(document),
known_at: ctx.now(),
})
}
fn recorded(state: &State, tool: &'static dyn Provider, document: &Value) -> Option<Found> {
let login = tool.fingerprint(document);
let known = state.in_use(tool.id())?;
if login.is_empty() || known.login != login {
return None;
}
Some(Found {
owner: known.owner.clone()?,
login,
known_at: known.known_at,
})
}
pub(crate) fn told(
ctx: &Context,
state: &State,
tool: &'static dyn Provider,
document: &Value,
) -> std::result::Result<Option<Found>, ProviderError> {
if tool.identifies_by_itself() {
return whose(ctx, state, tool, document).map(Some);
}
if let Some(found) = recorded(state, tool, document) {
return Ok(Some(found));
}
let lapsed = tool
.expiry(document)
.access_expires_at
.is_some_and(|at| at <= ctx.now());
if lapsed {
return Ok(None);
}
match whose(ctx, state, tool, document) {
Err(ProviderError::Unauthorized) => Ok(None),
asked => asked.map(Some),
}
}
enum Unrenewed {
Refused,
Asked(ProviderError),
Failed(Error),
}
impl Unrenewed {
fn said(self, which: ProviderId) -> Error {
match self {
Unrenewed::Refused => Error::SessionExpired {
tool: which,
refused: true,
},
Unrenewed::Asked(error) => super::unidentified(which, error),
Unrenewed::Failed(error) => error,
}
}
}
fn renewed(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
store: &provider::LiveStore,
document: &Value,
) -> std::result::Result<(Value, Found, bool), Unrenewed> {
if tool.id() == ProviderId::Claude && crate::settings::custom_oauth(ctx) {
return Err(Unrenewed::Failed(Error::CustomOauthEndpoint));
}
fault::point("identify.lapsed");
let refreshing = Refreshing::take(ctx, permit, tool)
.map_err(Unrenewed::Failed)?
.ok_or(Unrenewed::Asked(ProviderError::Unauthorized))?;
let told = renewed_under_the_lock(ctx, permit, state, tool, store, document);
let lost = Refreshing::let_go(Some(refreshing));
told.map(|(document, found, lock_lost)| (document, found, lock_lost || lost))
}
fn renewed_under_the_lock(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
store: &provider::LiveStore,
document: &Value,
) -> std::result::Result<(Value, Found, bool), Unrenewed> {
let which = tool.id();
let failed = Unrenewed::Failed;
let mut last = document.clone();
for _ in 0..2 {
let (seen, now) = super::read_stored(tool, store)
.map_err(failed)?
.ok_or_else(|| failed(super::nothing_signed_in(ctx, tool)))?;
if now != last
&& let Some(found) = told(ctx, state, tool, &now).map_err(Unrenewed::Asked)?
{
return Ok((now, found, false));
}
let login = tool
.slice(&now)
.map_err(|e| failed(super::shape(which, e)))?;
if tool.fingerprint(&login).is_empty() {
return Err(Unrenewed::Refused);
}
let kept = store::holder(&store.chain, &store.service)
.map_err(|e| failed(e.into()))?
.ok_or_else(|| failed(super::nothing_signed_in(ctx, tool)))?;
let place = Place {
store: kept,
service: &store.service,
};
let mut progress = Progress::default();
match refresh::renew(
ctx,
permit,
state,
tool,
place,
&seen,
&login,
&mut progress,
) {
Ok(Renewed::Saved(saved)) => {
let renewed: Value = serde_json::from_str(&saved)
.map_err(|e| failed(store::Error::Malformed(e.to_string()).into()))?;
let owner = tool
.identify(ctx, &provider::Credential::new(which, renewed.clone()))
.map_err(Unrenewed::Asked)?;
let found = Found {
owner,
login: tool.fingerprint(&renewed),
known_at: ctx.now(),
};
return Ok((renewed, found, progress.lock_lost));
}
Ok(Renewed::Refused) => return Err(Unrenewed::Refused),
Err(Stop::Changed) => last = now,
Err(Stop::Unrenewed(error)) => return Err(Unrenewed::Asked(error)),
Err(Stop::Failed(error)) if progress.lost() => {
return Err(failed(Error::RenewalLost {
tool: which,
error: Box::new(error),
}));
}
Err(Stop::Failed(error)) => return Err(failed(error)),
}
}
Err(failed(Error::SignedInAccountChanged))
}
pub(crate) fn told_by_a_read(
ctx: &Context,
permit: Permit,
state: &State,
tool: &'static dyn Provider,
document: &Value,
) -> std::result::Result<(Option<Value>, Found), ProviderError> {
let which = tool.id();
if let Some(found) = told(ctx, state, tool, document)? {
return Ok((None, found));
}
let untold = || ProviderError::Unauthorized;
let _exclusive = super::try_exclusive(ctx, permit).ok_or_else(untold)?;
if journal::pending(ctx) {
return Err(untold());
}
let mut state = state::load(ctx).map_err(|_| untold())?;
refresh::finished(ctx, permit, &mut state, which).map_err(|_| untold())?;
let store = super::live_store(ctx, tool).map_err(|_| untold())?;
match renewed(ctx, permit, &mut state, tool, &store, document) {
Ok((renewed, found, _)) => Ok((Some(renewed), found)),
Err(Unrenewed::Refused | Unrenewed::Failed(Error::RenewalLost { .. })) => {
Err(ProviderError::InvalidGrant {
service: which.service(),
})
}
Err(Unrenewed::Asked(error)) => Err(error),
Err(Unrenewed::Failed(_)) => Err(untold()),
}
}
pub(crate) enum Live {
Nothing,
Login(Login),
}
pub(crate) struct Login {
pub(crate) store: provider::LiveStore,
pub(crate) document: Value,
pub(crate) owner: Owner,
pub(crate) lock_lost: bool,
}
impl Login {
pub(crate) fn lock_warning(&self, which: ProviderId) -> Option<crate::service::Warning> {
self.lock_lost
.then_some(crate::service::Warning::LockCompromised { tool: which })
}
}
pub(crate) fn look(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
) -> Result<(Live, InUse)> {
let which = tool.id();
refresh::finished(ctx, permit, state, which)?;
let store = super::live_store(ctx, tool)?;
let named = in_use::named(ctx, which);
match super::read_stored(tool, &store)? {
None => {
let nobody = nobody(named.as_deref(), ctx.now())
.ok_or_else(|| super::nothing_signed_in(ctx, tool))?;
Ok((Live::Nothing, nobody))
}
Some((_, document)) => {
let (document, found, lock_lost) = match told(ctx, state, tool, &document) {
Ok(Some(found)) => (document, found, false),
Ok(None) => renewed(ctx, permit, state, tool, &store, &document)
.map_err(|untold| untold.said(which))?,
Err(error) => return Err(super::unidentified(which, error)),
};
let owner = found.owner.clone();
let live = Live::Login(Login {
store,
document,
owner,
lock_lost,
});
Ok((live, found.recorded(named)))
}
}
}
pub(crate) fn fresh(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
ahead: i64,
) -> Result<(Live, InUse)> {
let (live, found) = look(ctx, permit, state, tool)?;
let Live::Login(login) = live else {
return Ok((live, found));
};
let lapses = tool
.expiry(&login.document)
.access_expires_at
.is_some_and(|at| at <= ctx.now() + ahead);
if !lapses {
return Ok((Live::Login(login), found));
}
let (document, renewed, lock_lost) =
renewed(ctx, permit, state, tool, &login.store, &login.document)
.map_err(|untold| untold.said(tool.id()))?;
let owner = renewed.owner.clone();
let recorded = renewed.recorded(found.named);
let live = Live::Login(Login {
store: login.store,
document,
owner,
lock_lost: login.lock_lost || lock_lost,
});
Ok((live, recorded))
}
pub(crate) fn now(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
) -> Result<Live> {
let (live, found) = look(ctx, permit, state, tool)?;
keep(ctx, permit, state, tool, found)?;
Ok(live)
}
pub(crate) fn keep(
ctx: &Context,
permit: Permit,
state: &mut State,
tool: &'static dyn Provider,
found: InUse,
) -> Result<()> {
let (changed, noticed) = record(state, tool.id(), found, ctx.now());
if changed {
state::save(ctx, permit, state)?;
}
write_down(ctx, permit, ¬iced);
Ok(())
}
pub(crate) fn untold(error: &Error) -> String {
match error {
Error::SessionExpired { refused: true, .. } => {
"its login has expired and cannot be renewed".into()
}
Error::SessionExpired { tool, .. } => {
format!("{} refused its access token", tool.service())
}
Error::IdentityUnverifiable { detail, .. }
| Error::LiveCredentialShapeUnexpected { detail, .. } => detail.clone(),
Error::LiveStoreUnsupported { reason, .. } => reason.clone(),
Error::LiveCredentialElsewhere { email } => {
format!("its config names {email}, and Pitboard cannot find that login")
}
Error::Store(e) => e.to_string(),
Error::RenewalLost { error, .. } => untold(error),
other => other.code().replace('_', " "),
}
}
pub(crate) fn record_read(
ctx: &Context,
permit: Permit,
found: &[(ProviderId, InUse)],
plans: &BTreeMap<String, Plan>,
) {
let news = |state: &State| -> Vec<(ProviderId, InUse)> {
found
.iter()
.filter(|(which, found)| {
!state
.in_use(*which)
.is_some_and(|known| known.agrees(*which, found))
})
.cloned()
.collect()
};
let replanned = |state: &State| {
state
.accounts
.iter()
.any(|a| plans.get(&a.id).is_some_and(|plan| plan.said != a.plan))
};
let Ok(state) = state::load(ctx) else {
return;
};
if news(&state).is_empty() && !replanned(&state) {
return;
}
let Some(_exclusive) = super::try_exclusive(ctx, permit) else {
return;
};
if journal::pending(ctx) {
return;
}
let Ok(mut state) = state::load(ctx) else {
return;
};
fault::point("identify.recording");
let mut changed = false;
let mut noticed = Vec::new();
let mut held = BTreeMap::new();
let mut stored = |which: ProviderId| {
held.entry(which)
.or_insert_with(|| match provider::of(which) {
LoginKind::Document(tool) => holding(ctx, tool),
LoginKind::Folder(_) => None,
})
.clone()
};
for (which, found) in news(&state) {
let expected = found.owner.as_ref().map(|_| found.login.clone());
if in_use::named(ctx, which) != found.named || stored(which) != Some(expected) {
continue;
}
let (recorded, said) = record(&mut state, which, found, ctx.now());
changed |= recorded;
noticed.extend(said);
}
for account in &mut state.accounts {
let Some(plan) = plans
.get(&account.id)
.filter(|plan| plan.said != account.plan)
else {
continue;
};
let still = match &plan.seen {
Seen::Parked(service) => account
.parked
.as_ref()
.is_some_and(|p| p.service == *service),
Seen::Live(login) => stored(account.provider()).flatten().as_ref() == Some(login),
};
if still {
account.plan.clone_from(&plan.said);
changed = true;
}
}
if changed && state::save(ctx, permit, &state).is_ok() {
write_down(ctx, permit, ¬iced);
}
}
fn holding(ctx: &Context, tool: &'static dyn Provider) -> Option<Option<String>> {
let live = super::live_store(ctx, tool).ok()?;
let stored = super::read_stored(tool, &live).ok()?;
Some(stored.map(|(_, document)| tool.fingerprint(&document)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Noticed {
SignedIn(Option<Key>),
SignedOut,
Replaced(Key),
}
pub(crate) fn record(
state: &mut State,
which: ProviderId,
found: InUse,
at: i64,
) -> (bool, Vec<Noticed>) {
let before = state.in_use(which).map(|known| known.account(which));
let now = found.account(which);
let now_enrolled = found
.owner
.as_ref()
.and_then(|owner| state.account_of(which, owner))
.map(Account::key);
let identified = state.identified(which, found, at);
let mut noticed = Vec::new();
if before.is_some_and(|before| before != now) {
noticed.push(match now {
Some(_) => Noticed::SignedIn(now_enrolled),
None => Noticed::SignedOut,
});
}
noticed.extend(
identified
.replaced
.map(|replaced| Noticed::Replaced(replaced.key)),
);
(identified.changed, noticed)
}
pub(crate) fn write_down(ctx: &Context, permit: Permit, noticed: &[Noticed]) {
for noticed in noticed {
let (subject, outcome) = match noticed {
Noticed::SignedIn(now) => (
now.as_ref().map(Key::typed).unwrap_or_default(),
"signed_in_outside",
),
Noticed::SignedOut => (String::new(), "signed_out_outside"),
Noticed::Replaced(key) => (key.typed(), "login_replaced"),
};
audit::record(ctx, permit, "in-use", &subject, outcome);
}
}
#[cfg(test)]
mod tests {
use super::super::Settled;
use super::super::harness::{
Machine, NOW, Session, audit_lines, codex_id, codex_login, codex_machine, config_names,
document, hold, lapsed, lets_go_of_the_write_lock, lock_dir, login_of, machine, oauth,
owner, record_in_use, recover, renews, renews_meanwhile, saves, signed_in_outside,
state_file, takes_the_write_lock, window, write_target,
};
use super::*;
use crate::api::scripted::{Asked, Trouble};
use crate::error::Cause;
use crate::service::Pitboard;
use crate::store::memory::Fault;
use std::cell::Cell;
use std::rc::Rc;
use std::sync::Arc;
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn an_answer_about_a_login_replaced_meanwhile_is_not_recorded() {
let m = signed_in_outside("identify-meanwhile");
m.api.owned_by("access-third-refresh", owner("third"));
let third = document("third-refresh");
let before = state_file(&m);
fault::meanwhile(
"identify.recording",
{
let (store, service) = m.live_store();
let third = third.to_string();
move || store.plant(&service, &third)
},
|| Pitboard::new(m.ctx.clone()).status(false),
)
.expect("a read");
assert_eq!(
m.live(),
Some(third),
"the login moved while it was asked about"
);
assert_eq!(state_file(&m), before);
assert!(audit_lines(&m, "in-use").is_empty());
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn what_a_read_saw_the_config_name_before_it_was_written_is_not_recorded() {
let m = machine("identify-config-written-meanwhile");
config_names(&m, "there");
let mut state = state::load(&m.ctx).expect("state");
let (_, found) = look(
&m.ctx,
Permit::for_a_test(),
&mut state,
provider::document(ProviderId::Claude),
)
.expect("a look");
let pitboard = Pitboard::new(m.ctx.clone());
pitboard.switch_to("here").expect("already in use");
let before = state_file(&m);
record_read(
&m.ctx,
Permit::for_a_test(),
&[(ProviderId::Claude, found)],
&BTreeMap::new(),
);
assert_eq!(state_file(&m), before);
let read = pitboard.status_offline().expect("a read of what is known");
let codes: Vec<&str> = read
.warnings
.iter()
.map(crate::service::Warning::code)
.collect();
assert!(codes.is_empty(), "{codes:?}");
}
fn recorded_plan(m: &Machine, label: &str) -> Option<String> {
state::load(&m.ctx)
.expect("state")
.get(&m.key(label))
.and_then(|account| account.plan.clone())
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_read_records_the_plan_a_login_says() {
let m = machine("identify-plan");
let there = state::load(&m.ctx)
.expect("state")
.get(&m.key("there"))
.and_then(|a| a.parked.clone())
.expect("parked");
let mut parked = oauth("there-refresh", 30);
parked["subscriptionType"] = "max".into();
parked["rateLimitTier"] = "default_claude_max_20x".into();
m.mem.vault().plant(&there.service, &parked.to_string());
Pitboard::new(m.ctx.clone()).status(false).expect("a read");
assert_eq!(recorded_plan(&m, "there").as_deref(), Some("Max 20x"));
assert_eq!(recorded_plan(&m, "here"), None, "its login says none");
let m = codex_machine("identify-plan");
let mut state = state::load(&m.ctx).expect("state");
for account in &mut state.accounts {
account.plan = Some("Plus".into());
}
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
Pitboard::new(m.ctx.clone()).status(false).expect("a read");
assert_eq!(recorded_plan(&m, "here").as_deref(), Some("Pro"));
assert_eq!(recorded_plan(&m, "there").as_deref(), Some("Pro"));
}
fn enrolled_again(m: &Machine, label: &str, login: Value) {
let settled = super::super::settle(&m.ctx, Permit::for_a_test(), Some(m.which))
.expect("nothing to recover")
.0;
let signed = super::super::enroll::planted(
&m.ctx,
Permit::for_a_test(),
provider::document(m.which),
login,
)
.expect("a sign-in");
super::super::enroll::enroll(settled, &m.key(label), Some(signed)).expect("enrolled");
}
fn on_max(m: &Machine, who: &str, refresh: &str) -> Value {
let mut login = login_of(m, who, refresh);
login["claudeAiOauth"]["subscriptionType"] = "max".into();
login["claudeAiOauth"]["rateLimitTier"] = "default_claude_max_20x".into();
login
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_plan_read_off_a_login_replaced_meanwhile_is_not_recorded() {
let m = machine("identify-plan-parked-meanwhile");
let there = state::load(&m.ctx)
.expect("state")
.get(&m.key("there"))
.and_then(|a| a.parked.clone())
.expect("parked");
let mut parked = oauth("there-refresh", 30);
parked["subscriptionType"] = "pro".into();
m.mem.vault().plant(&there.service, &parked.to_string());
let read = state::load(&m.ctx).expect("state");
let answers = crate::status::ask(&m.ctx, Permit::for_a_test(), &read, true);
enrolled_again(&m, "there", on_max(&m, "there", "there-new"));
record_read(&m.ctx, Permit::for_a_test(), &answers.found, &answers.plans);
assert_eq!(recorded_plan(&m, "there").as_deref(), Some("Max 20x"));
let m = machine("identify-plan-live-meanwhile");
let mut live = document("here-refresh");
live["claudeAiOauth"]["subscriptionType"] = "pro".into();
m.sign_in(&live);
let read = state::load(&m.ctx).expect("state");
let answers = crate::status::ask(&m.ctx, Permit::for_a_test(), &read, true);
enrolled_again(&m, "here", on_max(&m, "here", "here-new"));
record_read(&m.ctx, Permit::for_a_test(), &answers.found, &answers.plans);
assert_eq!(recorded_plan(&m, "here").as_deref(), Some("Max 20x"));
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_read_whose_plans_agree_with_the_record_writes_nothing() {
let m = codex_machine("identify-plans-agree");
let pitboard = Pitboard::new(m.ctx.clone());
pitboard.status(false).expect("a read");
let before = state_file(&m);
pitboard.status(false).expect("a read");
assert_eq!(state_file(&m), before);
let pro = Plan {
said: Some("Pro".into()),
seen: Seen::Live(String::new()),
};
let gone = BTreeMap::from([("gone".to_string(), pro)]);
record_read(&m.ctx, Permit::for_a_test(), &[], &gone);
assert_eq!(state_file(&m), before);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_read_while_a_switch_is_interrupted_records_nothing() {
let m = machine("identify-interrupted");
m.api
.owned_by("access-elsewhere-refresh", owner("elsewhere"));
let settled = super::super::settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0;
let died = fault::killing("switch.park_recorded", || {
super::super::switch(settled, &m.key("there"))
});
assert_eq!(died.unwrap_err(), "switch.park_recorded");
m.sign_in(&document("elsewhere-refresh"));
let before = state_file(&m);
Pitboard::new(m.ctx.clone()).status(false).expect("a read");
assert!(super::super::interrupted(&m.ctx), "the switch still waits");
assert_eq!(state_file(&m), before);
assert!(audit_lines(&m, "in-use").is_empty());
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_login_with_no_fingerprint_is_always_asked_about() {
let m = machine("identify-no-fingerprint");
let mut state = state::load(&m.ctx).expect("state");
let brought_forward = InUse::of(state.get(&m.key("here")).expect("enrolled"), "", 0);
state
.in_use
.insert(ProviderId::Claude.code().into(), brought_forward);
let no_refresh =
serde_json::json!({"claudeAiOauth": {"accessToken": "access-here-refresh"}});
let found = whose(
&m.ctx,
&state,
provider::document(ProviderId::Claude),
&no_refresh,
)
.expect("named");
assert_eq!(found.owner, owner("here"));
assert_eq!(m.api.asked(), [Asked::Owner("access-here-refresh".into())]);
}
fn lapsed_stored(name: &str) -> Machine {
let m = machine(name);
m.sign_in(&lapsed("here-renewed"));
renews(&m, "here-renewed", "here-again");
m.api.owned_by("access-here-again", owner("here"));
m
}
fn told_now(m: &Machine) -> Result<Live> {
told_in(&m.ctx)
}
fn told_in(ctx: &Context) -> Result<Live> {
let Settled {
_exclusive,
mut state,
ctx,
permit,
} = super::super::settle(ctx, Permit::for_a_test(), None)?.0;
now(
&ctx,
permit,
&mut state,
provider::document(ProviderId::Claude),
)
}
fn killed_once_answered(m: &Machine) {
let died = fault::killing("refresh.exchanged", || told_now(m).map(|_| ()));
assert_eq!(died.err().as_deref(), Some("refresh.exchanged"));
m.api.renew_trouble("here-renewed", Trouble::InvalidGrant);
}
fn copies_of(m: &Machine, refresh: &str) -> Vec<String> {
let held = format!("\"refreshToken\":\"{refresh}\"");
m.mem
.vault()
.services()
.into_iter()
.filter(|service| {
m.mem
.vault()
.peek(service)
.is_some_and(|login| login.contains(&held))
})
.collect()
}
fn jams_the_write_lock(m: &Machine) -> impl FnOnce() + 'static {
let stuck = lock_dir(&write_target(m));
move || {
let parent = stuck.parent().expect("the storage directory");
std::fs::create_dir_all(parent).expect("the storage directory is made");
let long_ago = std::time::SystemTime::now() - std::time::Duration::from_secs(3600);
std::fs::File::create(&stuck)
.and_then(|lock| lock.set_modified(long_ago))
.expect("something in the lock's place");
}
}
fn locks_the_keychain(m: &Machine) -> impl FnOnce() + Send + 'static {
let mem = Arc::clone(&m.mem);
move || mem.lock_keychain()
}
fn owner_told(told: Result<Live>) -> Owner {
match told.expect("told") {
Live::Login(login) => login.owner,
Live::Nothing => panic!("a login is stored"),
}
}
fn stored_refresh(m: &Machine) -> Option<String> {
m.live()?
.pointer("/claudeAiOauth/refreshToken")?
.as_str()
.map(str::to_owned)
}
fn renewals(m: &Machine) -> Vec<Asked> {
m.api
.asked()
.into_iter()
.filter(|asked| matches!(asked, Asked::Renew(_)))
.collect()
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_lapsed_login_is_renewed_under_the_refresh_lock_and_named() {
let m = lapsed_stored("identify-renews");
let refreshing = lock_dir(&provider::claude::paths::refresh_lock(&m.ctx));
let held = Rc::new(Cell::new(false));
let looked = {
let (held, refreshing) = (Rc::clone(&held), refreshing.clone());
move || held.set(refreshing.is_dir())
};
let told = fault::meanwhile("refresh.exchanged", looked, || told_now(&m));
assert_eq!(owner_told(told), owner("here"));
assert!(held.get(), "renewed under Claude Code's refresh lock");
assert!(!refreshing.exists(), "and let go of it");
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
assert_eq!(
m.live().expect("stored")["mcpOAuth"],
document("x")["mcpOAuth"]
);
assert_eq!(
m.api.asked(),
[
Asked::Renew("here-renewed".into()),
Asked::Owner("access-here-again".into())
]
);
let state = state::load(&m.ctx).expect("state");
let recorded = state.in_use(ProviderId::Claude).expect("recorded");
assert_eq!(recorded.owner, Some(owner("here")));
assert_eq!(recorded.login, store::fingerprint("here-again"));
assert_eq!(state.renewing, []);
assert!(crate::pending::outstanding(&m.ctx, Some(&state)).is_empty());
hold(&m, "after a renewal");
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_login_a_session_renewed_meanwhile_is_told_and_nothing_is_sent() {
let m = lapsed_stored("identify-renewed-meanwhile");
m.api
.owned_by("access-here-renewed-by-a-session", owner("here"));
let session = Session::new();
let keychain = Arc::clone(m.mem.live());
let told = fault::meanwhile(
"identify.lapsed",
renews_meanwhile(&m, Arc::clone(&keychain), &session),
|| told_now(&m),
);
saves(&m, &keychain, &session);
assert_eq!(owner_told(told), owner("here"));
assert!(!session.waited.get());
assert_eq!(renewals(&m), []);
assert_eq!(
stored_refresh(&m).as_deref(),
Some("here-renewed-by-a-session")
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_lapsed_login_anthropic_refuses_says_sign_in_again_and_writes_nothing() {
let m = machine("identify-refused");
m.sign_in(&lapsed("here-renewed"));
m.api.renew_trouble("here-renewed", Trouble::InvalidGrant);
let (before, stored, vault) = (state_file(&m), m.live(), m.mem.vault().services());
let refused = told_now(&m).err().expect("refused");
assert_eq!(refused.code(), "session_expired");
assert_eq!(refused.cause(), Some(Cause::LoginRefused));
assert!(
refused
.to_string()
.ends_with("Run `claude`, sign in, then try again."),
"{refused}"
);
assert_eq!(state_file(&m).0, before.0);
assert_eq!(m.live(), stored);
assert_eq!(m.mem.vault().services(), vault);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_login_the_keychain_could_not_take_back_is_not_renewed() {
let mut m = lapsed_stored("identify-too-large");
m.ctx = m.ctx.clone().with_argv_fallback(false);
m.mem.live().takes_on_stdin(16);
let stored = m.live();
let refused = told_now(&m).err().expect("refused");
assert_eq!(refused.code(), "credential_too_large");
assert_eq!(renewals(&m), []);
assert_eq!(m.live(), stored);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_write_lock_held_once_the_refresh_token_is_spent_is_waited_for() {
let m = lapsed_stored("identify-write-lock-after");
let told = fault::meanwhile("refresh.waited", lets_go_of_the_write_lock(&m), || {
fault::meanwhile("refresh.exchanged", takes_the_write_lock(&m), || {
told_now(&m)
})
});
let Live::Login(login) = told.expect("told") else {
panic!("a login is stored");
};
assert_eq!(login.owner, owner("here"));
assert!(!login.lock_lost, "saved under the lock");
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn killed_once_anthropic_answered_the_renewed_login_is_saved_or_kept() {
for (point, next) in [
("refresh.exchanged", "change"),
("refresh.exchanged", "read"),
("refresh.saved", "change"),
] {
let m = lapsed_stored(&format!("identify-killed-{point}-{next}"));
let died = fault::killing(point, || told_now(&m).map(|_| ()));
assert_eq!(died.err().as_deref(), Some(point));
m.api.renew_trouble("here-renewed", Trouble::InvalidGrant);
let kept = m.mem.vault().services().into_iter().any(|service| {
m.mem
.vault()
.peek(&service)
.is_some_and(|held| held.contains("\"refreshToken\":\"here-again\""))
});
assert!(
kept || stored_refresh(&m).as_deref() == Some("here-again"),
"{point}: the renewed login is nowhere"
);
if next == "change" {
recover(&m).expect("the next change");
} else {
Pitboard::new(m.ctx.clone()).status(false).expect("a read");
}
assert_eq!(
stored_refresh(&m).as_deref(),
Some("here-again"),
"{point}, {next}"
);
assert!(
state::load(&m.ctx).expect("state").renewing.is_empty(),
"{point}, {next}"
);
recover(&m).expect("the next change");
hold(&m, point);
assert_eq!(owner_told(told_now(&m)), owner("here"), "{point}");
assert_eq!(
renewals(&m),
[Asked::Renew("here-renewed".into())],
"{point}, {next}"
);
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_renewed_login_that_cannot_be_saved_yet_is_kept_for_the_next_change() {
for (fault, code) in [
(Fault::Locked, "credential_store_locked"),
(
Fault::FailWrite("disk full".into()),
"credential_write_failed",
),
] {
let m = lapsed_stored(&format!("identify-unsaved-{code}"));
let (keychain, service) = m.live_store();
let faulted = {
let (keychain, service) = (Arc::clone(&keychain), service.clone());
let api = Arc::clone(&m.api);
move || {
api.renew_trouble("here-renewed", Trouble::InvalidGrant);
keychain.fault(&service, fault);
}
};
let refused = fault::meanwhile("refresh.exchanged", faulted, || told_now(&m))
.err()
.expect("not saved");
assert_eq!(refused.code(), code);
assert_eq!(copies_of(&m, "here-again").len(), 1, "{code}");
assert!(
keychain
.peek(&service)
.is_some_and(|held| held.contains("\"refreshToken\":\"here-renewed\"")),
"{code}"
);
keychain.heal(&service);
assert_eq!(owner_told(told_now(&m)), owner("here"), "{code}");
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"), "{code}");
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
assert_eq!(copies_of(&m, "here-again"), Vec::<String>::new());
hold(&m, code);
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_renewal_left_unsaved_is_saved_before_anything_is_sent() {
let m = lapsed_stored("identify-left-unsaved");
killed_once_answered(&m);
jams_the_write_lock(&m)();
let stuck = lock_dir(&write_target(&m));
let lifted = Rc::new(Cell::new(false));
let lift = {
let (stuck, lifted) = (stuck.clone(), Rc::clone(&lifted));
move || {
std::fs::remove_file(&stuck).expect("the lock is let go of");
lifted.set(true);
}
};
let refused = fault::meanwhile("identify.lapsed", lift, || told_now(&m))
.err()
.expect("the copy cannot be saved");
assert_eq!(refused.code(), "switch_in_progress");
assert!(!lifted.get(), "stopped before the renewal");
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
assert_eq!(copies_of(&m, "here-again").len(), 1);
std::fs::remove_file(&stuck).expect("the lock is let go of");
assert_eq!(owner_told(told_now(&m)), owner("here"));
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_renewal_left_for_another_slot_is_kept_for_it() {
let m = lapsed_stored("identify-two-slots");
killed_once_answered(&m);
let [left] = copies_of(&m, "here-again")
.try_into()
.expect("one copy left");
let dir = m.ctx_home().join("other");
std::fs::create_dir_all(&dir).expect("the other config directory");
let other = m
.ctx
.clone()
.with_claude_config_dir(dir.display().to_string());
m.mem.live().plant(
&provider::claude::paths::live_service(&other),
&lapsed("other-renewed").to_string(),
);
renews(&m, "other-renewed", "other-again");
m.api.owned_by("access-other-again", owner("there"));
assert_eq!(owner_told(told_in(&other)), owner("there"));
assert!(state::load(&m.ctx).expect("state").names(&left));
assert!(m.mem.vault().peek(&left).is_some());
assert_eq!(owner_told(told_now(&m)), owner("here"));
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
assert_eq!(
renewals(&m),
[
Asked::Renew("here-renewed".into()),
Asked::Renew("other-renewed".into())
]
);
hold(&m, "both slots saved");
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_parks_nothing_while_a_renewal_of_the_login_stored_waits() {
let m = lapsed_stored("identify-switch-waits");
killed_once_answered(&m);
let [copy] = copies_of(&m, "here-again")
.try_into()
.expect("one copy left");
let mut state = state::load(&m.ctx).expect("state");
record_in_use(
&m.ctx,
&mut state,
ProviderId::Claude,
&lapsed("here-renewed"),
);
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
m.mem
.vault()
.fault(©, Fault::Unreadable("damaged".into()));
let parked = |m: &Machine| {
state::load(&m.ctx)
.expect("state")
.get(&m.key("here"))
.and_then(|account| account.parked.clone())
.map(|parked| parked.login)
};
let pitboard = Pitboard::new(m.ctx.clone());
let refused = pitboard
.switch_to("there")
.expect_err("a renewal of the login stored waits")
.error;
assert_eq!(refused.code(), "credential_store_unreadable");
assert_eq!(parked(&m), None);
assert_eq!(stored_refresh(&m).as_deref(), Some("here-renewed"));
m.mem.vault().heal(©);
pitboard.switch_to("there").expect("switched");
assert_eq!(parked(&m), Some(store::fingerprint("here-again")));
assert_eq!(m.mem.vault().peek(©), None);
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
hold(&m, "after a switch from a renewal saved late");
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_keychain_locked_before_the_refresh_token_is_sent_stops_the_renewal() {
let m = lapsed_stored("identify-locked-before-sending");
let before = state_file(&m);
let refused = fault::meanwhile("refresh.checked", locks_the_keychain(&m), || told_now(&m))
.err()
.expect("the keychain is locked");
assert_eq!(refused.code(), "credential_store_locked");
assert_eq!(renewals(&m), []);
assert_eq!(state_file(&m), before);
m.mem.unlock_keychain();
assert_eq!(owner_told(told_now(&m)), owner("here"));
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_keychain_locked_while_anthropic_answers_says_the_renewal_is_lost() {
const LOST: &str = "Pitboard renewed Claude Code's login and could not save the renewed \
login, so the refresh token stored is spent. Run `claude`, sign in, \
then try again.";
let m = lapsed_stored("identify-locked-while-answering");
m.api.while_renewing("here-renewed", locks_the_keychain(&m));
let refused = told_now(&m).err().expect("the keychain is locked");
assert_eq!(refused.code(), "credential_store_locked");
assert!(refused.to_string().ends_with(LOST), "{refused}");
assert_eq!(state::load(&m.ctx).expect("state").renewing, []);
assert_eq!(copies_of(&m, "here-again"), Vec::<String>::new());
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_copy_written_and_not_read_back_is_kept_for_the_next_change() {
let m = lapsed_stored("identify-copy-not-read-back");
m.mem.vault().fault_all(Fault::LocksAfterWrite);
let (keychain, service) = m.live_store();
let locks = {
let (keychain, service) = (Arc::clone(&keychain), service.clone());
move || keychain.fault(&service, Fault::Locked)
};
let refused = fault::meanwhile("refresh.exchanged", locks, || told_now(&m))
.err()
.expect("the keychain is locked");
assert_eq!(refused.code(), "credential_store_locked");
assert_eq!(copies_of(&m, "here-again").len(), 1);
m.mem.vault().heal_all();
keychain.heal(&service);
m.api.renew_trouble("here-renewed", Trouble::InvalidGrant);
assert_eq!(owner_told(told_now(&m)), owner("here"));
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
assert_eq!(copies_of(&m, "here-again"), Vec::<String>::new());
hold(&m, "after a copy saved late");
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_renewal_saved_without_the_write_lock_is_said() {
let m = lapsed_stored("identify-lock-lost");
let done = fault::meanwhile("refresh.exchanged", jams_the_write_lock(&m), || {
Pitboard::new(m.ctx.clone()).update_config("here")
})
.expect("in use");
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
let codes: Vec<&str> = done
.warnings
.iter()
.map(crate::service::Warning::code)
.collect();
assert_eq!(codes, ["lock_compromised"]);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_read_renews_a_lapsed_login_to_tell_whose_it_is() {
let m = lapsed_stored("identify-read-renews");
m.api.using(
"access-here-again",
crate::usage::Snapshot {
windows: vec![window("session", 40.0)],
observed_at: Some(NOW),
answered_at: Some(NOW),
lists_every_limit: true,
source: crate::usage::Source::Live,
},
);
let read = Pitboard::new(m.ctx.clone()).status(false).expect("a read");
let here = read
.value
.rows
.iter()
.find(|row| row.signed_in)
.expect("an account in use");
assert_eq!(here.label.as_deref(), Some("here"));
assert_eq!(here.stale, None);
assert_eq!(
here.usage.as_ref().map(|usage| usage.windows[0].percent),
Some(40.0)
);
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
assert_eq!(stored_refresh(&m).as_deref(), Some("here-again"));
let state = state::load(&m.ctx).expect("state");
assert_eq!(
state.in_use(ProviderId::Claude).map(|r| r.login.clone()),
Some(store::fingerprint("here-again"))
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_read_never_renews_a_lapsed_login_it_knows_for_its_usage() {
let m = machine("identify-read-known-lapsed");
m.sign_in(&lapsed("here-refresh"));
renews(&m, "here-refresh", "here-again");
let read = Pitboard::new(m.ctx.clone()).status(false).expect("a read");
let here = read
.value
.rows
.iter()
.find(|row| row.signed_in)
.expect("an account in use");
assert_eq!(here.label.as_deref(), Some("here"));
assert_eq!(here.stale, Some(crate::status::Stale::SessionExpired));
assert_eq!(renewals(&m), []);
assert_eq!(stored_refresh(&m).as_deref(), Some("here-refresh"));
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_read_says_sign_in_again_where_the_login_is_refused_for_good() {
let m = machine("identify-read-refused");
m.sign_in(&lapsed("here-renewed"));
m.api.renew_trouble("here-renewed", Trouble::InvalidGrant);
let stored = m.live();
let read = Pitboard::new(m.ctx.clone()).status(false).expect("a read");
let here = read
.value
.rows
.iter()
.find(|row| row.signed_in)
.expect("the account last named");
assert_eq!(here.stale, Some(crate::status::Stale::LoginRefused));
assert_eq!(
here.explanation(),
Some("Claude Code's login is no longer accepted; run `claude` and sign in again")
);
assert_eq!(m.live(), stored);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_read_that_loses_the_renewal_says_sign_in_again() {
let m = lapsed_stored("identify-read-lost");
m.mem.vault().takes_on_stdin(16);
let (keychain, service) = m.live_store();
m.api.while_renewing("here-renewed", move || {
keychain.fault(&service, Fault::Locked);
});
let read = Pitboard::new(m.ctx.clone()).status(false).expect("a read");
let here = read
.value
.rows
.iter()
.find(|row| row.signed_in)
.expect("the account last named");
assert_eq!(here.stale, Some(crate::status::Stale::LoginRefused));
assert_eq!(
here.explanation(),
Some("Claude Code's login is no longer accepted; run `claude` and sign in again")
);
assert_eq!(renewals(&m), [Asked::Renew("here-renewed".into())]);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_codex_login_is_told_by_its_claims_and_never_renewed() {
let m = codex_machine("identify-codex-lapsed");
let mut login = codex_login("here", "here-again");
login["tokens"]["access_token"] = serde_json::json!(provider::jwt::unsigned(
&serde_json::json!({"exp": NOW - 60, "for": "here-again"})
));
m.sign_in(&login);
let Settled {
_exclusive,
mut state,
ctx,
permit,
} = super::super::settle(&m.ctx, Permit::for_a_test(), None)
.expect("settled")
.0;
let Live::Login(told) = now(
&ctx,
permit,
&mut state,
provider::document(ProviderId::Codex),
)
.expect("told") else {
panic!("a login is stored");
};
assert_eq!(told.owner.account_uuid, codex_id("here"));
assert_eq!(m.api.asked(), []);
}
}