#![allow(
clippy::disallowed_methods,
reason = "the one module that reads this process's environment"
)]
use crate::host::{Host, Os};
use crate::provider::ProviderId;
use crate::provider::claude::api::{Anthropic, Api};
use crate::provider::codex::api::{Network as OpenAiNetwork, OpenAi};
use crate::time::{Clock, SystemClock};
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;
use std::sync::Arc;
const READ: &[&str] = &[
"HOME",
"USER",
"PATH",
"SHELL",
"PITBOARD_HOME",
"PITBOARD_NO_ARGV",
"PITBOARD_API_BASE",
"PITBOARD_CLAUDE",
"PITBOARD_CODEX",
"CLAUDE_CONFIG_DIR",
"CLAUDE_SECURESTORAGE_CONFIG_DIR",
"CLAUDE_CODE_CUSTOM_OAUTH_URL",
"CLAUDE_CODE_HOVER_REST",
"CLAUDE_CODE_FORCE_WINDOWS_CREDMAN",
"CLAUDE_CODE_GIT_BASH_PATH",
"WSL_DISTRO_NAME",
"CODEX_HOME",
"CODEX_INSTALL_DIR",
"NO_COLOR",
"XPC_SERVICE_NAME",
"SUDO_UID",
"USERPROFILE",
"USERNAME",
"APPDATA",
"LOCALAPPDATA",
"ProgramData",
"SystemRoot",
"PATHEXT",
"TEMP",
"TMP",
"OneDrive",
"OneDriveCommercial",
"OneDriveConsumer",
];
#[cfg(any(test, feature = "test-support"))]
const READ_BY_BUILDS_FOR_TESTS: &[&str] = &[TEST_CLAUDE_DESKTOP];
#[cfg(not(any(test, feature = "test-support")))]
const READ_BY_BUILDS_FOR_TESTS: &[&str] = &[];
#[cfg(any(test, feature = "test-support"))]
const TEST_CLAUDE_DESKTOP: &str = "PITBOARD_TEST_CLAUDE_DESKTOP";
pub(crate) fn variables() -> impl Iterator<Item = &'static str> {
READ.iter()
.chain(READ_BY_BUILDS_FOR_TESTS.iter())
.chain(crate::settings::OVERRIDING_ENV.iter())
.chain(crate::proxy::NAMING.iter())
.chain(crate::proxy::EXEMPTING.iter())
.copied()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Environment {
os: Os,
variables: HashMap<OsString, (OsString, OsString)>,
}
impl Default for Environment {
fn default() -> Environment {
Environment::on(crate::host::OS, std::iter::empty::<(OsString, OsString)>())
}
}
impl Environment {
pub fn of_this_process() -> Environment {
std::env::vars_os().collect()
}
pub(crate) fn on<K: Into<OsString>, V: Into<OsString>>(
os: Os,
pairs: impl IntoIterator<Item = (K, V)>,
) -> Environment {
let mut variables = HashMap::new();
for (name, value) in pairs {
let (name, value) = (name.into(), value.into());
let key = os.variable_key(&name);
match os {
Os::MacOs | Os::Linux => {
variables.insert(key, (name, value));
}
Os::Windows => {
variables.entry(key).or_insert((name, value));
}
}
}
Environment { os, variables }
}
pub(crate) fn os(&self) -> Os {
self.os
}
pub(crate) fn path(&self, name: &str) -> Option<&OsStr> {
debug_assert!(
variables().any(|read| read == name),
"{name} is read but not listed in context::READ, so the tests would take it \
from whoever runs them"
);
self.variables
.get(&self.os.variable_key(OsStr::new(name)))
.map(|(_, value)| value.as_os_str())
}
pub(crate) fn text(&self, name: &str) -> Option<&str> {
self.path(name).and_then(OsStr::to_str)
}
pub(crate) fn spelling(&self, name: &str) -> Option<&OsStr> {
self.path(name)?;
self.variables
.get(&self.os.variable_key(OsStr::new(name)))
.map(|(spelled, _)| spelled.as_os_str())
}
pub(crate) fn set(&self, name: &str) -> bool {
self.text(name).is_some_and(|v| !v.is_empty())
}
pub(crate) fn named_folder(&self, name: &str) -> Option<PathBuf> {
self.path(name)
.filter(|folder| !folder.is_empty())
.map(PathBuf::from)
}
pub(crate) fn home(&self) -> PathBuf {
self.path(self.os.home_variable())
.map(PathBuf::from)
.or_else(crate::host::user::home)
.unwrap_or_default()
}
pub fn pitboard_home(&self) -> PathBuf {
self.path("PITBOARD_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| self.os.default_pitboard_home(&self.home()))
}
#[cfg_attr(
windows,
allow(
dead_code,
reason = "what the login shell is given, and Windows has none (`host::login_path`)"
)
)]
pub(crate) fn iter(&self) -> impl Iterator<Item = (&OsStr, &OsStr)> {
self.variables
.values()
.map(|(name, value)| (name.as_os_str(), value.as_os_str()))
}
}
#[cfg(test)]
fn unit_test_sentinel() -> PathBuf {
let thread = std::thread::current();
let test = thread.name().map_or_else(
|| format!("{:?}", thread.id()),
|name| {
name.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect()
},
);
std::env::temp_dir()
.join(format!("pitboard-unit-test-{}", std::process::id()))
.join(test)
}
impl<K: Into<OsString>, V: Into<OsString>> FromIterator<(K, V)> for Environment {
fn from_iter<I: IntoIterator<Item = (K, V)>>(pairs: I) -> Environment {
Environment::on(crate::host::OS, pairs)
}
}
#[derive(Debug, Clone)]
pub struct ScratchClaude {
pub data: PathBuf,
pub app: PathBuf,
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn refuse_a_real_claude(path: &std::path::Path) {
use crate::host::user;
assert!(
!path
.components()
.any(|part| part == std::path::Component::ParentDir),
"a test named {} for Claude through `..`, which may lead anywhere once a folder is made: \
give it a folder of its own",
path.display()
);
#[cfg(test)]
let fake = testing::guarded_home();
#[cfg(not(test))]
let fake: Option<PathBuf> = None;
let real = match crate::host::OS {
Os::MacOs | Os::Linux => {
["Library/Application Support", "Applications/Claude.app"]
.into_iter()
.any(|below| {
user::lies_in_the_accounts_own(path, below)
|| fake
.as_ref()
.is_some_and(|home| crate::host::lies_in(path, &home.join(below)))
})
|| crate::host::lies_in(path, std::path::Path::new("/Applications/Claude.app"))
}
Os::Windows => {
let (mut roaming, mut local) = user::app_data_folders(true);
if let Some(home) = &fake {
roaming.push(home.join("AppData").join("Roaming"));
local.push(home.join("AppData").join("Local"));
}
crate::host::claude_desktop_on_windows(&roaming, &local)
.iter()
.any(|place| crate::host::lies_in(path, place))
}
};
assert!(
!real,
"a test named {} for Claude, which is, holds or lies in where this account's own \
Claude Desktop keeps its data or its app: give it a folder of its own",
path.display()
);
}
#[cfg(test)]
pub(crate) mod testing {
use std::cell::RefCell;
use std::path::{Path, PathBuf};
thread_local! {
static GUARDED: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
}
pub(crate) fn also_guarding(home: &Path) -> AlsoGuarding {
GUARDED.with(|guarded| *guarded.borrow_mut() = Some(home.to_path_buf()));
AlsoGuarding(())
}
pub(crate) struct AlsoGuarding(());
impl Drop for AlsoGuarding {
fn drop(&mut self) {
GUARDED.with(|guarded| guarded.borrow_mut().take());
}
}
pub(super) fn guarded_home() -> Option<PathBuf> {
GUARDED.with(|guarded| guarded.borrow().clone())
}
}
#[derive(Clone, Debug)]
pub struct Context {
pub(crate) os: Os,
pub(crate) home: PathBuf,
pub(crate) pitboard_home: PathBuf,
pub(crate) claude_config_dir: Option<String>,
pub(crate) secure_storage_dir: Option<String>,
pub(crate) user: Option<String>,
pub(crate) custom_oauth: bool,
pub(crate) overriding_auth: Vec<String>,
pub(crate) argv_fallback: bool,
pub(crate) caller: String,
pub(crate) programs: std::collections::BTreeMap<ProviderId, PathBuf>,
pub(crate) api_base: Option<String>,
pub(crate) proxy: crate::proxy::Proxies,
pub(crate) agent: crate::api::Agents,
pub(crate) hover_rest: bool,
pub(crate) credential_manager_forced: bool,
pub(crate) codex_home: Option<String>,
pub(crate) profile: Option<PathBuf>,
pub(crate) local_app_data: Option<PathBuf>,
pub(crate) sid: Option<String>,
pub(crate) one_drive: Vec<PathBuf>,
pub(crate) schedule_program: Option<PathBuf>,
pub(crate) search_path: Option<OsString>,
pub(crate) installer_places: std::collections::BTreeMap<ProviderId, Vec<PathBuf>>,
pub(crate) path_ext: Option<OsString>,
pub(crate) git_bash: Option<PathBuf>,
pub(crate) wsl_distro: Option<String>,
pub(crate) scheduled_job: Option<String>,
pub(crate) marked_as_scheduled: bool,
pub(crate) sudo: bool,
pub(crate) clock: Arc<dyn Clock>,
pub(crate) locks: crate::lock::Locks,
pub(crate) host: Arc<dyn Host>,
pub(crate) api: Arc<dyn Api>,
pub(crate) openai: Arc<dyn OpenAi>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) sign_in_script: Option<Arc<dyn crate::switch::SignInScript>>,
#[cfg(any(test, feature = "test-support"))]
scratch_claude: Option<ScratchClaude>,
#[cfg(any(test, feature = "test-support"))]
awaited_every: Option<std::time::Duration>,
#[cfg(test)]
unknown_machine: bool,
#[cfg(any(test, feature = "test-support"))]
pub(crate) warm_script: Option<Arc<dyn crate::duty::warm::run::WarmScript>>,
}
fn programs(
named: impl Fn(ProviderId, crate::provider::Cli) -> PathBuf,
) -> std::collections::BTreeMap<ProviderId, PathBuf> {
ProviderId::ALL
.iter()
.filter_map(|&tool| Some((tool, named(tool, tool.cli()?))))
.collect()
}
impl Context {
pub(crate) fn now(&self) -> i64 {
self.clock.now()
}
pub(crate) fn now_millis(&self) -> i64 {
self.clock.now_millis()
}
pub(crate) fn zone(&self) -> jiff::tz::TimeZone {
self.clock.zone()
}
pub(crate) fn home(&self) -> &std::path::Path {
&self.home
}
pub(crate) fn default_pitboard_home(&self) -> PathBuf {
self.os.default_pitboard_home(&self.home)
}
pub(crate) fn host(&self) -> &dyn Host {
self.host.as_ref()
}
pub(crate) fn openai(&self) -> &dyn OpenAi {
self.openai.as_ref()
}
pub(crate) fn api(&self) -> &dyn Api {
self.api.as_ref()
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn new(home: PathBuf) -> Context {
Context {
os: crate::host::OS,
pitboard_home: crate::host::OS.default_pitboard_home(&home),
home,
claude_config_dir: None,
secure_storage_dir: None,
user: None,
custom_oauth: false,
argv_fallback: true,
overriding_auth: Vec::new(),
caller: "unknown".into(),
programs: programs(|tool, _| PathBuf::from(tool.program())),
api_base: None,
proxy: crate::proxy::Proxies::default(),
agent: crate::api::Agents::default(),
hover_rest: false,
credential_manager_forced: false,
codex_home: None,
profile: None,
local_app_data: None,
sid: None,
one_drive: Vec::new(),
schedule_program: None,
search_path: None,
installer_places: std::collections::BTreeMap::new(),
path_ext: None,
git_bash: None,
wsl_distro: None,
scheduled_job: None,
marked_as_scheduled: false,
sudo: false,
clock: Arc::new(SystemClock),
locks: crate::lock::Locks::CLAUDE_CODE,
host: crate::host::current(),
api: Arc::new(Anthropic),
openai: Arc::new(OpenAiNetwork),
#[cfg(any(test, feature = "test-support"))]
sign_in_script: None,
#[cfg(any(test, feature = "test-support"))]
scratch_claude: None,
#[cfg(any(test, feature = "test-support"))]
awaited_every: None,
#[cfg(test)]
unknown_machine: false,
#[cfg(any(test, feature = "test-support"))]
warm_script: None,
}
}
pub fn with_codex_home(mut self, dir: String) -> Context {
self.codex_home = Some(dir).filter(|d| !d.is_empty());
self
}
pub(crate) fn codex_home(&self) -> Option<&str> {
self.codex_home.as_deref()
}
#[cfg(test)]
pub(crate) fn with_profile(mut self, profile: PathBuf) -> Context {
self.profile = Some(profile);
self
}
pub fn with_pitboard_home(mut self, dir: PathBuf) -> Context {
self.pitboard_home = dir;
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_claude_config_dir(mut self, dir: String) -> Context {
self.claude_config_dir = Some(dir);
self
}
pub(crate) fn signed_in_at(&self, dir: &std::path::Path) -> Context {
let mut ctx = self.clone();
ctx.claude_config_dir = Some(dir.to_string_lossy().into_owned());
ctx
}
#[cfg(test)]
pub(crate) fn with_secure_storage_dir(mut self, dir: String) -> Context {
self.secure_storage_dir = Some(dir);
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_argv_fallback(mut self, allowed: bool) -> Context {
self.argv_fallback = allowed;
self
}
pub(crate) fn custom_oauth(&self) -> bool {
self.custom_oauth
}
pub fn claude_program(&self) -> &std::path::Path {
&self.programs[&ProviderId::Claude]
}
pub(crate) fn argv_fallback(&self) -> bool {
self.argv_fallback
}
pub(crate) fn overriding_auth(&self) -> &[String] {
&self.overriding_auth
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_caller(mut self, caller: String) -> Context {
self.caller = caller;
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_user(mut self, user: String) -> Context {
self.user = Some(user);
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_claude_program(mut self, program: PathBuf) -> Context {
self.programs.insert(ProviderId::Claude, program);
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_codex_program(mut self, program: PathBuf) -> Context {
self.programs.insert(ProviderId::Codex, program);
self
}
pub fn codex_program(&self) -> &std::path::Path {
&self.programs[&ProviderId::Codex]
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_schedule_program(mut self, program: PathBuf) -> Context {
self.schedule_program = Some(program);
self
}
pub fn schedule_program(&self) -> Option<&std::path::Path> {
self.schedule_program.as_deref()
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_search_path(mut self, path: String) -> Context {
self.search_path = Some(path.into());
self
}
pub(crate) fn lookup(&self) -> crate::host::program::Lookup {
crate::host::program::Lookup::new(self.os, self.path_ext.as_deref())
}
pub(crate) fn search_path(&self) -> OsString {
self.search_path
.clone()
.or_else(|| std::env::var_os("PATH"))
.unwrap_or_default()
}
pub(crate) fn scheduled_job(&self) -> Option<&str> {
self.scheduled_job.as_deref()
}
pub(crate) fn sudo(&self) -> bool {
self.sudo
}
#[cfg(test)]
pub(crate) fn with_scheduled_job(mut self, label: String) -> Context {
self.scheduled_job = Some(label);
self
}
pub fn started_by_the_schedule(mut self) -> Context {
self.marked_as_scheduled = true;
self
}
pub(crate) fn marked_as_scheduled(&self) -> bool {
self.marked_as_scheduled
}
pub(crate) fn with_proxy_variables_over(mut self, given: &[(String, String)]) -> Context {
let os = self.os;
let key = |name: &str| os.variable_key(OsStr::new(name));
let own = self
.proxy
.variables()
.iter()
.filter(|(name, _)| !given.iter().any(|(over, _)| key(over) == key(name)))
.map(|(name, value)| ((*name).to_string(), value.clone()));
let env = Environment::on(os, given.iter().cloned().chain(own));
self.proxy = crate::proxy::Proxies::read(&env);
self.agent = crate::api::Agents::default();
self
}
pub fn program_for(&self, tool: ProviderId) -> Option<&std::path::Path> {
self.programs.get(&tool).map(PathBuf::as_path)
}
pub(crate) fn scratch_claude(&self) -> Option<&ScratchClaude> {
#[cfg(any(test, feature = "test-support"))]
{
self.scratch_claude.as_ref()
}
#[cfg(not(any(test, feature = "test-support")))]
{
None
}
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_scratch_claude(mut self, folder: PathBuf) -> Context {
assert!(
folder.is_absolute(),
"a test named its Claude folder {} relative to wherever it runs: name it in full",
folder.display()
);
let scratch = ScratchClaude {
data: folder.join("data"),
app: folder.join("Claude.app"),
};
for named in [&folder, &scratch.data, &scratch.app] {
refuse_a_real_claude(named);
}
self.scratch_claude = Some(scratch);
self
}
pub(crate) fn awaited_every(&self) -> std::time::Duration {
#[cfg(any(test, feature = "test-support"))]
if let Some(every) = self.awaited_every {
return every;
}
std::time::Duration::from_secs(1)
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_awaited_every(mut self, every: std::time::Duration) -> Context {
self.awaited_every = Some(every);
self
}
pub(crate) fn this_machine(&self) -> String {
#[cfg(test)]
if self.unknown_machine {
return crate::state::UNKNOWN_MACHINE.into();
}
crate::state::machine_id()
}
#[cfg(test)]
pub(crate) fn with_unknown_machine(mut self) -> Context {
self.unknown_machine = true;
self
}
pub fn from_env() -> Context {
Context::for_command_line(&Environment::of_this_process())
}
pub fn for_command_line(env: &Environment) -> Context {
Context::read(env, "cli")
}
#[cfg(test)]
pub(crate) fn for_unit_test() -> Context {
let sentinel = unit_test_sentinel().into_os_string();
let homes = [
"HOME",
"USERPROFILE",
"PITBOARD_HOME",
"CLAUDE_CONFIG_DIR",
"CODEX_HOME",
];
let kept: Environment = std::env::vars_os()
.filter(|(name, _)| !variables().any(|read| name == read))
.chain(homes.map(|home| (home.into(), sentinel.clone())))
.collect();
Context::for_command_line(&kept)
}
pub(crate) fn read(env: &Environment, caller: &str) -> Context {
let home = env.home();
let owned = |name: &str| env.text(name).map(str::to_owned);
let program = |tool: ProviderId, cli: crate::provider::Cli| {
env.path(cli.program_variable)
.filter(|named| !named.is_empty())
.map_or_else(|| PathBuf::from(tool.program()), PathBuf::from)
};
let read = Context {
os: env.os(),
pitboard_home: env.pitboard_home(),
home,
claude_config_dir: owned("CLAUDE_CONFIG_DIR"),
secure_storage_dir: owned("CLAUDE_SECURESTORAGE_CONFIG_DIR"),
user: owned(env.os().user_variable()),
custom_oauth: env.set("CLAUDE_CODE_CUSTOM_OAUTH_URL"),
argv_fallback: env.text("PITBOARD_NO_ARGV") != Some("1"),
overriding_auth: crate::settings::OVERRIDING_ENV
.iter()
.filter(|name| env.set(name))
.map(|name| (*name).to_string())
.collect(),
caller: caller.into(),
programs: programs(program),
api_base: owned("PITBOARD_API_BASE"),
proxy: crate::proxy::Proxies::read(env),
agent: crate::api::Agents::default(),
hover_rest: matches!(env.text("CLAUDE_CODE_HOVER_REST"), Some("1" | "true")),
credential_manager_forced: env.text("CLAUDE_CODE_FORCE_WINDOWS_CREDMAN") == Some("1"),
codex_home: owned("CODEX_HOME").filter(|v| !v.is_empty()),
profile: (env.os() == Os::Windows)
.then(crate::host::user::profile)
.flatten(),
local_app_data: env
.path("LOCALAPPDATA")
.filter(|_| env.os() == Os::Windows)
.map(PathBuf::from),
sid: (env.os() == Os::Windows)
.then(crate::host::user::sid)
.flatten(),
one_drive: ["OneDrive", "OneDriveCommercial", "OneDriveConsumer"]
.into_iter()
.filter(|_| env.os() == Os::Windows)
.filter_map(|name| env.path(name).filter(|folder| !folder.is_empty()))
.map(PathBuf::from)
.collect(),
schedule_program: None,
search_path: Some(env.path("PATH").unwrap_or_default().to_os_string()),
installer_places: std::collections::BTreeMap::new(),
path_ext: env.path("PATHEXT").map(OsStr::to_os_string),
git_bash: env
.path("CLAUDE_CODE_GIT_BASH_PATH")
.filter(|named| env.os() == Os::Windows && !named.is_empty())
.map(PathBuf::from),
wsl_distro: env
.path("WSL_DISTRO_NAME")
.filter(|_| env.os() == Os::Linux)
.map(|named| named.to_string_lossy().into_owned()),
scheduled_job: None,
marked_as_scheduled: false,
sudo: env.set("SUDO_UID"),
clock: Arc::new(SystemClock),
locks: crate::lock::Locks::CLAUDE_CODE,
host: crate::host::current(),
api: Arc::new(Anthropic),
openai: Arc::new(OpenAiNetwork),
#[cfg(any(test, feature = "test-support"))]
sign_in_script: None,
#[cfg(any(test, feature = "test-support"))]
scratch_claude: None,
#[cfg(any(test, feature = "test-support"))]
awaited_every: None,
#[cfg(test)]
unknown_machine: false,
#[cfg(any(test, feature = "test-support"))]
warm_script: None,
};
#[cfg(any(test, feature = "test-support"))]
if let Some(folder) = env.path(TEST_CLAUDE_DESKTOP).filter(|f| !f.is_empty()) {
return read.with_scratch_claude(PathBuf::from(folder));
}
read
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_scripted_api(mut self, api: Arc<crate::api::scripted::ScriptedApi>) -> Context {
self.api = api.clone();
self.openai = api;
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_memory_stores(mut self, memory: Arc<crate::host::memory::MemoryHost>) -> Context {
self.host = memory;
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_clock(mut self, clock: Arc<dyn Clock>) -> Context {
self.clock = clock;
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_lock_waiting(mut self, retries: u32, every: std::time::Duration) -> Context {
self.locks = crate::lock::Locks {
write: self.locks.write.asked(retries, every),
refresh: self.locks.refresh.asked(retries, every),
};
self
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_sign_in_script(
mut self,
script: Arc<dyn crate::switch::enroll::SignInScript>,
) -> Context {
self.sign_in_script = Some(script);
self
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn sign_in_script(&self) -> Option<&Arc<dyn crate::switch::enroll::SignInScript>> {
self.sign_in_script.as_ref()
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn with_warm_script(
mut self,
script: Arc<dyn crate::duty::warm::run::WarmScript>,
) -> Context {
self.warm_script = Some(script);
self
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn warm_script(&self) -> Option<&Arc<dyn crate::duty::warm::run::WarmScript>> {
self.warm_script.as_ref()
}
#[cfg(any(test, feature = "test-support"))]
#[doc(hidden)]
pub fn reaches_only(
&self,
memory: &Arc<crate::host::memory::MemoryHost>,
api: &Arc<crate::api::scripted::ScriptedApi>,
) -> bool {
std::ptr::addr_eq(Arc::as_ptr(&self.host), Arc::as_ptr(memory))
&& std::ptr::addr_eq(Arc::as_ptr(&self.api), Arc::as_ptr(api))
&& std::ptr::addr_eq(Arc::as_ptr(&self.openai), Arc::as_ptr(api))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn the_test_variable_is_listed_only_in_a_build_for_tests() {
assert!(variables().any(|name| name == "PITBOARD_TEST_CLAUDE_DESKTOP"));
assert!(!READ.contains(&"PITBOARD_TEST_CLAUDE_DESKTOP"));
assert!(Context::for_unit_test().scratch_claude().is_none());
let folder = std::env::temp_dir().join("pitboard-scratch-claude");
let named = folder.to_string_lossy().into_owned();
let env: Environment = [
("HOME", "/home/x"),
("PITBOARD_TEST_CLAUDE_DESKTOP", named.as_str()),
]
.into_iter()
.collect();
let read = Context::for_command_line(&env);
let scratch = read.scratch_claude().expect("named");
assert_eq!(scratch.data, folder.join("data"));
assert_eq!(scratch.app, folder.join("Claude.app"));
}
#[test]
#[cfg_attr(
windows,
ignore = "Claude Desktop is macOS only: Windows names no Application Support"
)]
fn a_scratch_inside_the_real_application_support_panics() {
let home = {
let _reaching = crate::host::user::testing::reaching_the_real_home();
crate::host::user::home().expect("this account has a home")
};
let support = home.join("Library/Application Support");
let named = |folder: PathBuf| {
std::panic::catch_unwind(move || {
Context::new("/nowhere".into()).with_scratch_claude(folder)
})
.is_err()
};
assert!(named(support.join("pitboard-test-never-made")));
assert!(named(support.clone()));
assert!(named(home.join("Library")), "it holds it");
let link = std::env::temp_dir().join(format!("pitboard-home-link-{}", std::process::id()));
let _ = std::fs::remove_file(&link);
crate::host::fs::testing::link_dir(&home, &link).expect("a link");
let through = named(link.join("Library/Application Support/pitboard-test-never-made"));
let _ = std::fs::remove_file(&link);
assert!(through, "through a link");
assert!(!named(
std::env::temp_dir().join("pitboard-scratch-elsewhere")
));
let _guarding = testing::also_guarding(&std::env::temp_dir().join("pitboard-fake-home"));
assert!(
named(support.join("pitboard-test-never-made")),
"while a test guards a fake home too"
);
}
#[test]
#[cfg(windows)]
fn a_scratch_in_claude_desktops_windows_folders_panics() {
let named = |folder: PathBuf| {
std::panic::catch_unwind(move || {
Context::new(PathBuf::from(r"C:\nowhere")).with_scratch_claude(folder)
})
.is_err()
};
let (roaming, local) = crate::host::user::app_data_folders(true);
assert!(!roaming.is_empty() && !local.is_empty());
for dir in &roaming {
assert!(named(dir.join("Claude").join("pitboard-test-never-made")));
assert!(named(dir.join("claude-3p")), "in any case");
}
for dir in &local {
assert!(named(dir.join("AnthropicClaude")));
}
assert!(!named(
std::env::temp_dir().join("pitboard-scratch-elsewhere")
));
let fake = std::env::temp_dir().join("pitboard-fake-profile");
let _guarding = testing::also_guarding(&fake);
let roaming = fake.join("AppData").join("Roaming");
assert!(named(roaming.join("Claude").join("x")), "a fake profile's");
}
#[test]
fn a_scratch_named_through_a_folder_not_made_yet_panics() {
let home =
std::env::temp_dir().join(format!("pitboard-scratch-climb-{}", std::process::id()));
let _guarding = testing::also_guarding(&home);
let climbed = std::panic::catch_unwind(|| {
Context::new("/nowhere".into())
.with_scratch_claude(home.join("never-made/../Library/Application Support/x"))
});
assert!(climbed.is_err());
}
#[test]
fn a_relative_name_lies_where_the_working_directory_puts_it() {
let here = std::env::current_dir().expect("a working directory");
let name = Path::new("pitboard-never-made");
assert!(crate::host::lies_in(name, &here));
assert!(!crate::host::lies_in(name, &here.join("elsewhere")));
}
#[test]
fn a_relative_scratch_panics() {
let named = std::panic::catch_unwind(|| {
Context::new("/nowhere".into()).with_scratch_claude("pitboard-scratch".into())
});
assert!(named.is_err());
}
#[test]
#[cfg_attr(
windows,
ignore = "Claude Desktop is macOS only: Windows makes no link"
)]
fn a_scratch_whose_data_or_app_links_to_a_real_place_panics() {
let root =
std::env::temp_dir().join(format!("pitboard-scratch-links-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let home = root.join("home");
let support = home.join("Library/Application Support/Claude");
let app = home.join("Applications/Claude.app");
let other = root.join("other");
for made in [&support, &app, &other] {
std::fs::create_dir_all(made).expect("a folder to link to");
}
let _guarding = testing::also_guarding(&home);
let linked = |scratch: &str, name: &str, to: &Path| {
let scratch = root.join(scratch);
std::fs::create_dir_all(&scratch).expect("a scratch");
crate::host::fs::testing::link_dir(to, &scratch.join(name)).expect("a link");
std::panic::catch_unwind(|| {
Context::new("/nowhere".into()).with_scratch_claude(scratch)
})
.is_err()
};
let data = linked("one", "data", &support);
let claude = linked("two", "Claude.app", &app);
let elsewhere = linked("three", "data", &other);
let not_made = linked("four", "data", &support.join("never-made"));
let not_made_relative = linked(
"five",
"data",
Path::new("../home/Library/Application Support/Claude/never-made"),
);
std::fs::remove_dir_all(&root).expect("the scratch removed");
assert!(data, "data linked into Application Support");
assert!(claude, "Claude.app linked to ~/Applications/Claude.app");
assert!(!elsewhere, "data linked to a folder of the test's own");
assert!(not_made, "data linked to a folder not made yet there");
assert!(not_made_relative, "the same, by a relative link");
}
#[test]
#[cfg_attr(
windows,
ignore = "Claude Desktop is macOS only: Windows has no `/Applications`"
)]
fn a_scratch_of_an_applications_folder_panics() {
let home =
std::env::temp_dir().join(format!("pitboard-scratch-apps-{}", std::process::id()));
let _guarding = testing::also_guarding(&home);
let named = |folder: PathBuf| {
std::panic::catch_unwind(move || {
Context::new("/nowhere".into()).with_scratch_claude(folder)
})
.is_err()
};
assert!(named("/Applications".into()));
assert!(named(home.join("Applications")));
assert!(!named(home.join("Applications/elsewhere")));
}
#[test]
#[cfg_attr(
not(target_os = "macos"),
ignore = "a firmlink to the Data volume is macOS's own"
)]
fn a_scratch_named_through_the_data_volume_panics() {
let root =
std::env::temp_dir().join(format!("pitboard-scratch-data-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
let support = root.join("home/Library/Application Support/Claude");
let scratch = root.join("scratch");
for made in [&support, &scratch] {
std::fs::create_dir_all(made).expect("a folder of the test's own");
}
let through = |place: &Path| {
let real = std::fs::canonicalize(place).expect("a folder of the test's own");
Path::new("/System/Volumes/Data").join(real.strip_prefix("/").expect("from the root"))
};
crate::host::fs::testing::link_dir(&through(&support), &scratch.join("data"))
.expect("a link");
let _guarding = testing::also_guarding(&root.join("home"));
let named = |folder: PathBuf| {
std::panic::catch_unwind(move || {
Context::new("/nowhere".into()).with_scratch_claude(folder)
})
.is_err()
};
let linked = named(scratch);
let inside = named(through(&support).join("pitboard-test"));
let own = named(through(&root).join("own"));
let applications = named("/System/Volumes/Data/Applications".into());
std::fs::remove_dir_all(&root).expect("the scratch removed");
assert!(
linked,
"data linked into Application Support through the Data volume"
);
assert!(
inside,
"a scratch in Application Support named through the Data volume"
);
assert!(
!own,
"a folder of the test's own named through the Data volume"
);
assert!(applications, "/Applications named through the Data volume");
}
#[test]
fn an_explicit_context_reads_claude_codes_settings_the_way_the_environment_does() {
let (home, named) = full_home();
let ctx = Context::new(home.clone())
.with_claude_config_dir(String::new())
.with_secure_storage_dir(String::new());
assert_eq!(
ctx.pitboard_home,
crate::host::OS.default_pitboard_home(&home)
);
assert_eq!(
ctx.claude_config_dir.as_deref(),
Some(""),
"empty is the empty path for the config dir"
);
assert_eq!(
ctx.secure_storage_dir.as_deref(),
Some(""),
"empty is set, and pins the default slot"
);
assert_eq!(ctx.claude_program(), Path::new("claude"));
let env: Environment = [
(crate::host::OS.home_variable(), named.as_str()),
("CLAUDE_CONFIG_DIR", ""),
]
.into_iter()
.collect();
let read = Context::for_command_line(&env);
assert_eq!(read.claude_config_dir, ctx.claude_config_dir);
for ctx in [ctx, read] {
assert!(
matches!(
crate::home::check_absolute(&ctx),
Err(crate::error::Error::HomeNotAbsolute {
variable: "CLAUDE_CONFIG_DIR",
..
})
),
"refused"
);
}
}
#[test]
fn a_context_says_whether_it_reaches_only_the_machine_and_services_it_was_given() {
use crate::api::scripted::ScriptedApi;
use crate::host::memory::MemoryHost;
let (memory, api) = (MemoryHost::new(), ScriptedApi::new());
let home = PathBuf::from("/home/x");
let both = Context::new(home.clone())
.with_memory_stores(Arc::clone(&memory))
.with_scripted_api(Arc::clone(&api));
assert!(both.reaches_only(&memory, &api));
assert!(
!Context::new(home.clone())
.with_scripted_api(Arc::clone(&api))
.reaches_only(&memory, &api),
"this machine"
);
assert!(
!Context::new(home)
.with_memory_stores(Arc::clone(&memory))
.reaches_only(&memory, &api),
"the network"
);
assert!(
!both.reaches_only(&MemoryHost::new(), &api),
"another machine"
);
assert!(
!both.reaches_only(&memory, &ScriptedApi::new()),
"other services"
);
}
#[test]
fn only_a_front_end_that_names_one_changes_what_the_schedule_runs() {
assert_eq!(Context::for_unit_test().schedule_program(), None);
let ctx = Context::new(PathBuf::from("/Users/x"));
assert_eq!(ctx.schedule_program(), None);
let bundled = PathBuf::from("/Applications/Pitboard.app/Contents/Helpers/pitboard");
assert_eq!(
ctx.with_schedule_program(bundled.clone())
.schedule_program(),
Some(bundled.as_path())
);
}
#[test]
fn the_command_line_reads_the_environment_it_is_given() {
let (home, named) = full_home();
let env: Environment = [
(crate::host::OS.home_variable(), named.as_str()),
("PITBOARD_CLAUDE", "/elsewhere/claude"),
("PITBOARD_CODEX", ""),
("CLAUDE_CODE_CUSTOM_OAUTH_URL", "https://oauth.example"),
("ANTHROPIC_API_KEY", "not-a-key"),
("ANTHROPIC_AUTH_TOKEN", ""),
("PATH", "/opt/tools/bin:/usr/bin"),
]
.into_iter()
.collect();
let ctx = Context::for_command_line(&env);
assert_eq!(ctx.claude_program(), Path::new("/elsewhere/claude"));
assert_eq!(
ctx.codex_program(),
Path::new("codex"),
"empty names nothing"
);
assert!(ctx.custom_oauth());
assert_eq!(
ctx.overriding_auth(),
["ANTHROPIC_API_KEY"],
"empty is unset"
);
assert_eq!(ctx.search_path(), "/opt/tools/bin:/usr/bin");
assert_eq!(
ctx.pitboard_home,
crate::host::OS.default_pitboard_home(&home)
);
assert_eq!(ctx.caller, "cli");
let no_path: Environment = [(crate::host::OS.home_variable(), named.as_str())]
.into_iter()
.collect();
assert_eq!(
Context::for_command_line(&no_path).search_path(),
"",
"no PATH is nowhere, not this process's"
);
}
#[test]
fn a_windows_environment_has_one_value_per_name_in_any_case_and_the_first_wins() {
let search_path = |os: Os, pairs: &[(&str, &str)]| {
let home = [("HOME", "/home/x"), ("USERPROFILE", "/home/x")];
let env = Environment::on(os, home.iter().chain(pairs).copied());
Context::read(&env, "cli").search_path()
};
let windows = |pairs: &[(&str, &str)]| search_path(Os::Windows, pairs);
assert_eq!(
windows(&[("Path", "C:\\a")]),
"C:\\a",
"as a program gets it"
);
assert_eq!(windows(&[("PATH", "C:\\b")]), "C:\\b");
assert_eq!(windows(&[("Path", "C:\\a"), ("PATH", "C:\\b")]), "C:\\a");
assert_eq!(windows(&[("PATH", "C:\\b"), ("Path", "C:\\a")]), "C:\\b");
for os in [Os::MacOs, Os::Linux] {
assert_eq!(search_path(os, &[("Path", "/a")]), "", "{os:?}");
assert_eq!(search_path(os, &[("Path", "/a"), ("PATH", "/b")]), "/b");
}
let both = Environment::on(Os::Windows, [("Path", "C:\\a"), ("PATH", "C:\\b")]);
let given: Vec<_> = both.iter().collect();
assert_eq!(
given,
[(OsStr::new("Path"), OsStr::new("C:\\a"))],
"a child gets one"
);
assert_eq!(both.text("PATH"), Some("C:\\a"));
}
#[test]
fn the_person_is_named_by_the_systems_own_variable() {
let user = |os: Os| {
let pairs = [
("HOME", "/home/x"),
("USERPROFILE", "/home/x"),
("USER", "posix"),
("username", "windows"),
];
Context::read(&Environment::on(os, pairs), "cli").user
};
assert_eq!(user(Os::Windows).as_deref(), Some("windows"));
assert_eq!(user(Os::MacOs).as_deref(), Some("posix"));
assert_eq!(user(Os::Linux).as_deref(), Some("posix"));
}
#[test]
fn each_tool_finds_its_home_in_userprofile_on_windows_whatever_home_says() {
let profile = std::env::temp_dir().join("pitboard-profile-tools");
let named = profile.to_string_lossy().into_owned();
let pairs = [("HOME", "/c/Users/x"), ("USERPROFILE", named.as_str())];
let ctx = Context::read(&Environment::on(Os::Windows, pairs), "cli");
assert_eq!(ctx.home, profile);
assert_eq!(
crate::provider::claude::paths::config_dir(&ctx),
profile.join(".claude")
);
assert_eq!(
crate::provider::claude::paths::config_file(&ctx),
profile.join(".claude.json")
);
assert_eq!(
crate::provider::codex::paths::home(&ctx),
profile.join(".codex")
);
assert_eq!(
ctx.pitboard_home,
profile.join("AppData").join("Local").join("Pitboard")
);
let posix = Context::read(&Environment::on(Os::Linux, pairs), "cli");
assert_eq!(posix.home, PathBuf::from("/c/Users/x"));
}
fn full_home() -> (PathBuf, String) {
let home = std::env::temp_dir().join("pitboard-never-made-home");
let named = home.to_string_lossy().into_owned();
(home, named)
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "PITBOARD_SOMETHING_NEW is read but not listed")]
fn a_variable_read_without_being_listed_is_caught() {
let _ = Environment::default().path("PITBOARD_SOMETHING_NEW");
}
#[test]
fn without_home_the_home_is_the_accounts_own() {
let _real = crate::host::user::testing::reaching_the_real_home();
let own = crate::host::user::home().expect("this account has a home");
let ctx = Context::for_command_line(&Environment::default());
assert_eq!(ctx.home, own);
assert_eq!(
ctx.pitboard_home,
crate::host::OS.default_pitboard_home(&own)
);
let empty: Environment = [(crate::host::OS.home_variable(), "")]
.into_iter()
.collect();
assert_eq!(
Context::for_command_line(&empty).home,
PathBuf::new(),
"set, even empty, it is what the person said"
);
}
#[test]
fn a_unit_tests_homes_are_a_folder_of_its_own_that_nothing_makes() {
let ctx = Context::for_unit_test();
let sentinel = ctx.home.clone();
assert!(
sentinel.starts_with(std::env::temp_dir()),
"{}",
sentinel.display()
);
assert!(!sentinel.exists(), "{}", sentinel.display());
assert_eq!(ctx.pitboard_home, sentinel);
assert_eq!(
crate::provider::claude::paths::config_dir(&ctx),
sentinel,
"Claude Code's"
);
assert_eq!(
crate::provider::codex::paths::home(&ctx),
sentinel,
"Codex's"
);
assert_eq!(Context::for_unit_test().home, sentinel, "the same test's");
let another = std::thread::Builder::new()
.name("another::test".into())
.spawn(|| Context::for_unit_test().home)
.expect("a thread")
.join()
.expect("its context");
assert_ne!(another, sentinel, "another test's");
assert!(another.ends_with("another--test"), "{}", another.display());
}
#[test]
fn the_homes_an_app_asks_for_are_the_contexts() {
let _real = crate::host::user::testing::reaching_the_real_home();
let cases: [&[(&str, &str)]; 5] = [
&[],
&[("HOME", "/Users/x")],
&[("HOME", "/Users/x/")],
&[
("HOME", "/Users/x"),
("PITBOARD_HOME", "/elsewhere/./pitboard/"),
],
&[("PITBOARD_HOME", "")],
];
for pairs in cases {
let env: Environment = pairs.iter().copied().collect();
let ctx = Context::for_command_line(&env);
assert_eq!(env.home(), ctx.home, "{pairs:?}");
assert_eq!(env.pitboard_home(), ctx.pitboard_home, "{pairs:?}");
}
let given: Environment = [("PITBOARD_HOME", "/elsewhere/./pitboard/")]
.into_iter()
.collect();
assert_eq!(
given.pitboard_home().as_os_str(),
"/elsewhere/./pitboard/",
"as the environment gives it"
);
}
}