use crate::api::{self, ApiError, Owner};
use crate::context::Context;
use crate::error::Cause;
use crate::in_use::{self, InUse};
use crate::provider::claude::paths as claude;
use crate::provider::{ProviderError, ProviderId};
use crate::state::{Key, Park, State};
use crate::switch::identify;
use crate::usage::{Snapshot, Source};
use crate::{budget, park, readings};
use serde_json::Value;
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Stale {
NothingSignedIn,
LoginUnreadable,
LoginUnusable,
SessionExpired,
ParkedAccessExpired,
NothingParked,
LoginReplaced,
ParkUnreadable,
RateLimited,
Unreachable,
ServerError,
AnswerNotUnderstood,
LoginRefused,
AskedRecently,
Interrupted,
NotAsked,
}
impl Stale {
fn of(error: &ApiError, signed_in: bool) -> Stale {
match Cause::of(error) {
Cause::TokenExpired if signed_in => Stale::SessionExpired,
Cause::TokenExpired => Stale::ParkedAccessExpired,
Cause::RateLimited => Stale::RateLimited,
Cause::Unreachable => Stale::Unreachable,
Cause::ServerError => Stale::ServerError,
Cause::AnswerNotUnderstood => Stale::AnswerNotUnderstood,
Cause::LoginRefused => Stale::LoginRefused,
}
}
pub fn code(self) -> &'static str {
match self {
Stale::NothingSignedIn => "nothing_signed_in",
Stale::LoginUnreadable => "login_unreadable",
Stale::LoginUnusable => "login_unusable",
Stale::SessionExpired => "session_expired",
Stale::ParkedAccessExpired => "parked_access_expired",
Stale::NothingParked => "nothing_parked",
Stale::LoginReplaced => "login_replaced",
Stale::ParkUnreadable => "park_unreadable",
Stale::RateLimited => "rate_limited",
Stale::Unreachable => "unreachable",
Stale::ServerError => "server_error",
Stale::AnswerNotUnderstood => "answer_not_understood",
Stale::LoginRefused => "login_refused",
Stale::AskedRecently => "asked_recently",
Stale::Interrupted => "interrupted",
Stale::NotAsked => "not_asked",
}
}
pub fn explanation(self) -> Option<&'static str> {
self.explanation_for(crate::label::DEFAULT)
}
pub fn explanation_for(self, provider: ProviderId) -> Option<&'static str> {
let per_tool = |claude: &'static str, codex: &'static str| match provider {
ProviderId::Claude => claude,
ProviderId::Codex => codex,
};
match self {
Stale::NothingSignedIn => Some("nothing is signed in"),
Stale::LoginUnreadable => Some(per_tool(
"Claude Code's login could not be read; run `pitboard doctor`",
"Codex's login could not be read; run `pitboard doctor`",
)),
Stale::LoginUnusable => Some(per_tool(
"Claude Code's login is not one Pitboard can park or switch; run `pitboard doctor`",
"Codex's login is not one Pitboard can park or switch; run `pitboard doctor`",
)),
Stale::SessionExpired => Some(per_tool(
"Claude Code's session has expired; `claude` renews it",
"Codex's session has expired; `codex` renews it",
)),
Stale::ParkedAccessExpired | Stale::NothingParked => None,
Stale::LoginReplaced => {
Some("its login was replaced by a sign-in outside Pitboard; sign in again")
}
Stale::ParkUnreadable => Some("its parked login cannot be read; run `pitboard doctor`"),
Stale::RateLimited => Some(per_tool(
"Anthropic is rate limiting usage checks",
"OpenAI is rate limiting usage checks",
)),
Stale::Unreachable => Some(per_tool(
"Anthropic could not be reached",
"OpenAI could not be reached",
)),
Stale::ServerError => Some(per_tool(
"Anthropic answered with an error; try again later",
"OpenAI answered with an error; try again later",
)),
Stale::AnswerNotUnderstood => Some(per_tool(
"Anthropic's answer was not understood",
"OpenAI's answer was not understood",
)),
Stale::LoginRefused => Some("its parked login is no longer accepted; sign in again"),
Stale::AskedRecently => None,
Stale::Interrupted => Some("the check did not finish"),
Stale::NotAsked => Some(per_tool(
"read without asking Anthropic",
"read without asking OpenAI",
)),
}
}
}
pub struct Row {
pub provider: ProviderId,
pub label: Option<String>,
pub email: String,
pub id: String,
pub account_uuid: String,
pub organization_uuid: Option<String>,
pub signed_in: bool,
pub parked: Option<Park>,
pub usage: Option<Snapshot>,
pub stale: Option<Stale>,
}
impl Row {
pub fn lasts(&self, now: i64) -> Option<crate::pace::Lasts<'_>> {
let usage = self.usage.as_ref()?;
if self.signed_in {
crate::pace::lasts(&usage.windows, usage.observed_at.unwrap_or(now), now)
} else {
crate::pace::until_reset(&usage.windows, now)
}
}
pub fn switchable(&self, now: i64) -> bool {
!self.signed_in && self.parked.as_ref().is_some_and(|p| p.restorable_at(now))
}
pub fn key(&self) -> Option<Key> {
self.label
.as_ref()
.map(|label| Key::new(self.provider, label.clone()))
}
pub fn explanation(&self) -> Option<&'static str> {
self.stale
.and_then(|stale| stale.explanation_for(self.provider))
}
pub fn unplaced(&self) -> bool {
self.label.is_none()
&& !self.signed_in
&& matches!(
self.stale,
Some(Stale::LoginUnreadable | Stale::LoginUnusable)
)
}
}
pub struct Report {
pub now: i64,
pub rows: Vec<Row>,
pub signed_in: Result<Owner, String>,
pub slot: Slot,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Slot {
pub service: String,
pub default: bool,
}
#[derive(Default)]
struct LiveLogin {
signed_in: Option<Result<Owner, String>>,
known: Option<Owner>,
usage: Option<Result<Snapshot, Stale>>,
out_of_reach: bool,
}
impl LiveLogin {
fn usage(&self) -> Result<Snapshot, Stale> {
self.usage.clone().unwrap_or(Err(Stale::NothingSignedIn))
}
}
struct Facts {
live: BTreeMap<ProviderId, LiveLogin>,
asked: bool,
parked: BTreeMap<String, Result<Snapshot, Stale>>,
}
impl Facts {
fn live_for(&self, which: ProviderId) -> Option<&LiveLogin> {
self.live.get(&which)
}
}
fn parked_document(
ctx: &Context,
key: &Key,
parked: Option<&Park>,
now: i64,
) -> Result<Value, Stale> {
match parked {
None => Err(Stale::NothingParked),
Some(p) if !p.askable_at(now) => Err(Stale::ParkedAccessExpired),
Some(p) => park::load(ctx, key, p).map_err(|_| Stale::ParkUnreadable),
}
}
fn slot_of(ctx: &Context) -> Slot {
Slot {
service: claude::live_service(ctx),
default: claude::is_default_slot(ctx),
}
}
fn tools(state: &State) -> Vec<ProviderId> {
ProviderId::ALL
.iter()
.copied()
.filter(|&which| {
which == crate::label::DEFAULT || state.accounts.iter().any(|a| a.provider() == which)
})
.collect()
}
pub fn gather_offline(ctx: &Context, state: &State) -> Report {
let known: BTreeMap<ProviderId, Option<Owner>> = tools(state)
.into_iter()
.map(|which| (which, in_use::known(ctx, state, which).owner().cloned()))
.collect();
let facts = Facts {
live: known
.iter()
.map(|(&which, owner)| {
let login = LiveLogin {
known: owner.clone(),
usage: Some(Err(Stale::NotAsked)),
..LiveLogin::default()
};
(which, login)
})
.collect(),
asked: false,
parked: BTreeMap::new(),
};
let remembered = readings::load(ctx);
Report {
now: ctx.now(),
slot: slot_of(ctx),
rows: assemble(
state,
&facts,
|uuid| remembered.get(uuid).cloned(),
ctx.now(),
),
signed_in: known
.get(&crate::label::DEFAULT)
.cloned()
.flatten()
.ok_or_else(|| "not asked".into()),
}
}
type Asked = (Result<Snapshot, Stale>, Option<(String, budget::Outcome)>);
struct LiveRead {
named: Option<String>,
login: Result<Option<Value>, ProviderError>,
}
struct Answered {
login: LiveLogin,
learned: Option<(String, budget::Outcome)>,
found: Option<InUse>,
}
impl Answered {
fn learning_nothing(login: LiveLogin) -> Answered {
Answered {
login,
learned: None,
found: None,
}
}
}
fn to_api(error: crate::provider::ProviderError) -> ApiError {
use crate::provider::ProviderError as P;
match error {
P::Unauthorized => ApiError::Unauthorized,
P::RateLimited { retry_after, .. } => ApiError::RateLimited { retry_after },
P::Network { detail, .. } => ApiError::Network(detail),
P::Unexpected { status, .. } => ApiError::Unexpected { status },
P::Malformed { detail, .. }
| P::ShapeUnexpected { detail, .. }
| P::Unsupported { reason: detail, .. } => ApiError::Malformed(detail),
P::NoLogin { .. } => ApiError::Malformed("nothing is signed in".into()),
P::InvalidGrant { .. } => ApiError::InvalidGrant,
}
}
fn ask_usage(
ctx: &Context,
which: ProviderId,
id: &str,
document: &Value,
signed_in: bool,
remembered: Option<&Snapshot>,
fresh: bool,
) -> Asked {
if let Some(held) = budget::may_ask(ctx, id, remembered, fresh) {
let stale = match held {
budget::Held::Fresh => Stale::AskedRecently,
budget::Held::RateLimited => Stale::RateLimited,
budget::Held::Unreachable => Stale::Unreachable,
};
return (Err(stale), None);
}
let answer = crate::provider::of(which).usage(
ctx,
&crate::provider::Credential::new(which, document.clone()),
);
if !signed_in
&& matches!(
answer,
Err(crate::provider::ProviderError::ShapeUnexpected { .. })
)
{
return (Err(Stale::ParkUnreadable), None);
}
let answer = answer.map_err(to_api);
let learned = match &answer {
Ok(_) => Some(budget::Outcome::Answered),
Err(api::ApiError::RateLimited { retry_after }) => {
Some(budget::Outcome::RateLimited(*retry_after))
}
Err(api::ApiError::Network(_) | api::ApiError::Unexpected { .. }) => {
Some(budget::Outcome::Unreachable)
}
Err(_) => None,
};
(
answer.map_err(|e| Stale::of(&e, signed_in)),
learned.map(|outcome| (id.to_string(), outcome)),
)
}
fn ask_live(
ctx: &Context,
state: &State,
which: ProviderId,
read: &LiveRead,
remembered: &HashMap<String, Snapshot>,
fresh: bool,
) -> Answered {
let tool = crate::provider::of(which);
let recorded = || last_known(state, which);
let nobody = || Answered {
found: identify::nobody(read.named.as_deref(), ctx.now()),
..Answered::learning_nothing(LiveLogin::default())
};
let document = match &read.login {
Ok(Some(document)) => document,
Ok(None) => return nobody(),
Err(error) => {
return Answered::learning_nothing(LiveLogin {
signed_in: Some(Err(error.to_string())),
known: recorded(),
usage: Some(Err(Stale::LoginUnreadable)),
out_of_reach: true,
});
}
};
match tool.slice(document) {
Ok(_) => {}
Err(ProviderError::NoLogin { .. }) => return nobody(),
Err(unusable) => {
let claimed = tool
.identifies_by_itself()
.then(|| {
tool.identify(
ctx,
&crate::provider::Credential::new(which, document.clone()),
)
})
.and_then(Result::ok)
.map(Owner::from);
return Answered::learning_nothing(LiveLogin {
signed_in: Some(Err(unusable.to_string())),
known: claimed,
usage: Some(Err(Stale::LoginUnusable)),
out_of_reach: true,
});
}
}
let found = match identify::whose(ctx, state, which, document) {
Ok(found) => found,
Err(error) => {
let error = to_api(error);
return Answered::learning_nothing(LiveLogin {
signed_in: Some(Err(error.to_string())),
known: recorded(),
usage: Some(Err(Stale::of(&error, true))),
out_of_reach: false,
});
}
};
let id = state.id_of(which, &found.owner);
let (usage, learned) = ask_usage(ctx, which, &id, document, true, remembered.get(&id), fresh);
Answered {
login: LiveLogin {
signed_in: Some(Ok(found.owner.clone())),
known: None,
usage: Some(usage),
out_of_reach: false,
},
learned,
found: Some(found.recorded(read.named.clone())),
}
}
fn settle(
state: &State,
answers: Vec<(ProviderId, std::thread::Result<Answered>)>,
) -> BTreeMap<ProviderId, Answered> {
answers
.into_iter()
.map(|(which, answer)| {
let answer = answer.unwrap_or_else(|_| {
Answered::learning_nothing(LiveLogin {
signed_in: Some(Err("the check did not finish".into())),
known: last_known(state, which),
usage: Some(Err(Stale::Interrupted)),
out_of_reach: false,
})
});
(which, answer)
})
.collect()
}
pub(crate) struct Answers {
live: BTreeMap<ProviderId, LiveLogin>,
parked: BTreeMap<String, Result<Snapshot, Stale>>,
learned: Vec<(String, budget::Outcome)>,
pub(crate) found: Vec<(ProviderId, InUse)>,
}
pub(crate) fn ask(ctx: &Context, state: &State, fresh: bool) -> Answers {
let now = ctx.now();
let live_reads: Vec<(ProviderId, LiveRead)> = tools(state)
.into_iter()
.map(|which| {
let named = in_use::named(ctx, which);
let login = crate::provider::of(which)
.read_live(ctx)
.map(|found| found.map(|credential| credential.raw));
(which, LiveRead { named, login })
})
.collect();
let parked_documents: Vec<Result<Value, Stale>> = state
.accounts
.iter()
.map(|a| parked_document(ctx, &a.key(), a.parked.as_ref(), now))
.collect();
let remembered = readings::load(ctx);
let (answers, parked_asked): (Vec<(ProviderId, std::thread::Result<Answered>)>, Vec<Asked>) =
std::thread::scope(|scope| {
let per_tool: Vec<_> = live_reads
.iter()
.map(|(which, read)| {
let remembered = &remembered;
let which = *which;
let handle =
scope.spawn(move || ask_live(ctx, state, which, read, remembered, fresh));
(which, handle)
})
.collect();
let parked: Vec<_> = parked_documents
.iter()
.zip(state.accounts.iter())
.map(|(token, account)| {
let remembered = &remembered;
scope.spawn(move || match token.as_ref() {
Err(stale) => (Err(*stale), None),
Ok(document) => ask_usage(
ctx,
account.provider(),
&account.id,
document,
false,
remembered.get(&account.id),
fresh,
),
})
})
.collect();
(
per_tool
.into_iter()
.map(|(which, handle)| (which, handle.join()))
.collect(),
parked
.into_iter()
.map(|h| h.join().unwrap_or((Err(Stale::Interrupted), None)))
.collect(),
)
});
let mut live = BTreeMap::new();
let mut learned = Vec::new();
let mut found = Vec::new();
for (which, answered) in settle(state, answers) {
live.insert(which, answered.login);
learned.extend(answered.learned);
found.extend(answered.found.map(|found| (which, found)));
}
let mut parked = BTreeMap::new();
for (account, (usage, outcome)) in state.accounts.iter().zip(parked_asked) {
parked.insert(account.id.clone(), usage);
learned.extend(outcome);
}
Answers {
live,
parked,
learned,
found,
}
}
pub(crate) fn report(
ctx: &Context,
permit: crate::service::Permit,
state: &State,
answers: Answers,
) -> Report {
let now = ctx.now();
budget::record(ctx, permit, &answers.learned);
let signed_in_default = answers
.live
.get(&crate::label::DEFAULT)
.and_then(|login| login.signed_in.clone());
let facts = Facts {
live: answers.live,
asked: true,
parked: answers.parked,
};
let remembered = readings::load(ctx);
let rows = assemble(state, &facts, |uuid| remembered.get(uuid).cloned(), now);
crate::home::remove_retired(ctx, permit);
readings::answered(
ctx,
permit,
&rows
.iter()
.filter_map(|r| {
r.usage
.as_ref()
.filter(|u| u.source == Source::Live)
.map(|u| (r.id.clone(), u.clone()))
})
.collect::<Vec<_>>(),
);
Report {
slot: slot_of(ctx),
now,
rows,
signed_in: match signed_in_default {
Some(Ok(owner)) => Ok(owner),
Some(Err(why)) => Err(why),
None => Err("nothing is signed in".into()),
},
}
}
fn last_known(state: &State, which: ProviderId) -> Option<Owner> {
state.in_use(which)?.owner.clone()
}
fn rank(which: ProviderId) -> usize {
ProviderId::ALL
.iter()
.position(|&known| known == which)
.unwrap_or(usize::MAX)
}
fn assemble(
state: &State,
facts: &Facts,
recall: impl Fn(&str) -> Option<Snapshot>,
now: i64,
) -> Vec<Row> {
let in_use: BTreeMap<ProviderId, &Owner> = ProviderId::ALL
.iter()
.filter_map(|&which| {
let live = facts.live_for(which)?;
let owner = match (&live.signed_in, facts.asked) {
(Some(Ok(owner)), _) => Some(owner),
(Some(Err(_)), _) | (None, false) => live.known.as_ref(),
(None, true) => None,
}?;
Some((which, owner))
})
.collect();
let live_ids: BTreeMap<ProviderId, String> = in_use
.iter()
.map(|(&which, owner)| (which, state.id_of(which, owner)))
.collect();
let reading = |id: &str, asked: &Result<Snapshot, Stale>| match asked {
Ok(live) => (
Some(crate::usage::answered(recall(id).as_ref(), live, now)),
None,
),
Err(stale) => (recall(id), Some(*stale)),
};
let mut rows: Vec<Row> = state
.accounts
.iter()
.map(|account| {
let id = account.id.as_str();
let which = account.provider();
let signed_in = live_ids.get(&which).is_some_and(|live| live == id);
let (usage, stale) = if signed_in {
let live = facts
.live_for(which)
.map_or(Err(Stale::NothingSignedIn), LiveLogin::usage);
reading(id, &live)
} else if account.parked.is_none() && account.replaced_at.is_some() {
reading(id, &Err(Stale::LoginReplaced))
} else {
let parked = facts.parked.get(id).cloned();
reading(id, &parked.unwrap_or(Err(Stale::NotAsked)))
};
Row {
provider: which,
label: Some(account.label.clone()),
email: account.email.clone(),
id: account.id.clone(),
account_uuid: account.account_uuid.clone(),
organization_uuid: account.claude().map(|c| c.organization_uuid.to_owned()),
signed_in,
parked: account.parked.clone(),
usage,
stale,
}
})
.collect();
for &which in ProviderId::ALL {
let Some(live) = facts.live_for(which) else {
continue;
};
if let Some(owner) = in_use.get(&which) {
let id = state.id_of(which, owner);
if rows.iter().any(|r| r.provider == which && r.id == id) {
continue;
}
let (usage, stale) = reading(&id, &live.usage());
rows.push(Row {
provider: which,
label: None,
email: owner.email.clone(),
id,
account_uuid: owner.account_uuid.clone(),
organization_uuid: (which == ProviderId::Claude)
.then(|| owner.organization_uuid.clone()),
signed_in: true,
parked: None,
usage,
stale,
});
continue;
}
let enrolled = state.accounts.iter().any(|a| a.provider() == which);
if live.out_of_reach && enrolled {
rows.push(Row {
provider: which,
label: None,
email: String::new(),
id: String::new(),
account_uuid: String::new(),
organization_uuid: None,
signed_in: false,
parked: None,
usage: None,
stale: live.usage().err(),
});
}
}
rows.sort_by_key(|r| (rank(r.provider), !r.signed_in));
rows
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::scripted::{Asked as Question, ScriptedApi, Trouble};
use crate::host::memory::MemoryHost;
use crate::in_use::InUse;
use crate::service::Permit;
use crate::state::Account;
use crate::store::memory::Fault;
use crate::time::{Clock, FixedClock};
use crate::usage::Window;
use serde_json::json;
use std::sync::Arc;
const NOW: i64 = 1_789_935_000;
fn owner(uuid: &str) -> Owner {
Owner {
account_uuid: uuid.into(),
email: format!("{uuid}@example.com"),
organization_uuid: "org".into(),
}
}
fn reading(percent: f64, source: Source) -> Snapshot {
Snapshot {
windows: vec![Window {
kind: "session".into(),
scope: None,
severity: None,
percent,
resets_at: Some(NOW + 3_600),
is_active: true,
length_seconds: None,
}],
observed_at: Some(NOW - 7_200),
answered_at: Some(NOW - 7_200),
lists_every_limit: true,
source,
}
}
fn parked(refresh_expires_at: i64) -> Park {
Park {
service: "pitboard-park-x-1".into(),
parked_at: NOW - 86_400,
refresh_fingerprint: "f".into(),
access_expires_at: Some(NOW - 3_600),
refresh_expires_at: Some(refresh_expires_at),
}
}
fn account(label: &str) -> Account {
Account {
last_used_at: None,
replaced_at: None,
label: label.into(),
id: format!("{label}-uuid"),
account_uuid: format!("{label}-uuid"),
email: format!("{label}@example.com"),
detail: crate::state::Detail::Claude {
organization_uuid: "org".into(),
oauth_account: json!({}),
},
parked: Some(parked(NOW + 20 * 86_400)),
}
}
fn state(labels: &[&str]) -> State {
State {
accounts: labels.iter().map(|l| account(l)).collect(),
..State::default()
}
}
fn facts(
signed_in: &str,
live: Result<Snapshot, Stale>,
parked: &[(&str, Result<Snapshot, Stale>)],
) -> Facts {
only_claude(
LiveLogin {
signed_in: Some(Ok(owner(signed_in))),
usage: Some(live),
..LiveLogin::default()
},
parked,
)
}
fn only_claude(live: LiveLogin, parked: &[(&str, Result<Snapshot, Stale>)]) -> Facts {
Facts {
live: std::iter::once((ProviderId::Claude, live)).collect(),
asked: true,
parked: asked_about(parked),
}
}
fn asked_about(
answers: &[(&str, Result<Snapshot, Stale>)],
) -> BTreeMap<String, Result<Snapshot, Stale>> {
answers
.iter()
.map(|(id, answer)| ((*id).to_owned(), answer.clone()))
.collect()
}
#[test]
fn an_unreachable_anthropic_leaves_the_signed_in_row_signed_in() {
let state = state(&["alpha", "beta"]);
let facts = Facts {
live: std::iter::once((
ProviderId::Claude,
LiveLogin {
signed_in: Some(Err("could not reach Anthropic: offline".into())),
known: Some(owner("alpha-uuid")),
usage: Some(Err(Stale::Unreachable)),
out_of_reach: false,
},
))
.collect(),
asked: true,
parked: asked_about(&[("beta-uuid", Err(Stale::Unreachable))]),
};
let rows = assemble(&state, &facts, nothing_remembered, NOW);
let a = rows
.iter()
.find(|r| r.account_uuid == "alpha-uuid")
.unwrap();
assert!(
a.signed_in,
"the account in use is still the account in use"
);
let b = rows.iter().find(|r| r.account_uuid == "beta-uuid").unwrap();
assert!(!b.signed_in);
}
fn nothing_remembered(_: &str) -> Option<Snapshot> {
None
}
fn gather(ctx: &Context, state: &State) -> Report {
report(ctx, Permit::for_a_test(), state, ask(ctx, state, true))
}
fn stored(login: Value) -> LiveRead {
LiveRead {
named: None,
login: Ok(Some(login)),
}
}
#[test]
fn every_stale_code_is_its_json_form() {
for stale in [
Stale::NothingSignedIn,
Stale::LoginUnreadable,
Stale::LoginUnusable,
Stale::SessionExpired,
Stale::ParkedAccessExpired,
Stale::NothingParked,
Stale::LoginReplaced,
Stale::ParkUnreadable,
Stale::RateLimited,
Stale::Unreachable,
Stale::ServerError,
Stale::AnswerNotUnderstood,
Stale::LoginRefused,
Stale::Interrupted,
Stale::NotAsked,
] {
assert_eq!(serde_json::to_value(stale).unwrap(), stale.code());
}
}
#[test]
fn anthropics_failures_are_told_apart() {
use crate::api::ApiError;
let parked = |e: &ApiError| Stale::of(e, false);
assert_eq!(
parked(&ApiError::Unexpected { status: 503 }),
Stale::ServerError
);
assert_eq!(
parked(&ApiError::Malformed("no windows".into())),
Stale::AnswerNotUnderstood
);
assert_eq!(parked(&ApiError::InvalidGrant), Stale::LoginRefused);
assert_eq!(
parked(&ApiError::RateLimited { retry_after: None }),
Stale::RateLimited
);
assert_eq!(
parked(&ApiError::Network("down".into())),
Stale::Unreachable
);
assert_eq!(parked(&ApiError::Unauthorized), Stale::ParkedAccessExpired);
assert_eq!(
Stale::of(&ApiError::Unauthorized, true),
Stale::SessionExpired
);
}
#[test]
fn an_expired_session_shows_what_was_remembered_and_says_why() {
let s = state(&["work"]);
let f = facts("work-uuid", Err(Stale::SessionExpired), &[]);
let recorded = |_: &str| Some(reading(22.0, Source::Remembered));
let rows = assemble(&s, &f, recorded, NOW);
let usage = rows[0].usage.as_ref().unwrap();
assert_eq!(usage.windows[0].percent, 22.0);
assert_eq!(usage.source, Source::Remembered);
assert_eq!(rows[0].stale, Some(Stale::SessionExpired));
}
#[test]
fn offline_the_account_in_use_shows_the_readings() {
let s = state(&["work"]);
let mut f = facts("work-uuid", Err(Stale::NotAsked), &[]);
f.asked = false;
let recorded = |_: &str| Some(reading(22.0, Source::Remembered));
let rows = assemble(&s, &f, recorded, NOW);
assert!(rows[0].signed_in);
let usage = rows[0].usage.as_ref().unwrap();
assert_eq!(usage.windows[0].percent, 22.0);
assert_eq!(usage.source, Source::Remembered);
assert_eq!(rows[0].stale, Some(Stale::NotAsked));
}
#[test]
fn a_live_reading_behind_what_a_session_recorded_does_not_move_the_row_back() {
let s = state(&["work"]);
let f = facts("work-uuid", Ok(reading(20.0, Source::Live)), &[]);
let recorded = |_: &str| {
let mut since = reading(22.0, Source::Remembered);
since.observed_at = Some(NOW - 60);
Some(since)
};
let rows = assemble(&s, &f, recorded, NOW);
assert_eq!(rows[0].usage.as_ref().unwrap().windows[0].percent, 22.0);
assert_eq!(rows[0].stale, None);
let f = facts("work-uuid", Ok(reading(30.0, Source::Live)), &[]);
let usage = assemble(&s, &f, recorded, NOW)[0].usage.clone().unwrap();
assert_eq!(usage.windows[0].percent, 30.0);
assert_eq!(
usage.source,
Source::Live,
"a live reading ahead is shown as live"
);
}
#[test]
fn a_live_reading_taken_after_what_was_recorded_is_shown_however_low() {
let s = state(&["work"]);
let mut answered = reading(14.0, Source::Live);
answered.observed_at = Some(NOW);
answered.answered_at = Some(NOW);
let f = facts("work-uuid", Ok(answered), &[]);
let recorded = |_: &str| Some(reading(100.0, Source::Remembered));
let usage = assemble(&s, &f, recorded, NOW)[0].usage.clone().unwrap();
assert_eq!(usage.windows[0].percent, 14.0);
assert_eq!(usage.source, Source::Live);
}
#[test]
fn a_parked_account_is_asked_with_its_own_login() {
let s = state(&["work", "personal"]);
let f = facts(
"work-uuid",
Ok(reading(30.0, Source::Live)),
&[("personal-uuid", Ok(reading(12.0, Source::Live)))],
);
let rows = assemble(&s, &f, nothing_remembered, NOW);
let personal = rows
.iter()
.find(|r| r.label.as_deref() == Some("personal"))
.unwrap();
assert_eq!(personal.usage.as_ref().unwrap().windows[0].percent, 12.0);
assert!(personal.switchable(NOW));
}
#[test]
fn a_refused_token_is_an_expired_session_when_it_is_the_one_signed_in() {
let api = crate::api::scripted::ScriptedApi::new();
api.token_trouble("t", crate::api::scripted::Trouble::Unauthorized);
let ctx = Context::new(std::path::PathBuf::from("/nowhere"))
.with_pitboard_home(std::env::temp_dir().join("pitboard-status-refused"))
.with_scripted_api(api);
let login = serde_json::json!({"claudeAiOauth": {"accessToken": "t"}});
let ask = |signed_in| {
ask_usage(
&ctx,
ProviderId::Claude,
"t-uuid",
&login,
signed_in,
None,
true,
)
.0
};
assert_eq!(ask(true).unwrap_err(), Stale::SessionExpired);
assert_eq!(ask(false).unwrap_err(), Stale::ParkedAccessExpired);
}
#[test]
fn a_park_not_in_its_own_tools_shape_is_unreadable_rather_than_an_answer() {
let ctx = Context::new(std::path::PathBuf::from("/nowhere"));
let not_codex = serde_json::json!({"claudeAiOauth": {"accessToken": "t"}});
let (answer, learned) = ask_usage(
&ctx,
ProviderId::Codex,
"t-acc",
¬_codex,
false,
None,
true,
);
assert_eq!(answer.unwrap_err(), Stale::ParkUnreadable);
assert!(
learned.is_none(),
"nothing was asked, so there is nothing to learn"
);
}
#[test]
fn a_parked_login_past_its_access_expiry_is_not_asked() {
let ctx = Context::for_unit_test();
let personal = Key::new(ProviderId::Claude, "personal");
let token = parked_document(&ctx, &personal, Some(&parked(NOW + 86_400)), NOW);
assert_eq!(token, Err(Stale::ParkedAccessExpired));
assert_eq!(
parked_document(&ctx, &personal, None, NOW),
Err(Stale::NothingParked)
);
}
#[test]
fn what_cannot_be_asked_falls_back_to_what_was_remembered() {
let s = state(&["work", "personal"]);
let f = facts(
"work-uuid",
Ok(reading(30.0, Source::Live)),
&[("personal-uuid", Err(Stale::ParkedAccessExpired))],
);
let remembered =
|uuid: &str| (uuid == "personal-uuid").then(|| reading(44.0, Source::Remembered));
let rows = assemble(&s, &f, remembered, NOW);
let personal = rows
.iter()
.find(|r| r.label.as_deref() == Some("personal"))
.unwrap();
assert_eq!(personal.usage.as_ref().unwrap().source, Source::Remembered);
assert_eq!(personal.stale, Some(Stale::ParkedAccessExpired));
}
#[test]
fn the_signed_in_account_comes_first_and_is_taken_from_the_server_not_the_config() {
let s = state(&["alpha", "beta"]);
let f = facts(
"beta-uuid",
Ok(reading(5.0, Source::Live)),
&[("alpha-uuid", Err(Stale::NothingParked))],
);
let rows = assemble(&s, &f, nothing_remembered, NOW);
assert_eq!(rows[0].label.as_deref(), Some("beta"));
assert!(rows[0].signed_in && !rows[0].switchable(NOW));
assert!(!rows[1].signed_in);
}
const EVERY_STALE: [Stale; 16] = [
Stale::NothingSignedIn,
Stale::LoginUnreadable,
Stale::LoginUnusable,
Stale::SessionExpired,
Stale::ParkedAccessExpired,
Stale::NothingParked,
Stale::LoginReplaced,
Stale::ParkUnreadable,
Stale::RateLimited,
Stale::Unreachable,
Stale::ServerError,
Stale::AnswerNotUnderstood,
Stale::LoginRefused,
Stale::AskedRecently,
Stale::Interrupted,
Stale::NotAsked,
];
#[test]
fn claude_codes_explanations_are_word_for_word_what_they_were() {
let before = |stale: Stale| match stale {
Stale::NothingSignedIn => Some("nothing is signed in"),
Stale::SessionExpired => Some("Claude Code's session has expired; `claude` renews it"),
Stale::ParkedAccessExpired | Stale::NothingParked | Stale::AskedRecently => None,
Stale::ParkUnreadable => Some("its parked login cannot be read; run `pitboard doctor`"),
Stale::RateLimited => Some("Anthropic is rate limiting usage checks"),
Stale::Unreachable => Some("Anthropic could not be reached"),
Stale::ServerError => Some("Anthropic answered with an error; try again later"),
Stale::AnswerNotUnderstood => Some("Anthropic's answer was not understood"),
Stale::LoginRefused => Some("its parked login is no longer accepted; sign in again"),
Stale::Interrupted => Some("the check did not finish"),
Stale::NotAsked => Some("read without asking Anthropic"),
Stale::LoginReplaced => {
Some("its login was replaced by a sign-in outside Pitboard; sign in again")
}
Stale::LoginUnreadable => {
Some("Claude Code's login could not be read; run `pitboard doctor`")
}
Stale::LoginUnusable => Some(
"Claude Code's login is not one Pitboard can park or switch; run `pitboard doctor`",
),
};
for stale in EVERY_STALE {
assert_eq!(
stale.explanation_for(ProviderId::Claude),
before(stale),
"{stale:?}"
);
assert_eq!(
stale.explanation(),
stale.explanation_for(ProviderId::Claude),
"a caller with only a code gets the default tool's words"
);
}
}
#[test]
fn a_codex_row_names_openai_and_codex_never_anthropic_or_claude() {
for stale in EVERY_STALE {
let Some(said) = stale.explanation_for(ProviderId::Codex) else {
assert_eq!(stale.explanation_for(ProviderId::Claude), None, "{stale:?}");
continue;
};
for other in ["Anthropic", "Claude", "claude"] {
assert!(!said.contains(other), "{stale:?}: {said}");
}
}
assert_eq!(
Stale::Unreachable.explanation_for(ProviderId::Codex),
Some("OpenAI could not be reached")
);
assert_eq!(
Stale::SessionExpired.explanation_for(ProviderId::Codex),
Some("Codex's session has expired; `codex` renews it")
);
let mut codex = codex_account("work", "work-acc");
codex.parked = None;
let s = State {
accounts: vec![codex],
..State::default()
};
let f = Facts {
live: BTreeMap::new(),
asked: true,
parked: asked_about(&[("work-acc", Err(Stale::Unreachable))]),
};
let rows = assemble(&s, &f, nothing_remembered, NOW);
assert_eq!(rows[0].explanation(), Some("OpenAI could not be reached"));
}
fn codex_account(label: &str, uuid: &str) -> Account {
Account {
last_used_at: None,
replaced_at: None,
label: label.into(),
id: uuid.into(),
account_uuid: uuid.into(),
email: format!("{label}@example.com"),
detail: crate::state::Detail::Codex {
workspace_id: None,
plan: None,
},
parked: Some(parked(NOW + 20 * 86_400)),
}
}
fn live(owner_uuid: &str, usage: Result<Snapshot, Stale>) -> LiveLogin {
LiveLogin {
signed_in: Some(Ok(owner(owner_uuid))),
usage: Some(usage),
..LiveLogin::default()
}
}
#[test]
fn the_same_identity_on_two_tools_is_two_rows() {
let s = state(&["same"]);
let f = Facts {
live: [
(
ProviderId::Claude,
live("same-uuid", Ok(reading(10.0, Source::Live))),
),
(
ProviderId::Codex,
live("same-uuid", Ok(reading(20.0, Source::Live))),
),
]
.into_iter()
.collect(),
asked: true,
parked: BTreeMap::new(),
};
let rows = assemble(&s, &f, nothing_remembered, NOW);
assert_eq!(rows.len(), 2, "one row per tool");
assert_eq!(rows[0].provider, ProviderId::Claude);
assert_eq!(rows[0].label.as_deref(), Some("same"));
assert_eq!(rows[1].provider, ProviderId::Codex);
assert_eq!(rows[1].label, None, "nothing has enrolled the Codex login");
assert!(rows[1].signed_in);
assert_eq!(rows[1].usage.as_ref().unwrap().windows[0].percent, 20.0);
}
#[test]
fn a_codex_row_keeps_a_running_window_openai_left_out() {
let s = State {
accounts: vec![codex_account("work", "work-acc")],
..State::default()
};
let window = |kind: &str, percent: f64| Window {
kind: kind.into(),
scope: None,
severity: None,
percent,
resets_at: Some(NOW + 3_600),
is_active: true,
length_seconds: None,
};
let known = Snapshot {
windows: vec![window("five_hour", 30.0), window("seven_day", 40.0)],
observed_at: Some(NOW - 600),
answered_at: Some(NOW - 600),
lists_every_limit: false,
source: Source::Remembered,
};
let answer = Snapshot {
windows: vec![window("seven_day", 45.0)],
observed_at: Some(NOW),
answered_at: Some(NOW),
lists_every_limit: false,
source: Source::Live,
};
let f = Facts {
live: std::iter::once((ProviderId::Codex, live("work-acc", Ok(answer)))).collect(),
asked: true,
parked: BTreeMap::new(),
};
let rows = assemble(&s, &f, |_| Some(known.clone()), NOW);
let shares: Vec<(&str, f64)> = rows[0]
.usage
.as_ref()
.unwrap()
.windows
.iter()
.map(|w| (w.kind.as_str(), w.percent))
.collect();
assert_eq!(shares, [("five_hour", 30.0), ("seven_day", 45.0)]);
}
#[test]
fn rows_are_grouped_by_tool_with_the_signed_in_account_first_in_each() {
let mut s = state(&["alpha"]);
s.accounts.push(codex_account("work", "work-acc"));
s.accounts.push(account("beta"));
s.accounts.push(codex_account("home", "home-acc"));
let f = Facts {
live: [
(
ProviderId::Claude,
live("beta-uuid", Ok(reading(5.0, Source::Live))),
),
(
ProviderId::Codex,
live("home-acc", Ok(reading(6.0, Source::Live))),
),
]
.into_iter()
.collect(),
asked: true,
parked: BTreeMap::new(),
};
let order: Vec<(ProviderId, String, bool)> = assemble(&s, &f, nothing_remembered, NOW)
.into_iter()
.map(|r| (r.provider, r.label.unwrap_or_default(), r.signed_in))
.collect();
assert_eq!(
order,
[
(ProviderId::Claude, "beta".into(), true),
(ProviderId::Claude, "alpha".into(), false),
(ProviderId::Codex, "home".into(), true),
(ProviderId::Codex, "work".into(), false),
]
);
}
#[test]
fn a_thread_that_stopped_stands_in_for_its_own_tool_only() {
let claude_answer = Answered {
learned: Some(("alpha-uuid".into(), budget::Outcome::Answered)),
..Answered::learning_nothing(live("alpha-uuid", Ok(reading(30.0, Source::Live))))
};
let panicked: Box<dyn std::any::Any + Send> = Box::new("boom");
let settled = settle(
&state(&["alpha"]),
vec![
(ProviderId::Claude, Ok(claude_answer)),
(ProviderId::Codex, Err(panicked)),
],
);
assert_eq!(
settled[&ProviderId::Claude].learned,
Some(("alpha-uuid".to_string(), budget::Outcome::Answered))
);
let codex = &settled[&ProviderId::Codex];
assert_eq!(codex.login.usage().err(), Some(Stale::Interrupted));
assert!(
matches!(codex.login.signed_in, Some(Err(_))),
"unknown, not nobody"
);
assert!(codex.learned.is_none() && codex.found.is_none());
let mut s = state(&["alpha"]);
s.accounts.push(codex_account("work", "work-acc"));
let f = Facts {
live: settled
.into_iter()
.map(|(which, answered)| (which, answered.login))
.collect(),
asked: true,
parked: BTreeMap::new(),
};
let rows = assemble(&s, &f, nothing_remembered, NOW);
let alpha = rows
.iter()
.find(|r| r.provider == ProviderId::Claude)
.unwrap();
assert!(alpha.signed_in, "Claude Code's row survives Codex's thread");
assert_eq!(alpha.usage.as_ref().unwrap().windows[0].percent, 30.0);
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn a_document_holding_no_account_is_nothing_signed_in_and_nobody_is_asked() {
let home = scratch("logged-out");
let (ctx, _mem, api) = machine(&home.0, None);
let logged_out = json!({"mcpOAuth": {"some-server": {"token": "unrelated"}}});
let answered = ask_live(
&ctx,
&State::default(),
ProviderId::Claude,
&stored(logged_out),
&HashMap::new(),
true,
);
let login = &answered.login;
assert!(login.signed_in.is_none(), "nobody, rather than unknown");
assert_eq!(login.usage().err(), Some(Stale::NothingSignedIn));
assert!(answered.learned.is_none());
assert_eq!(
answered.found.map(|found| (found.owner, found.login)),
Some((None, String::new())),
"found holding no login, which a read records"
);
assert_eq!(api.calls(), 0, "Anthropic was asked: {:?}", api.asked());
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn a_login_that_cannot_be_read_is_said_rather_than_read_as_nobody_signed_in() {
let home = scratch("unreadable");
let (ctx, mem, api) = machine(&home.0, Some("alpha-uuid"));
let service = claude::live_service(&ctx);
mem.live().plant(
&service,
&json!({"claudeAiOauth": {"accessToken": "t"}}).to_string(),
);
mem.live()
.fault(&service, Fault::Unreadable("the keychain is locked".into()));
let mut s = state(&["alpha", "beta"]);
for a in &mut s.accounts {
a.parked = None;
}
let alpha = InUse::of(&s.accounts[0], "alpha-refresh", NOW);
s.identified(ProviderId::Claude, alpha, NOW);
let report = gather(&ctx, &s);
let alpha = report
.rows
.iter()
.find(|r| r.label.as_deref() == Some("alpha"))
.unwrap();
assert!(
alpha.signed_in,
"Pitboard's record of whose login is stored still names the account in use"
);
assert_eq!(alpha.stale, Some(Stale::LoginUnreadable));
assert_eq!(
alpha.explanation(),
Some("Claude Code's login could not be read; run `pitboard doctor`")
);
let why = report.signed_in.expect_err("nobody could say whose it is");
assert_ne!(why, "nothing is signed in");
assert_eq!(api.calls(), 0, "a login nobody could read was sent nowhere");
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn the_account_last_recorded_in_use_is_not_sent_to_sign_in_while_the_login_cannot_be_read() {
let home = scratch("unreadable-recorded");
let (ctx, mem, api) = machine(&home.0, Some("alpha-uuid"));
let service = claude::live_service(&ctx);
mem.live().plant(
&service,
&json!({"claudeAiOauth": {"refreshToken": "beta-refresh"}}).to_string(),
);
mem.live()
.fault(&service, Fault::Unreadable("the keychain is locked".into()));
let mut s = state(&["alpha", "beta"]);
s.accounts[1].parked = None;
let beta = InUse::of(&s.accounts[1], "beta-refresh", NOW);
s.identified(ProviderId::Claude, beta, NOW);
let report = gather(&ctx, &s);
let row = |label: &str| {
report
.rows
.iter()
.find(|r| r.label.as_deref() == Some(label))
.map(|r| (r.signed_in, r.stale))
.unwrap()
};
assert_eq!(row("beta"), (true, Some(Stale::LoginUnreadable)));
assert!(!row("alpha").0, "whatever Claude Code's config names");
assert_eq!(api.calls(), 0);
let readable = only_claude(
live("alpha-uuid", Ok(reading(5.0, Source::Live))),
&[("beta-uuid", Err(Stale::NothingParked))],
);
let rows = assemble(&s, &readable, nothing_remembered, NOW);
let beta = rows
.iter()
.find(|r| r.label.as_deref() == Some("beta"))
.unwrap();
assert_eq!(beta.stale, Some(Stale::NothingParked));
}
#[test]
fn an_unreadable_login_gets_a_row_only_for_a_tool_with_accounts() {
let unreadable = || LiveLogin {
signed_in: Some(Err("the keychain is locked".into())),
usage: Some(Err(Stale::LoginUnreadable)),
out_of_reach: true,
..LiveLogin::default()
};
let facts = || Facts {
live: [
(
ProviderId::Claude,
live("alpha-uuid", Ok(reading(5.0, Source::Live))),
),
(ProviderId::Codex, unreadable()),
]
.into_iter()
.collect(),
asked: true,
parked: BTreeMap::new(),
};
let claude_only = state(&["alpha"]);
let rows = assemble(&claude_only, &facts(), nothing_remembered, NOW);
assert_eq!(
rows.len(),
1,
"nothing new for somebody who never used Codex"
);
let mut both = state(&["alpha"]);
both.accounts.push(codex_account("work", "work-acc"));
let rows = assemble(&both, &facts(), nothing_remembered, NOW);
let said = rows
.iter()
.find(|r| r.provider == ProviderId::Codex && r.label.is_none())
.expect("a row saying Codex's login could not be read");
assert_eq!(said.stale, Some(Stale::LoginUnreadable));
assert!(!said.signed_in && !said.switchable(NOW));
assert_eq!(
said.explanation(),
Some("Codex's login could not be read; run `pitboard doctor`")
);
let work = rows
.iter()
.find(|r| r.label.as_deref() == Some("work"))
.unwrap();
assert!(!work.signed_in);
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn a_login_nobody_could_name_this_time_is_not_asked_about_usage() {
let home = scratch("unnamed");
let (ctx, _mem, api) = machine(&home.0, Some("acc-x"));
api.token_trouble("access-x", Trouble::Offline);
let answered = ask_live(
&ctx,
&State::default(),
ProviderId::Claude,
&stored(json!({"claudeAiOauth": {"accessToken": "access-x"}})),
&HashMap::new(),
true,
);
assert_eq!(answered.login.usage().err(), Some(Stale::Unreachable));
assert!(matches!(answered.login.signed_in, Some(Err(_))));
assert!(answered.learned.is_none() && answered.found.is_none());
assert_eq!(api.asked(), [Question::Owner("access-x".into())]);
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn offline_every_tool_names_its_own_signed_in_account() {
let home = scratch("offline");
let (ctx, _mem, api) = machine(&home.0, Some("alpha-uuid"));
let codex = crate::provider::of(ProviderId::Codex)
.live(&ctx)
.expect("Codex keeps its login in a file here");
let login = json!({
"auth_mode": "chatgpt",
"tokens": {
"id_token": crate::provider::jwt::unsigned(&json!({
"email": "work@example.com",
"https://api.openai.com/auth": {"chatgpt_account_id": "work-acc"},
})),
"access_token": "codex-access",
"refresh_token": "codex-refresh",
"account_id": "work-acc",
},
"last_refresh": "2026-09-15T05:05:11Z",
});
crate::store::write_raw(
&codex.chain,
Permit::for_a_test(),
&codex.service,
&login.to_string(),
)
.expect("a Codex login");
let mut s = state(&["alpha", "beta"]);
s.accounts.push(codex_account("work", "work-acc"));
s.accounts.push(codex_account("home", "home-acc"));
let beta = InUse::of(&s.accounts[1], "beta-refresh", NOW);
s.identified(ProviderId::Claude, beta, NOW);
let home = InUse::of(&s.accounts[3], "home-refresh", NOW);
s.identified(ProviderId::Codex, home, NOW);
let report = gather_offline(&ctx, &s);
let signed_in: Vec<(ProviderId, &str)> = report
.rows
.iter()
.filter(|r| r.signed_in)
.map(|r| (r.provider, r.label.as_deref().unwrap_or_default()))
.collect();
assert_eq!(
signed_in,
[(ProviderId::Claude, "beta"), (ProviderId::Codex, "work")]
);
assert_eq!(
report
.signed_in
.expect("what Anthropic last said")
.account_uuid,
"beta-uuid"
);
assert_eq!(api.calls(), 0, "offline asks nobody");
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn offline_a_login_nobody_enrolled_has_its_row() {
let home = scratch("offline-unenrolled");
let (ctx, _mem, api) = machine(&home.0, Some("alpha-uuid"));
let mut s = state(&["alpha"]);
let stranger = InUse {
owner: Some(owner("stranger-uuid")),
login: "stranger-refresh".into(),
known_at: NOW,
named: in_use::named(&ctx, ProviderId::Claude),
};
s.identified(ProviderId::Claude, stranger, NOW);
readings::answered(
&ctx,
Permit::for_a_test(),
&[("stranger-uuid_org".into(), reading(30.0, Source::Live))],
);
let report = gather_offline(&ctx, &s);
let in_use: Vec<(Option<&str>, &str, Option<f64>)> = report
.rows
.iter()
.filter(|r| r.signed_in)
.map(|r| {
let share = r.usage.as_ref().map(|u| u.windows[0].percent);
(r.label.as_deref(), r.email.as_str(), share)
})
.collect();
assert_eq!(in_use, [(None, "stranger-uuid@example.com", Some(30.0))]);
assert_eq!(
report.signed_in.expect("the record's").account_uuid,
"stranger-uuid"
);
assert_eq!(api.calls(), 0);
}
fn plant_codex(ctx: &Context, raw: &str) {
let codex = crate::provider::of(ProviderId::Codex)
.live(ctx)
.expect("Codex keeps its login in a file here");
crate::store::write_raw(&codex.chain, Permit::for_a_test(), &codex.service, raw)
.expect("a Codex login");
}
fn codex_login(tokens_of: &str, account_id: &str) -> String {
json!({
"auth_mode": "chatgpt",
"tokens": {
"id_token": crate::provider::jwt::unsigned(&json!({
"email": format!("{tokens_of}@example.com"),
"https://api.openai.com/auth": {"chatgpt_account_id": tokens_of},
})),
"access_token": format!("access-{tokens_of}"),
"refresh_token": format!("refresh-{tokens_of}"),
"account_id": account_id,
},
"last_refresh": "2026-09-15T05:05:11Z",
})
.to_string()
}
fn codex_a_parked_b_in_use() -> State {
let mut b = codex_account("b", "acc-B");
b.parked = None;
let mut s = State {
accounts: vec![codex_account("a", "acc-A"), b],
..State::default()
};
let b = InUse::of(&s.accounts[1], "refresh-acc-B", NOW);
s.identified(ProviderId::Codex, b, NOW);
s
}
fn codex_row<'a>(report: &'a Report, label: &str) -> &'a Row {
report
.rows
.iter()
.find(|r| r.provider == ProviderId::Codex && r.label.as_deref() == Some(label))
.unwrap_or_else(|| panic!("a row for codex/{label}"))
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_codex_login_that_mixes_two_accounts_is_said_rather_than_read_as_nobody() {
let home = scratch("mixed");
let (ctx, _mem, api) = machine(&home.0, None);
plant_codex(&ctx, &codex_login("acc-A", "acc-B"));
let s = codex_a_parked_b_in_use();
let report = gather(&ctx, &s);
let a = codex_row(&report, "a");
assert!(a.signed_in, "the tokens are a's, as its own record says");
assert_eq!(a.stale, Some(Stale::LoginUnusable));
assert_eq!(
a.explanation(),
Some("Codex's login is not one Pitboard can park or switch; run `pitboard doctor`")
);
assert!(!codex_row(&report, "b").signed_in);
assert!(
report
.rows
.iter()
.all(|r| r.stale != Some(Stale::NothingSignedIn)),
"something is signed in"
);
assert!(
report.rows.iter().all(|r| !r.unplaced()),
"the login is pinned on a, so it needs no row of its own"
);
assert_eq!(
api.calls(),
0,
"nobody was asked about a login mixing two accounts"
);
let offline = gather_offline(&ctx, &s);
assert!(codex_row(&offline, "a").signed_in, "offline says the same");
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_codex_login_with_an_api_key_is_said_and_pinned_on_no_account() {
let home = scratch("api-key");
let (ctx, _mem, api) = machine(&home.0, None);
plant_codex(
&ctx,
&json!({"auth_mode": "apikey", "OPENAI_API_KEY": "sk-not-a-real-key"}).to_string(),
);
let s = codex_a_parked_b_in_use();
let report = gather(&ctx, &s);
assert!(
report.rows.iter().all(|r| !r.signed_in),
"no account is signed in"
);
let said = report
.rows
.iter()
.find(|r| r.unplaced())
.expect("a row saying what is signed in to Codex");
assert_eq!(said.provider, ProviderId::Codex);
assert_eq!(said.stale, Some(Stale::LoginUnusable));
assert!(said.email.is_empty() && said.account_uuid.is_empty());
assert_eq!(api.calls(), 0);
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_tool_with_nothing_enrolled_is_neither_read_nor_asked() {
let home = scratch("not-opted-in");
let (ctx, _mem, api) = machine(&home.0, None);
plant_codex(&ctx, &codex_login("acc-B", "acc-B"));
let s = State::default();
let report = gather(&ctx, &s);
assert!(
report.rows.iter().all(|r| r.provider != ProviderId::Codex),
"no Codex row for a machine that enrolled no Codex account"
);
assert!(
api.asked()
.iter()
.all(|asked| !matches!(asked, crate::api::scripted::Asked::Usage(t) if t.contains("acc-B"))),
"and nothing was asked about it"
);
let offline = gather_offline(&ctx, &s);
assert!(offline.rows.iter().all(|r| r.provider != ProviderId::Codex));
}
#[test]
#[cfg_attr(windows, ignore = "W21: switching Codex on Windows")]
fn a_codex_login_caught_half_written_still_names_the_account_in_use() {
let home = scratch("half-written");
let (ctx, _mem, api) = machine(&home.0, None);
let whole = codex_login("acc-B", "acc-B");
plant_codex(&ctx, &whole[..whole.len() / 2]);
let s = codex_a_parked_b_in_use();
crate::state::save(&ctx, Permit::for_a_test(), &s).expect("an account list");
let report = gather(&ctx, &s);
let b = codex_row(&report, "b");
assert!(b.signed_in, "the account Pitboard last recorded in use");
assert_eq!(b.stale, Some(Stale::LoginUnreadable));
assert!(!codex_row(&report, "a").signed_in);
assert!(
report.rows.iter().all(|r| !r.unplaced()),
"pinned, so no row of its own"
);
assert_eq!(api.calls(), 0);
let doctor = crate::doctor::gather(&ctx);
let active: Vec<&str> = doctor
.parks
.iter()
.filter(|p| p.active)
.map(|p| p.label.as_str())
.collect();
assert_eq!(active, ["b"], "doctor reads the same machine the same way");
}
#[test]
#[cfg_attr(windows, ignore = "W15: files made private to the person on Windows")]
fn only_a_document_holding_no_login_is_nobody_signed_in() {
let home = scratch("shapes");
let (ctx, _mem, api) = machine(&home.0, Some("alpha-uuid"));
let mut beta_in_use = state(&["alpha", "beta"]);
let beta = InUse::of(&beta_in_use.accounts[1], "beta-refresh", NOW);
beta_in_use.identified(ProviderId::Claude, beta, NOW);
let ask = |document: Value| {
ask_live(
&ctx,
&beta_in_use,
ProviderId::Claude,
&stored(document),
&HashMap::new(),
true,
)
.login
};
let nobody = ask(json!({"mcpOAuth": {}}));
assert!(nobody.signed_in.is_none() && !nobody.out_of_reach);
assert_eq!(nobody.usage().err(), Some(Stale::NothingSignedIn));
let unusable = ask(json!(["a login", "that is not an object"]));
assert!(matches!(unusable.signed_in, Some(Err(_))));
assert!(unusable.out_of_reach);
assert_eq!(unusable.usage().err(), Some(Stale::LoginUnusable));
assert_eq!(
unusable.known, None,
"neither Claude Code's config nor Pitboard's record of whose login was stored"
);
assert_eq!(api.calls(), 0);
}
#[test]
fn the_row_in_use_is_the_organisation_anthropic_names() {
let in_organisation = |label: &str, id: &str, organization: &str| Account {
id: id.into(),
account_uuid: "acc".into(),
detail: crate::state::Detail::Claude {
organization_uuid: organization.into(),
oauth_account: json!({}),
},
..account(label)
};
let state = State {
accounts: vec![
in_organisation("work", "acc", "org-a"),
in_organisation("team", "acc_org-b", "org-b"),
],
..State::default()
};
let facts = only_claude(
LiveLogin {
signed_in: Some(Ok(Owner {
account_uuid: "acc".into(),
email: "acc@example.com".into(),
organization_uuid: "org-b".into(),
})),
usage: Some(Ok(reading(30.0, Source::Live))),
..LiveLogin::default()
},
&[],
);
let rows = assemble(&state, &facts, nothing_remembered, NOW);
let said: Vec<(Option<&str>, &str, Option<&str>, bool)> = rows
.iter()
.map(|r| {
(
r.label.as_deref(),
r.id.as_str(),
r.organization_uuid.as_deref(),
r.signed_in,
)
})
.collect();
assert_eq!(
said,
[
(Some("team"), "acc_org-b", Some("org-b"), true),
(Some("work"), "acc", Some("org-a"), false),
]
);
}
#[test]
fn a_row_is_unplaced_only_when_it_is_a_login_on_no_account() {
let mut row = Row {
provider: ProviderId::Codex,
label: None,
email: String::new(),
id: String::new(),
account_uuid: String::new(),
organization_uuid: None,
signed_in: false,
parked: None,
usage: None,
stale: Some(Stale::LoginUnreadable),
};
assert!(row.unplaced());
row.stale = Some(Stale::LoginUnusable);
assert!(row.unplaced());
row.signed_in = true;
row.stale = Some(Stale::SessionExpired);
assert!(!row.unplaced(), "an unenrolled login that is signed in");
row.signed_in = false;
row.label = Some("work".into());
row.stale = Some(Stale::LoginUnreadable);
assert!(!row.unplaced(), "an account");
}
struct Scratch(std::path::PathBuf);
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn scratch(name: &str) -> Scratch {
let root = std::env::temp_dir().join(format!(
"pitboard-status-{name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch home");
Scratch(root)
}
fn machine(
root: &std::path::Path,
recorded: Option<&str>,
) -> (Context, Arc<MemoryHost>, Arc<ScriptedApi>) {
let mem = MemoryHost::new();
let api = ScriptedApi::new();
let ctx = Context::new(root.to_path_buf())
.with_pitboard_home(root.join(".pitboard"))
.with_codex_home(root.join("codex").to_string_lossy().into())
.with_codex_program(root.join("bin").join("codex"))
.with_memory_stores(Arc::clone(&mem))
.with_scripted_api(Arc::clone(&api))
.with_clock(Arc::new(FixedClock::at(NOW)) as Arc<dyn Clock>);
crate::home::ensure(&ctx, Permit::for_a_test()).expect("a Pitboard home");
if let Some(uuid) = recorded {
std::fs::write(
root.join(".claude.json"),
json!({"oauthAccount": {
"accountUuid": uuid,
"emailAddress": format!("{uuid}@example.com"),
"organizationUuid": "org",
}})
.to_string(),
)
.expect("a Claude Code config");
}
(ctx, mem, api)
}
}