use super::{Result, Settled, purge};
use crate::context::Context;
use crate::error::Error;
use crate::provider::{self, LoginKind, ProviderId};
use crate::service::{Permit, Warning};
use crate::state::Account;
use crate::{home, schedule, state};
#[derive(Debug)]
#[non_exhaustive]
pub struct Removed {
pub parks: usize,
pub pending: usize,
pub left: usize,
pub set_aside: usize,
pub home_removed: bool,
pub schedule_removed: bool,
pub warnings: Vec<Warning>,
}
pub(crate) fn uninstall(settled: Settled) -> Result<Removed> {
let Settled {
_exclusive,
mut state,
ctx,
permit,
} = settled;
let mut warnings = Vec::new();
let mut _closed = Vec::new();
for &tool in ProviderId::ALL {
match provider::of(tool) {
LoginKind::Document(_) => {}
LoginKind::Folder(app) => {
if let Some((closed, said)) =
super::folder::before_uninstall(&ctx, permit, &mut state, app)?
{
_closed.push(closed);
warnings.extend(said);
}
}
}
}
match take_away(&ctx, permit, &mut state) {
Ok(removed) => Ok(Removed {
warnings,
..removed
}),
Err(refused) => Err(refused.after(warnings)),
}
}
fn take_away(ctx: &Context, permit: Permit, state: &mut state::State) -> Result<Removed> {
let schedule_removed = remove_schedule(ctx, permit)?;
let held = state.accounts.iter().filter_map(|a| a.parked.as_ref());
let left = held
.clone()
.filter(|park| state.is_foreign(&park.service))
.count();
let parks = held.count() - left;
let asides: Vec<String> = state.aside.iter().map(|aside| aside.name.clone()).collect();
for key in state.accounts.iter().map(Account::key).collect::<Vec<_>>() {
state.remove(&key);
}
state.forget_asides();
state.forget_discarded();
state.forget_in_use();
state::save(ctx, permit, state)?;
let undeleted = purge(ctx, permit, state);
let asides_left = asides
.iter()
.filter(|name| {
state
.discarded
.iter()
.any(|listed| listed.service == **name)
})
.count();
let pending = undeleted + state.renewing.len();
if pending == 0 {
crate::pending::clear(ctx, permit);
}
let home_removed = pending == 0 && remove_home(ctx, permit);
Ok(Removed {
parks: parks.saturating_sub(undeleted - asides_left),
pending,
left,
set_aside: asides.len() - asides_left,
home_removed,
schedule_removed,
warnings: Vec::new(),
})
}
fn remove_schedule(ctx: &Context, permit: Permit) -> Result<bool> {
if !schedule::serves(ctx) {
return Ok(false);
}
match schedule::uninstall(ctx, permit) {
Err(Error::ScheduleUnsupported) => Ok(false),
removed => removed,
}
}
fn remove_home(ctx: &Context, permit: Permit) -> bool {
crate::host::fs::remove_dir_all(permit, &home::dir(ctx)).is_ok()
}
#[cfg(test)]
mod tests {
use super::super::harness::{codex_machine, machine};
use super::super::settle;
use super::*;
#[test]
#[cfg_attr(windows, ignore = "W25: Task Scheduler")]
fn uninstalling_takes_the_renewal_schedule_with_it() {
for make in [machine, codex_machine] {
let m = make("uninstall-schedule");
schedule::install(&m.ctx, Permit::for_a_test()).expect("scheduled");
let removed = uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled");
assert!(removed.schedule_removed);
assert!(removed.home_removed);
assert_eq!(schedule::status(&m.ctx), schedule::Installed::No);
}
}
#[test]
#[cfg_attr(windows, ignore = "W25: Task Scheduler")]
fn a_schedule_that_cannot_be_taken_away_leaves_every_login_where_it_was() {
use crate::host::fs::testing;
for make in [machine, codex_machine] {
let m = make("uninstall-stuck");
schedule::install(&m.ctx, Permit::for_a_test()).expect("scheduled");
let dir = schedule::path(&m.ctx)
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
.expect("where the scheduler keeps it");
testing::deny_changes(&dir).expect("read-only");
let refused = uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
);
testing::allow_changes(&dir).expect("changeable again");
let Err(error) = refused else {
panic!("{:?}: uninstalled with the schedule still there", m.which);
};
assert_eq!(error.code(), "home_unwritable");
let state = state::load(&m.ctx).expect("the account list");
assert_eq!(state.accounts.len(), 2, "{:?}", m.which);
let there = state.get(&m.key("there")).expect("`there` is enrolled");
let park = there.parked.as_ref().expect("`there` is still parked");
crate::park::load(&m.ctx, provider::document(m.which), &m.key("there"), park)
.expect("its parked login is kept");
assert!(home::dir(&m.ctx).is_dir());
assert!(matches!(
schedule::status(&m.ctx),
schedule::Installed::Yes { .. }
));
}
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn uninstalling_where_nothing_is_scheduled_is_not_a_failure() {
let m = machine("uninstall-unscheduled");
let removed = uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled");
assert!(!removed.schedule_removed);
assert!(removed.home_removed);
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn uninstalling_keeps_a_renewed_login_not_saved_yet() {
use super::super::harness::{lapsed, owner, renews};
use crate::store::memory::Fault;
let m = machine("uninstall-renewing");
m.sign_in(&lapsed("here-renewed"));
renews(&m, "here-renewed", "here-again");
m.api.owned_by("access-here-again", owner("here"));
let pitboard = crate::service::Pitboard::new(m.ctx.clone());
let died = crate::fault::killing("refresh.exchanged", || pitboard.switch_to("there"));
assert_eq!(died.err().as_deref(), Some("refresh.exchanged"));
let copies = |m: &super::super::harness::Machine| -> Vec<String> {
m.mem
.vault()
.services()
.into_iter()
.filter(|service| service.contains("-renewing-"))
.collect()
};
let [copy] = copies(&m).try_into().expect("one copy");
m.fault_live(Fault::Locked);
let removed = uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled");
assert!(!removed.home_removed);
assert_eq!(removed.pending, 1);
assert!(m.mem.vault().peek(©).is_some());
assert!(state::load(&m.ctx).expect("state").names(©));
m.mem.live().heal_all();
let removed = uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled");
assert!(removed.home_removed);
assert_eq!(copies(&m), Vec::<String>::new());
assert_eq!(
m.live()
.and_then(|stored| stored["claudeAiOauth"]["refreshToken"]
.as_str()
.map(str::to_owned)),
Some("here-again".into())
);
}
#[test]
#[cfg_attr(
windows,
ignore = "Claude Desktop is macOS only: Windows makes and removes no folder"
)]
fn uninstalling_counts_the_set_aside_folders_it_deleted_and_keeps_the_rest_listed() {
use crate::host::fs::testing;
use crate::provider::ProviderId;
let m = machine("uninstall-asides");
let parks = crate::store::folder::parks_dir(&m.ctx);
let names = ["pitboard-aside-1", "pitboard-aside-2"];
let mut state = state::load(&m.ctx).expect("state");
for name in names {
std::fs::create_dir_all(parks.join(name).join("items")).expect("a folder");
std::fs::write(parks.join(name).join("items/Cookies"), b"a jar").expect("a jar");
state.aside.push(state::Aside {
name: name.into(),
tool: ProviderId::Desktop,
at: 1,
why: state::SetAside::InTheWay,
account: None,
login: None,
});
}
state::save(&m.ctx, Permit::for_a_test(), &state).expect("saved");
let uninstalled = || {
uninstall(
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled")
};
testing::allow_changes(&parks).expect("open to others");
let removed = uninstalled();
assert_eq!(
(removed.parks, removed.set_aside, removed.pending),
(1, 0, 2)
);
assert!(!removed.home_removed);
for name in names {
assert!(parks.join(name).join("items/Cookies").exists(), "{name}");
assert!(state::load(&m.ctx).expect("state").names(name), "{name}");
}
testing::make_private(&parks).expect("private");
let removed = uninstalled();
assert_eq!(removed.pending, 0);
assert!(removed.home_removed);
}
#[test]
#[cfg_attr(windows, ignore = "W25: Task Scheduler")]
fn uninstalling_another_home_leaves_the_schedule_alone() {
let m = machine("uninstall-elsewhere");
schedule::install(&m.ctx, Permit::for_a_test()).expect("scheduled");
let elsewhere = m
.ctx
.clone()
.with_pitboard_home(m.ctx_home().join("elsewhere"));
let removed = uninstall(
settle(&elsewhere, Permit::for_a_test(), None)
.expect("nothing to recover")
.0,
)
.expect("uninstalled");
assert!(!removed.schedule_removed);
assert!(matches!(
schedule::status(&m.ctx),
schedule::Installed::Yes { .. }
));
}
}