use super::harness::{
NOW, POINTS, codex_machine, document, hold, in_organisation, login_of, machine, owner, recover,
signed_in,
};
use super::*;
use crate::api::scripted::{ScriptedApi, Trouble};
use crate::fault;
use crate::provider::claude::api::Renewed;
use crate::service::Permit;
use crate::store::memory::Fault;
use serde_json::json;
use std::sync::Arc;
type Make = fn(&str) -> harness::Machine;
macro_rules! each_tool_and_step {
($check:ident) => {
each_tool_and_step!(
$check;
claude_journal_written: machine, "switch.journal_written";
claude_park_stored: machine, "switch.park_stored";
claude_park_recorded: machine, "switch.park_recorded";
claude_installed: machine, "switch.installed";
claude_recorded: machine, "switch.recorded";
claude_config_updated: machine, "switch.config_updated";
codex_journal_written: codex_machine, "switch.journal_written";
codex_park_stored: codex_machine, "switch.park_stored";
codex_park_recorded: codex_machine, "switch.park_recorded";
codex_installed: codex_machine, "switch.installed";
codex_recorded: codex_machine, "switch.recorded";
codex_config_updated: codex_machine, "switch.config_updated";
);
};
($check:ident; $($case:ident: $make:ident, $point:literal;)*) => {
const _: () = assert!([$($point),*].len() == 2 * POINTS.len());
$(
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn $case() {
$check(stringify!($case), $make, $point);
}
)*
};
}
mod a_switch_killed_at_any_step_recovers_to_something_whole {
use super::*;
each_tool_and_step!(recovers_whole);
}
fn recovers_whole(at: &str, make: Make, point: &'static str) {
let m = make(&point.replace('.', "-"));
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let died = fault::killing(point, || switch(settled, &m.key("there")));
assert_eq!(
died.unwrap_err(),
point,
"{at}: the switch must reach {point} on this machine, or the case proves nothing"
);
recover(&m).unwrap_or_else(|e| panic!("{at}: recovery refused: {e}"));
hold(&m, at);
recover(&m).unwrap_or_else(|e| panic!("{at}: the second recovery refused: {e}"));
hold(&m, &format!("{at}, recovered twice"));
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_killed_with_nobody_to_ask_is_recovered_from_the_record() {
for point in POINTS {
let m = machine(&format!("offline-{}", point.replace('.', "-")));
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let died = fault::killing(point, || {
switch(settled, &Key::new(ProviderId::Claude, "there"))
});
assert_eq!(died.unwrap_err(), point);
let offline = ScriptedApi::new();
let ctx = m.ctx.clone().with_scripted_api(Arc::clone(&offline));
offline.token_trouble("access-here-refresh", Trouble::Offline);
offline.token_trouble("access-there-refresh", Trouble::Offline);
let decided = settle(&ctx, Permit::for_a_test(), None).is_ok();
assert!(
decided,
"{point}: recovery should read the live login's fingerprint rather than \
needing Anthropic"
);
assert_eq!(
offline.calls(),
0,
"{point}: and it should not have asked at all"
);
hold(&m, &format!("{point}, recovered with no network"));
recover(&m).unwrap_or_else(|e| panic!("{point}: recovery refused once back: {e}"));
hold(&m, &format!("{point}, then online"));
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_whose_token_rotated_while_it_was_interrupted_still_needs_anthropic() {
let m = machine("rotated-offline");
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let died = fault::killing("switch.park_recorded", || {
switch(settled, &Key::new(ProviderId::Claude, "there"))
});
assert_eq!(died.unwrap_err(), "switch.park_recorded");
m.mem.live().plant(
&m.service,
&json!({"claudeAiOauth": {
"refreshToken": "rotated-since",
"accessToken": "access-here-refresh",
"expiresAt": (NOW + 3600) * 1000,
"refreshTokenExpiresAt": (NOW + 30 * 86_400) * 1000,
}})
.to_string(),
);
let before = m.mem.vault().services();
let live_before = m.mem.live().peek(&m.service);
let offline = ScriptedApi::new();
let ctx = m.ctx.clone().with_scripted_api(Arc::clone(&offline));
offline.token_trouble("access-here-refresh", Trouble::Offline);
assert!(
settle(&ctx, Permit::for_a_test(), None).is_err(),
"with nothing to read off and nobody to ask, this must not be guessed at"
);
assert_eq!(m.mem.vault().services(), before, "nothing may be deleted");
assert_eq!(m.mem.live().peek(&m.service), live_before);
assert!(
journal::pending(&ctx),
"and the record is kept for a later run"
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_between_two_organisations_is_settled_by_the_organisation_anthropic_names() {
let m = machine("two-organisations-rotated");
let team = provider::Owner {
account_uuid: "here".into(),
email: "here@example.com".into(),
organization_uuid: "org-team".into(),
};
m.api.owned_by("access-team-refresh", team.clone());
let mut state = state::load(&m.ctx).expect("state");
let team_id = state::new_id(ProviderId::Claude, &team);
let service = park::reserve(&m.ctx, Permit::for_a_test(), &team_id).expect("a free name");
let parked = park::store_at(
&m.ctx,
Permit::for_a_test(),
provider::document(ProviderId::Claude),
&service,
&harness::oauth("team-refresh", 30),
)
.expect("parked");
state.upsert(in_organisation("team", "here", "org-team", Some(parked)));
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let to_team = Key::new(ProviderId::Claude, "team");
let died = fault::killing("switch.installed", || switch(settled, &to_team));
assert_eq!(died.unwrap_err(), "switch.installed");
let rotated = json!({"claudeAiOauth": {
"refreshToken": "rotated-since",
"accessToken": "access-team-refresh",
"expiresAt": (NOW + 3600) * 1000,
"refreshTokenExpiresAt": (NOW + 30 * 86_400) * 1000,
}});
m.mem.live().plant(&m.service, &rotated.to_string());
let (settled, recovered) = settle(&m.ctx, Permit::for_a_test(), None).expect("settled");
assert!(
recovered.is_some_and(|r| r.finished),
"the switch had landed"
);
let claude = ProviderId::Claude;
assert_eq!(
settled
.state
.account_in_use(claude)
.map(|account| account.label.as_str()),
Some("team")
);
assert_eq!(
settled.state.in_use(claude).map(|r| r.login.clone()),
Some(provider::document(claude).fingerprint(&rotated)),
"the login Anthropic named, not the one the switch installed"
);
}
type Leave = fn(&harness::Machine) -> (Context, Arc<ScriptedApi>);
const AFTERWARDS: [(&str, Leave); 9] = [
("as it was", |m| (m.ctx.clone(), Arc::clone(&m.api))),
("signed out", |m| {
m.fault_live(Fault::Vanish);
(m.ctx.clone(), Arc::clone(&m.api))
}),
("renewed since", |m| {
m.sign_in(&login_of(m, "here", "renewed-since"));
(m.ctx.clone(), Arc::clone(&m.api))
}),
("park unreadable", |m| {
unreadable(m);
(m.ctx.clone(), Arc::clone(&m.api))
}),
("renewed since and park unreadable", |m| {
m.sign_in(&login_of(m, "here", "renewed-since"));
unreadable(m);
(m.ctx.clone(), Arc::clone(&m.api))
}),
("offline", unreachable),
("renewed since and offline", |m| {
m.sign_in(&login_of(m, "here", "renewed-since"));
unreachable(m)
}),
("record damaged", |m| {
if recorded(m, "park_service").is_some() {
std::fs::write(
home::dir(&m.ctx).join("journal.json"),
"{\"started_at\": 17",
)
.expect("written");
}
(m.ctx.clone(), Arc::clone(&m.api))
}),
("recorded for another slot", |m| {
let path = home::dir(&m.ctx).join("journal.json");
if let Ok(raw) = std::fs::read_to_string(&path) {
let mut record: Value = serde_json::from_str(&raw).expect("a record");
record["slot"] = json!("somewhere else");
std::fs::write(&path, record.to_string()).expect("written");
}
(m.ctx.clone(), Arc::clone(&m.api))
}),
];
fn unreadable(m: &harness::Machine) {
if let Some(park) = recorded(m, "park_service") {
m.mem
.vault()
.fault(&park, Fault::Unreadable("locked".into()));
}
}
fn unreachable(m: &harness::Machine) -> (Context, Arc<ScriptedApi>) {
let offline = ScriptedApi::new();
for refresh in ["here-refresh", "there-refresh", "renewed-since"] {
offline.token_trouble(&format!("access-{refresh}"), Trouble::Offline);
}
let ctx = m.ctx.clone().with_scripted_api(Arc::clone(&offline));
(ctx, offline)
}
fn recorded(m: &harness::Machine, field: &str) -> Option<String> {
let raw = std::fs::read_to_string(home::dir(&m.ctx).join("journal.json")).ok()?;
let record: Value = serde_json::from_str(&raw).ok()?;
record[field].as_str().map(str::to_owned)
}
type Everything = (
Vec<(String, Vec<u8>)>,
Vec<(String, Option<String>)>,
Option<String>,
);
fn everything(m: &harness::Machine) -> Everything {
let mut files: Vec<(String, Vec<u8>)> = std::fs::read_dir(home::dir(&m.ctx))
.expect("a Pitboard home")
.map(|entry| {
let path = entry.expect("an entry").path();
let body = std::fs::read(&path).unwrap_or_default();
(path.display().to_string(), body)
})
.collect();
files.sort();
let vault = m.mem.vault();
let parked = vault
.services()
.into_iter()
.map(|service| {
let held = vault.peek(&service);
(service, held)
})
.collect();
let (store, service) = m.live_store();
(files, parked, store.peek(&service))
}
mod a_read_says_a_switch_is_waiting_exactly_where_settling_it_is_refused {
use super::*;
each_tool_and_step!(reads_as_settling_refuses);
}
fn reads_as_settling_refuses(case: &str, make: Make, point: &'static str) {
for (left, leave) in AFTERWARDS {
let at = format!("{case}, {left}");
let m = make(&format!(
"read-{}-{}",
point.replace('.', "-"),
left.replace(' ', "-")
));
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let died = fault::killing(point, || switch(settled, &m.key("there")));
assert_eq!(died.unwrap_err(), point, "{at}");
let (ctx, api) = leave(&m);
let state = state::load(&ctx).expect("an account list");
let before = everything(&m);
let asked = stuck(&ctx, &state, Asking::Service).map(|e| e.to_string());
let calls = api.calls();
let unasked = stuck(&ctx, &state, Asking::Nobody).map(|e| e.to_string());
assert_eq!(api.calls(), calls, "{at}: asking nobody asks nobody");
assert_eq!(everything(&m), before, "{at}: a read changes nothing");
let refused = match settle(&ctx, Permit::for_a_test(), None) {
Err(e) if e.code() == "recovery_undetermined" => Some(e.to_string()),
_ => None,
};
assert_eq!(asked, refused, "{at}");
assert!(
unasked.is_none() || unasked == asked,
"{at}: {unasked:?} where a read that asks says {asked:?}"
);
if m.which == ProviderId::Codex {
assert_eq!(unasked, asked, "{at}: a Codex login names its account");
}
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn enrolling_killed_between_the_write_and_the_record_leaves_nothing_unnamed() {
for point in ["enroll.park_stored", "enroll.park_recorded"] {
let m = machine(&point.replace('.', "-"));
m.api.owned_by("access-third-refresh", owner("third"));
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0;
let login = planted(
&m.ctx,
Permit::for_a_test(),
provider::document(ProviderId::Claude),
document("third-refresh"),
)
.expect("a sign-in");
let died = fault::killing(point, || {
enroll(settled, &Key::new(ProviderId::Claude, "third"), Some(login))
});
assert_eq!(died.unwrap_err(), point);
recover(&m).unwrap_or_else(|e| panic!("{point}: recovery refused: {e}"));
hold(&m, point);
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn signing_in_again_killed_at_any_step_recovers_to_something_whole() {
let machines: [(&str, Make); 2] = [("claude", machine), ("codex", codex_machine)];
for (tool, make) in machines {
for point in ["enroll.installed", "enroll.recorded"] {
let m = make(&format!("again-{}", point.replace('.', "-")));
let login = signed_in(&m, "here", "here-refresh-2");
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
let died = fault::killing(point, || enroll(settled, &m.key("here"), Some(login)));
assert_eq!(
died.unwrap_err(),
point,
"{tool}: the sign-in must reach {point} on this machine, or the case proves \
nothing"
);
let at = format!("{tool}, {point}");
recover(&m).unwrap_or_else(|e| panic!("{at}: recovery refused: {e}"));
hold(&m, &at);
let live = m.live().expect("a login in use");
assert_eq!(
provider::document(m.which).fingerprint(&live),
store::fingerprint("here-refresh-2"),
"{at}: the new login is the one in use"
);
recover(&m).unwrap_or_else(|e| panic!("{at}: the second recovery refused: {e}"));
hold(&m, &format!("{at}, recovered twice"));
}
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_whose_login_was_removed_again_does_not_report_a_switch() {
let m = machine("did-not-hold");
let parked_before = m.mem.vault().services();
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
m.mem.live().fault(&m.service, Fault::DeletedAfterWrite);
let failed = switch(settled, &Key::new(ProviderId::Claude, "there"))
.expect_err("the login did not stay");
assert!(
matches!(failed, Error::SwitchDidNotHold { .. }),
"got {failed:?}"
);
let state = state::load(&m.ctx).expect("state");
assert!(
state
.get(&Key::new(ProviderId::Claude, "here"))
.expect("account")
.parked
.is_some(),
"the outgoing login was parked and stays parked"
);
assert!(
state
.get(&Key::new(ProviderId::Claude, "there"))
.expect("account")
.parked
.is_some(),
"the incoming login is not discarded over a switch that did not stand"
);
assert!(m.mem.vault().services().len() > parked_before.len());
assert!(
!journal::pending(&m.ctx),
"and there is nothing half-done to finish"
);
hold(&m, "did not hold");
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_switch_that_cannot_read_the_store_back_keeps_every_copy_and_its_record() {
let m = machine("unverified");
let before = m.mem.live().peek(&m.service).expect("a live login");
let parked_before = m.mem.vault().services();
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover yet")
.0;
m.mem.live().fault(&m.service, Fault::LocksOnWrite);
let failed = switch(settled, &Key::new(ProviderId::Claude, "there"))
.expect_err("a keychain that locked partway");
assert!(
matches!(failed, Error::SwitchUnverified { .. }),
"got {failed:?}"
);
assert!(
journal::pending(&m.ctx),
"the record of intent stays, because nobody can say what happened"
);
assert_eq!(
m.mem.live().peek(&m.service).as_deref(),
Some(before.as_str()),
"and nothing was written"
);
let state = state::load(&m.ctx).expect("state");
assert!(
state.discarded.is_empty(),
"nothing may be listed for deletion on a guess"
);
assert!(
m.mem.vault().services().len() >= parked_before.len(),
"and no parked login was thrown away"
);
m.mem.live().heal_all();
recover(&m).expect("recovery once the keychain is unlocked");
hold(&m, "unverified, then unlocked");
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn renewing_killed_between_the_write_and_the_record_leaves_nothing_unnamed() {
let m = machine("renew-park-stored");
let mut state = state::load(&m.ctx).expect("state");
let park = state
.get(&Key::new(ProviderId::Claude, "there"))
.expect("account")
.parked
.clone()
.expect("parked");
m.mem.vault().plant(
&park.service,
&json!({
"accessToken": "access-there-refresh",
"refreshToken": "there-refresh",
"expiresAt": (NOW - 60) * 1000,
"refreshTokenExpiresAt": (NOW + 30 * 86_400) * 1000
})
.to_string(),
);
state.park(
&Key::new(ProviderId::Claude, "there"),
park::describe(
provider::document(ProviderId::Claude),
&park.service,
NOW,
&json!({
"refreshToken": "there-refresh",
"expiresAt": (NOW - 60) * 1000,
"refreshTokenExpiresAt": (NOW + 30 * 86_400) * 1000
}),
),
);
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
m.api.renews(
"there-refresh",
Renewed {
access_token: "access-fresh".into(),
refresh_token: Some("fresh".into()),
expires_in: 3600,
refresh_token_expires_in: Some(30 * 86_400),
scopes: None,
at: None,
},
);
let died = fault::killing("renew.park_stored", || {
renew_parked(&m.ctx, Permit::for_a_test())
});
assert_eq!(died.unwrap_err(), "renew.park_stored");
recover(&m).expect("recovery");
hold(&m, "renew.park_stored");
let kept = state::load(&m.ctx)
.expect("state")
.get(&Key::new(ProviderId::Claude, "there"))
.and_then(|a| a.parked.clone())
.expect("`there` still holds a login");
assert_eq!(
kept.login,
provider::claude::document::fingerprint_of(&json!({"refreshToken": "fresh"})),
"the fresh login is kept and the spent one dropped, not the other way round"
);
assert_eq!(m.mem.vault().services(), vec![kept.service]);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_renewal_that_cannot_record_its_answer_keeps_it_for_the_next_run() {
use crate::host::fs::testing;
let m = machine("renew-save-fails");
let key = Key::new(ProviderId::Claude, "there");
let mut state = state::load(&m.ctx).expect("state");
let held = state.get(&key).unwrap().parked.clone().unwrap();
let lapsed = json!({
"accessToken": "access-there-refresh",
"refreshToken": "there-refresh",
"expiresAt": (NOW - 60) * 1000,
"refreshTokenExpiresAt": (NOW + 30 * 86_400) * 1000
});
m.mem.vault().plant(&held.service, &lapsed.to_string());
state.park(
&key,
park::describe(
provider::document(ProviderId::Claude),
&held.service,
NOW,
&lapsed,
),
);
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
m.api.renews(
"there-refresh",
Renewed {
access_token: "access-fresh".into(),
refresh_token: Some("fresh".into()),
expires_in: 3600,
refresh_token_expires_in: Some(30 * 86_400),
scopes: None,
at: None,
},
);
let home = home::dir(&m.ctx);
let locked = home.clone();
let outcomes = fault::meanwhile(
"renew.park_stored",
move || {
testing::deny_changes(&locked).expect("read-only");
},
|| renew_parked(&m.ctx, Permit::for_a_test()),
);
testing::allow_changes(&home).expect("changeable again");
assert!(
outcomes
.iter()
.any(|(_, r)| matches!(r, Renewal::Failed(_))),
"the save failed, and says so"
);
recover(&m).expect("the next change");
let kept = state::load(&m.ctx)
.expect("state")
.get(&key)
.and_then(|a| a.parked.clone())
.expect("`there` still holds a login");
assert_eq!(
kept.login,
provider::claude::document::fingerprint_of(&json!({"refreshToken": "fresh"})),
);
hold(&m, "after a renewal that could not be recorded");
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn forgetting_killed_after_the_record_still_deletes_the_park() {
let m = machine("forget-recorded");
let settled = settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0;
let died = fault::killing("forget.recorded", || {
forget(settled, &Key::new(ProviderId::Claude, "there"))
});
assert_eq!(died.unwrap_err(), "forget.recorded");
recover(&m).expect("recovery");
hold(&m, "forget.recorded");
assert!(
m.mem.vault().services().is_empty(),
"a forgotten account's login is deleted, not left in the vault"
);
}