use super::harness::{Machine, codex_access, codex_machine, login_of, machine, recover};
use super::*;
use crate::api::scripted::Asked;
use crate::fault;
use crate::service::Permit;
use crate::store::memory::Touch;
use std::cell::RefCell;
use std::collections::BTreeSet;
use std::path::Path;
use std::rc::Rc;
const LIVE: &str = "Claude Code-credentials";
const CLAUDE_FILE: &str = "file .claude/.credentials.json";
const CLAUDE_HERE: &str = "pitboard-park-here-1760000000000";
const CLAUDE_THERE: &str = "pitboard-park-there-1760000000000";
const CODEX_FILE: &str = "file .codex/auth.json";
const AUTH_JSON: &str = "auth.json";
const CODEX_HERE: &str = "pitboard-park-here_user-here-1760000000000";
const CODEX_THERE: &str = "pitboard-park-there_user-there-1760000000000";
struct Pinned {
touched: &'static [(&'static str, &'static str, &'static str)],
asked: Vec<Asked>,
at_journal: &'static [&'static str],
made: &'static [&'static str],
gone: &'static [&'static str],
}
struct Seen {
touched: Vec<Touch>,
asked: Vec<Asked>,
at_journal: Vec<String>,
made: Vec<String>,
gone: Vec<String>,
}
fn pin(m: Machine, what: &str, change: fn(&Machine), pinned: Pinned) {
let tool = m.which.code();
let seen = watch(&m, change);
let touched: Vec<Touch> = pinned
.touched
.iter()
.map(|&(store, call, service)| Touch {
store: store.into(),
call,
service: service.into(),
})
.collect();
assert_eq!(seen.touched, touched, "{tool} {what}: the stores, in order");
assert_eq!(seen.asked, pinned.asked, "{tool} {what}: the service");
assert_eq!(
seen.at_journal, pinned.at_journal,
"{tool} {what}: what was new when the journal was written"
);
assert_eq!(seen.made, pinned.made, "{tool} {what}: what it made");
assert_eq!(seen.gone, pinned.gone, "{tool} {what}: what it removed");
}
fn watch(m: &Machine, change: fn(&Machine)) -> Seen {
let root = m.ctx_home();
let (touched, asked) = (m.mem.trail().len(), m.api.calls());
let before = listing(&root);
let journalled = Rc::new(RefCell::new(before.clone()));
let (seen, at) = (Rc::clone(&journalled), root.clone());
fault::meanwhile(
"switch.journal_written",
move || *seen.borrow_mut() = listing(&at),
|| change(m),
);
let after = listing(&root);
Seen {
touched: m.mem.trail()[touched..]
.iter()
.map(|touch| under(&root, touch))
.collect(),
asked: m.api.asked()[asked..].to_vec(),
at_journal: journalled.borrow().difference(&before).cloned().collect(),
made: after.difference(&before).cloned().collect(),
gone: before.difference(&after).cloned().collect(),
}
}
fn under(root: &Path, touch: &Touch) -> Touch {
let store = match touch.store.strip_prefix("file ") {
Some(path) => format!(
"file {}",
Path::new(path)
.strip_prefix(root)
.expect("a file under the machine's root")
.display()
),
None => touch.store.clone(),
};
Touch {
store,
..touch.clone()
}
}
fn listing(root: &Path) -> BTreeSet<String> {
let mut found = BTreeSet::new();
let mut left = vec![root.to_path_buf()];
while let Some(dir) = left.pop() {
for entry in std::fs::read_dir(&dir).expect("a directory") {
let path = entry.expect("an entry").path();
let name = path
.strip_prefix(root)
.expect("under the root")
.display()
.to_string();
if std::fs::symlink_metadata(&path)
.expect("still there")
.is_dir()
{
found.insert(format!("{name}/"));
left.push(path);
} else {
found.insert(name);
}
}
}
found
}
fn settled(m: &Machine) -> Settled {
settle(&m.ctx, Permit::for_a_test(), None)
.expect("nothing to recover")
.0
}
fn switched(m: &Machine) {
switch(settled(m), &m.key("there")).expect("switched");
}
fn forgotten(m: &Machine) {
forget(settled(m), &m.key("there")).expect("forgotten");
}
fn enrolled(m: &Machine) {
enroll(settled(m), &m.key("third"), None).expect("enrolled");
}
fn recovered(m: &Machine) {
let settled = settled(m);
let died = fault::killing("switch.park_recorded", || switch(settled, &m.key("there")));
assert_eq!(died.unwrap_err(), "switch.park_recorded");
recover(m).expect("recovered");
}
fn signed_in_as_third(m: Machine) -> Machine {
m.sign_in(&login_of(&m, "third", "third-refresh"));
m
}
#[test]
#[cfg_attr(
windows,
ignore = "W23: Claude Code's Credential Manager store, which a machine in memory plays"
)]
fn a_document_switch_touches_its_stores_in_the_order_it_always_did() {
pin(
machine("pinned-switch"),
"switch",
switched,
Pinned {
touched: &[
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "read", CLAUDE_THERE),
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("vault", "read", CLAUDE_HERE),
("vault", "write", CLAUDE_HERE),
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("live", "write", LIVE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "delete", CLAUDE_THERE),
("live", "contains", LIVE),
(CLAUDE_FILE, "contains", LIVE),
],
asked: vec![Asked::Owner("access-there-refresh".into())],
at_journal: &[
".claude.lock/",
".claude/",
".claude/.oauth_refresh.lock/",
".claude/.storage-write.lock/",
".pitboard/journal.json",
".pitboard/state.lock",
],
made: &[
".claude/",
".pitboard/audit.log",
".pitboard/backups/",
".pitboard/backups/claude.json.1760000000",
".pitboard/state.lock",
],
gone: &[],
},
);
pin(
machine("pinned-forget"),
"forget",
forgotten,
Pinned {
touched: &[
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "delete", CLAUDE_THERE),
],
asked: vec![],
at_journal: &[],
made: &[".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
pin(
signed_in_as_third(machine("pinned-enroll")),
"enroll",
enrolled,
Pinned {
touched: &[
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("live", "read", LIVE),
],
asked: vec![Asked::Owner("access-third-refresh".into())],
at_journal: &[],
made: &[".pitboard/audit.log", ".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
pin(
machine("pinned-recover"),
"recover",
recovered,
Pinned {
touched: &[
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "read", CLAUDE_THERE),
("live", "contains", LIVE),
("live", "read", LIVE),
("live", "contains", LIVE),
("vault", "read", CLAUDE_HERE),
("vault", "write", CLAUDE_HERE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "read", CLAUDE_HERE),
("live", "contains", LIVE),
("live", "read", LIVE),
("vault", "delete", CLAUDE_HERE),
],
asked: vec![Asked::Owner("access-there-refresh".into())],
at_journal: &[
".claude.lock/",
".claude/",
".claude/.oauth_refresh.lock/",
".claude/.storage-write.lock/",
".pitboard/journal.json",
".pitboard/state.lock",
],
made: &[".claude/", ".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
pin(
codex_machine("pinned-switch"),
"switch",
switched,
Pinned {
touched: &[
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "read", CODEX_THERE),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
("vault", "read", CODEX_HERE),
("vault", "write", CODEX_HERE),
("vault", "read", CODEX_HERE),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "write", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "delete", CODEX_THERE),
],
asked: vec![Asked::Usage(codex_access("there-refresh"))],
at_journal: &[".pitboard/journal.json", ".pitboard/state.lock"],
made: &[".pitboard/state.lock"],
gone: &[],
},
);
pin(
codex_machine("pinned-forget"),
"forget",
forgotten,
Pinned {
touched: &[
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "delete", CODEX_THERE),
],
asked: vec![],
at_journal: &[],
made: &[".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
pin(
signed_in_as_third(codex_machine("pinned-enroll")),
"enroll",
enrolled,
Pinned {
touched: &[
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
],
asked: vec![],
at_journal: &[],
made: &[".pitboard/audit.log", ".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
pin(
codex_machine("pinned-recover"),
"recover",
recovered,
Pinned {
touched: &[
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "read", CODEX_THERE),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
(CODEX_FILE, "contains", AUTH_JSON),
("vault", "read", CODEX_HERE),
("vault", "write", CODEX_HERE),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "read", CODEX_HERE),
(CODEX_FILE, "contains", AUTH_JSON),
(CODEX_FILE, "read", AUTH_JSON),
("vault", "delete", CODEX_HERE),
],
asked: vec![Asked::Usage(codex_access("there-refresh"))],
at_journal: &[".pitboard/journal.json", ".pitboard/state.lock"],
made: &[".pitboard/state.lock"],
gone: &[".pitboard/pending"],
},
);
}