use crate::paths::Paths;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
pub fn shim_path(paths: &Paths) -> PathBuf {
shim_path_for(paths, "claude-code")
}
pub fn shim_bin_dir(paths: &Paths) -> PathBuf {
paths.store_dir().join("bin")
}
pub fn shim_path_for(paths: &Paths, tool: &str) -> PathBuf {
let bin = match tool {
"codex" => "codex",
_ => "claude",
};
paths.store_dir().join("bin").join(bin)
}
fn sh_quote(p: &Path) -> String {
let s = p.to_string_lossy();
format!("'{}'", s.replace('\'', "'\\''"))
}
pub fn shim_script(pointer: &Path, real_claude: &Path, swapdex: &Path) -> String {
format!(
"#!/bin/sh\n\
# swapdex claude shim - launch claude in the default account's slot.\n\
# Managed by swapdex; re-created by `swapdex shim`.\n\
# Signing in must reach Anthropic directly. The OAuth exchange is between\n\
# the browser and the real API, and a proxy in the middle both breaks the\n\
# code exchange and answers with whichever account it already has - so a\n\
# fresh slot looks signed in as somebody else, or the prompt takes no\n\
# input at all.\n\
sx_login=no\n\
for a in \"$@\"; do\n\
\tcase \"$a\" in login|/login|logout|/logout|setup-token) sx_login=yes ;; esac\n\
done\n\
# Ask swapdex for a live proxy (it starts one if needed and prints the\n\
# port); silence and a non-zero status mean \"run without one\".\n\
if [ \"$sx_login\" = no ]; then\n\
\tport=$({sx} proxy --ensure 2>/dev/null)\n\
\tif [ -n \"$port\" ]; then\n\
\t\tANTHROPIC_BASE_URL=\"http://127.0.0.1:$port\"\n\
\t\texport ANTHROPIC_BASE_URL\n\
\tfi\n\
fi\n\
if [ -z \"$CLAUDE_CONFIG_DIR\" ]; then\n\
\tdir=$(cat {ptr} 2>/dev/null)\n\
\tif [ -n \"$dir\" ]; then\n\
\t\tCLAUDE_CONFIG_DIR=\"$dir\"\n\
\t\texport CLAUDE_CONFIG_DIR\n\
\tfi\n\
fi\n\
exec {real} \"$@\"\n",
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_claude),
)
}
pub fn codex_shim_script(pointer: &Path, real_codex: &Path, swapdex: &Path) -> String {
format!(
"#!/bin/sh\n\
# swapdex codex shim - launch codex in the default account's slot.\n\
# Managed by swapdex; re-created by `swapdex shim`.\n\
# The provider overrides belong on a run that TALKS to the model. On\n\
# `resume` they emptied the session picker: Codex lists the sessions that\n\
# match the configured provider, and a conversation held long before\n\
# swapdex existed matches none. A sign-in is excluded for its own reason -\n\
# the OAuth exchange is between the browser and the real backend, and a\n\
# proxy in the middle answers with whichever account it already holds.\n\
sx_plain=no\n\
for a in \"$@\"; do\n\
\tcase \"$a\" in login|/login|logout|/logout|resume|/resume|history|sessions) sx_plain=yes ;; esac\n\
done\n\
# Ask swapdex for a live proxy (it starts one if needed and prints the\n\
# port); silence means \"run without one\", exactly as before.\n\
if [ \"$sx_plain\" = no ]; then\n\
\tport=$({sx} proxy --ensure --tool codex 2>/dev/null)\n\
\t# Who pays. Codex prints the provider name on /status and nothing\n\
\t# else about identity, so the account goes in the one field it shows.\n\
\tsx_who=$({sx} serve --tool codex --quiet 2>/dev/null)\n\
fi\n\
if [ -n \"$port\" ]; then\n\
\tsx_name=swapdex\n\
\tif [ -n \"$sx_who\" ]; then\n\
\t\tsx_name=\"swapdex: $sx_who\"\n\
\tfi\n\
\tset -- -c model_provider=swapdex \\\n\
\t\t-c model_providers.swapdex.name=\"$sx_name\" \\\n\
\t\t-c model_providers.swapdex.base_url=\"http://127.0.0.1:$port/v1\" \\\n\
\t\t-c model_providers.swapdex.wire_api=responses \"$@\"\n\
fi\n\
if [ -z \"$CODEX_HOME\" ]; then\n\
\tdir=$(cat {ptr} 2>/dev/null)\n\
\tif [ -n \"$dir\" ]; then\n\
\t\tCODEX_HOME=\"$dir\"\n\
\t\texport CODEX_HOME\n\
\tfi\n\
fi\n\
exec {real} \"$@\"\n",
sx = sh_quote(swapdex),
ptr = sh_quote(pointer),
real = sh_quote(real_codex),
)
}
pub fn proxy_marker(paths: &Paths) -> PathBuf {
proxy_marker_for(paths, "claude-code")
}
pub fn proxy_marker_for(paths: &Paths, tool: &str) -> PathBuf {
match tool {
"codex" => paths.store_dir().join("proxy-codex"),
_ => paths.store_dir().join("proxy"),
}
}
const SHIM_MARKER: &str = "swapdex claude shim";
const SHIM_MARKER_CODEX: &str = "swapdex codex shim";
fn is_our_shim(path: &Path) -> bool {
let mut buf = [0u8; 256];
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
use std::io::Read;
let n = f.read(&mut buf).unwrap_or(0);
let head = String::from_utf8_lossy(&buf[..n]);
head.contains(SHIM_MARKER) || head.contains(SHIM_MARKER_CODEX)
}
pub(crate) fn resolved_claude() -> Option<(PathBuf, bool)> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
let cand = dir.join("claude");
if cand.is_file() {
let ours = is_our_shim(&cand);
return Some((cand, ours));
}
}
None
}
fn find_real_claude(shim_dir: &Path) -> Option<PathBuf> {
find_real(shim_dir, "claude")
}
fn find_real(shim_dir: &Path, bin: &str) -> Option<PathBuf> {
let path = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&path) {
if dir == shim_dir {
continue;
}
let cand = dir.join(bin);
if cand.is_file() && !is_our_shim(&cand) {
return Some(cand);
}
}
None
}
#[derive(Debug, PartialEq)]
pub enum ShimReach {
Active,
ConfiguredElsewhere,
Missing,
}
pub fn shim_reach(active: bool, profile_text: Option<&str>, shim_dir: &Path) -> ShimReach {
if active {
return ShimReach::Active;
}
match profile_text {
Some(t) if profile_already_adds(t, shim_dir) => ShimReach::ConfiguredElsewhere,
_ => ShimReach::Missing,
}
}
pub fn shell_profile_text() -> Option<(PathBuf, String)> {
let p = shell_profile()?;
let t = std::fs::read_to_string(&p).ok()?;
Some((p, t))
}
fn profile_already_adds(profile_text: &str, shim_dir: &Path) -> bool {
let full = shim_dir.to_string_lossy().to_string();
let home = dirs::home_dir().map(|h| h.to_string_lossy().to_string());
let alts: Vec<String> = home
.iter()
.filter_map(|h| full.strip_prefix(h.as_str()))
.flat_map(|rest| [format!("$HOME{rest}"), format!("~{rest}")])
.collect();
profile_text.lines().any(|l| {
let l = l.trim();
if !l.contains("PATH") || l.starts_with('#') {
return false;
}
l.contains(&full) || alts.iter().any(|a| l.contains(a))
})
}
fn path_line(shim_dir: &Path) -> String {
format!("export PATH=\"{}:$PATH\"", shim_dir.display())
}
const PROFILE_MARKER: &str = "# added by swapdex (claude shim)";
fn shell_profile() -> Option<PathBuf> {
let home = dirs::home_dir()?;
let shell = std::env::var("SHELL").unwrap_or_default();
let name = shell.rsplit('/').next().unwrap_or("");
match name {
"zsh" => Some(home.join(".zshrc")),
"bash" => {
let bp = home.join(".bash_profile");
if bp.exists() {
Some(bp)
} else {
Some(home.join(".bashrc"))
}
}
_ => None,
}
}
fn already_on_path(shim_dir: &Path) -> bool {
std::env::var_os("PATH")
.map(|p| std::env::split_paths(&p).any(|d| d == shim_dir))
.unwrap_or(false)
}
pub enum PathSetup {
AlreadyThere,
Added(PathBuf),
Manual,
}
pub fn ensure_on_path(shim_dir: &Path) -> Result<PathSetup> {
if already_on_path(shim_dir) {
return Ok(PathSetup::AlreadyThere);
}
let Some(profile) = shell_profile() else {
return Ok(PathSetup::Manual);
};
let existing = std::fs::read_to_string(&profile).unwrap_or_default();
let line = path_line(shim_dir);
if existing.contains(PROFILE_MARKER) || profile_already_adds(&existing, shim_dir) {
return Ok(PathSetup::Added(profile));
}
let mut out = existing;
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&format!("\n{PROFILE_MARKER}\n{line}\n"));
std::fs::write(&profile, out).with_context(|| format!("edit {}", profile.display()))?;
Ok(PathSetup::Added(profile))
}
#[derive(Debug, PartialEq)]
pub enum PinVerdict {
Pin(u16),
RefuseNoService,
}
pub fn pin_verdict(service_installed: bool, port: u16) -> PinVerdict {
if service_installed {
PinVerdict::Pin(port)
} else {
PinVerdict::RefuseNoService
}
}
pub fn pinned_port(settings: &serde_json::Value) -> Option<u16> {
let url = settings.get("env")?.get("ANTHROPIC_BASE_URL")?.as_str()?;
let rest = url.strip_prefix("http://")?;
let (host, port) = rest.trim_end_matches('/').rsplit_once(':')?;
if host != "127.0.0.1" && host != "localhost" {
return None;
}
port.parse().ok()
}
pub fn with_base_url(settings: &serde_json::Value, port: u16) -> serde_json::Value {
let mut out = settings.clone();
if !out.is_object() {
out = serde_json::json!({});
}
let obj = out.as_object_mut().expect("object");
let env = obj.entry("env").or_insert_with(|| serde_json::json!({}));
if !env.is_object() {
*env = serde_json::json!({});
}
env.as_object_mut().expect("env object").insert(
"ANTHROPIC_BASE_URL".to_string(),
serde_json::Value::String(format!("http://127.0.0.1:{port}")),
);
out
}
pub fn pin_base_url(paths: &Paths, port: u16, service_installed: bool) -> Result<Option<PathBuf>> {
if pin_verdict(service_installed, port) == PinVerdict::RefuseNoService {
return Ok(None);
}
let file = paths.claude_dir().join("settings.json");
let current: serde_json::Value = std::fs::read(&file)
.ok()
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_else(|| serde_json::json!({}));
let next = with_base_url(¤t, port);
if next == current {
return Ok(Some(file));
}
if file.exists() {
let _ = std::fs::copy(&file, file.with_extension("json.swapdex-bak"));
}
if let Some(d) = file.parent() {
std::fs::create_dir_all(d).ok();
}
let bytes = serde_json::to_vec_pretty(&next).context("serialize settings.json")?;
crate::atomic::write_secret(&file, &bytes).context("write settings.json")?;
Ok(Some(file))
}
#[derive(Debug, PartialEq)]
pub enum PathVerdict {
Wins,
Shadowed(String),
Absent,
}
pub fn path_verdict(shim_dir: &std::path::Path, entries: &[&str]) -> PathVerdict {
path_verdict_with(shim_dir, entries, &|d| {
std::path::Path::new(d).join("claude").exists()
})
}
pub fn path_verdict_with(
shim_dir: &std::path::Path,
entries: &[&str],
holds_tool: &dyn Fn(&str) -> bool,
) -> PathVerdict {
let shim = shim_dir.to_string_lossy();
if !entries.iter().any(|e| *e == shim) {
return PathVerdict::Absent;
}
for e in entries {
if *e == shim {
return PathVerdict::Wins;
}
if holds_tool(e) {
return PathVerdict::Shadowed((*e).to_string());
}
}
PathVerdict::Absent
}
pub fn install(paths: &Paths) -> Result<(PathBuf, PathBuf)> {
let shim = shim_path(paths);
let shim_dir = shim
.parent()
.map(|p| p.to_path_buf())
.context("shim path has no parent")?;
let real = find_real_claude(&shim_dir)
.context("could not find the real `claude` on PATH - install it first")?;
let pointer = paths.store_dir().join("active-claude");
let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("swapdex"));
std::fs::create_dir_all(&shim_dir).context("create shim dir")?;
std::fs::write(&shim, shim_script(&pointer, &real, &me)).context("write shim")?;
make_executable(&shim)?;
Ok((shim, shim_dir))
}
pub fn install_codex(paths: &Paths) -> Result<Option<PathBuf>> {
let shim = shim_path_for(paths, "codex");
let shim_dir = shim
.parent()
.map(|p| p.to_path_buf())
.context("shim path has no parent")?;
let Some(real) = find_real(&shim_dir, "codex") else {
return Ok(None);
};
let pointer = paths.store_dir().join("active-codex");
let me = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("swapdex"));
std::fs::create_dir_all(&shim_dir).context("create shim dir")?;
std::fs::write(&shim, codex_shim_script(&pointer, &real, &me)).context("write codex shim")?;
make_executable(&shim)?;
Ok(Some(shim))
}
fn make_executable(p: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o755))
.context("chmod shim")?;
}
Ok(())
}
#[cfg(test)]
mod reach_tests {
use super::*;
#[test]
fn a_shell_that_never_read_the_profile_is_not_a_broken_setup() {
let dir = Path::new("/Users/x/Library/Application Support/swapdex/bin");
let zshrc = "export PATH=\"/Users/x/Library/Application Support/swapdex/bin:$PATH\"\n";
assert_eq!(
shim_reach(false, Some(zshrc), dir),
ShimReach::ConfiguredElsewhere
);
}
#[test]
fn nothing_putting_it_on_path_is_still_a_real_finding() {
let dir = Path::new("/Users/x/Library/Application Support/swapdex/bin");
assert_eq!(
shim_reach(false, Some("export EDITOR=vim\n"), dir),
ShimReach::Missing
);
assert_eq!(shim_reach(false, None, dir), ShimReach::Missing);
}
#[test]
fn a_profile_that_set_up_some_other_store_excuses_nothing() {
let mine = Path::new("/tmp/store-a/bin");
let theirs = "# added by swapdex\nexport PATH=\"/tmp/store-b/bin:$PATH\"\n";
assert_eq!(shim_reach(false, Some(theirs), mine), ShimReach::Missing);
}
#[test]
fn a_shim_that_works_here_needs_no_explaining() {
let dir = Path::new("/tmp/bin");
assert_eq!(shim_reach(true, None, dir), ShimReach::Active);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn an_existing_path_line_is_recognised_however_it_is_spelled() {
let home = dirs::home_dir().expect("a home dir");
let shim_dir = home.join("Library/Application Support/swapdex/bin");
let full = shim_dir.display().to_string();
for spelling in [
format!("export PATH=\"{full}:$PATH\""),
"export PATH=\"$HOME/Library/Application Support/swapdex/bin:$PATH\"".to_string(),
"export PATH=\"~/Library/Application Support/swapdex/bin:$PATH\"".to_string(),
] {
assert!(
profile_already_adds(&format!("# something\n{spelling}\n"), &shim_dir),
"not recognised: {spelling}"
);
}
assert!(!profile_already_adds(
"export PATH=\"/usr/local/bin:$PATH\"\n",
&shim_dir
));
assert!(!profile_already_adds(
&format!("# export PATH=\"{full}:$PATH\"\n"),
&shim_dir
));
assert!(!profile_already_adds(&format!("echo {full}\n"), &shim_dir));
}
#[test]
fn the_shim_does_not_proxy_a_sign_in() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("sx_login=no"),
"it decides whether this is a sign-in: {s}"
);
for verb in ["login", "/login", "logout", "setup-token"] {
assert!(s.contains(verb), "recognised: {verb}");
}
let guard = s.find("if [ \"$sx_login\" = no ]").expect("the guard");
let export = s.find("ANTHROPIC_BASE_URL").expect("the export");
assert!(
guard < export,
"the proxy address is only set when not signing in"
);
}
#[test]
fn the_codex_shim_routes_through_a_running_proxy() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("proxy --ensure --tool codex"),
"asks swapdex for a live codex proxy: {s}"
);
assert!(s.contains("model_provider=swapdex"), "selects the provider");
assert!(
s.contains("model_providers.swapdex.base_url=\"http://127.0.0.1:$port/v1\""),
"points it at the proxy: {s}"
);
assert!(
s.contains("model_providers.swapdex.wire_api=responses"),
"the protocol codex speaks"
);
assert!(
!s.contains("env_key"),
"declaring an api key would stop codex attaching its own OAuth"
);
assert!(
s.contains("if [ -n \"$port\" ]"),
"the overrides are conditional: {s}"
);
}
#[test]
fn the_codex_shim_leaves_reading_commands_alone() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
for verb in ["resume", "history", "sessions"] {
assert!(s.contains(verb), "recognised as a plain run: {verb}");
}
let guard = s.find("if [ \"$sx_plain\" = no ]").expect("the guard");
let ask = s.find("proxy --ensure").expect("the ask");
assert!(guard < ask, "the proxy is only asked for on a talking run");
let home = s.find("CODEX_HOME=").expect("home");
assert!(home > guard, "the home is set outside the guard: {s}");
}
#[test]
fn the_codex_shim_points_codex_home_at_the_default_slot() {
let s = codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/bin/swapdex"),
);
assert!(s.starts_with("#!/bin/sh"));
assert!(
s.contains("/store/active-codex"),
"reads codex's own pointer"
);
assert!(s.contains("/usr/bin/codex"), "execs the real codex");
assert!(s.contains("CODEX_HOME="), "sets the slot env");
assert!(
s.contains("if [ -z \"$CODEX_HOME\" ]"),
"an explicit CODEX_HOME is a decision already made"
);
assert!(s.contains("exec "), "replaces the process");
assert!(!s.contains("CLAUDE_CONFIG_DIR"));
assert!(!s.contains("ANTHROPIC_BASE_URL"));
}
#[test]
fn script_references_pointer_real_claude_and_config_dir() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(s.starts_with("#!/bin/sh"));
assert!(s.contains("/store/active-claude"), "reads the pointer");
assert!(s.contains("/usr/bin/claude"), "execs the real claude");
assert!(s.contains("CLAUDE_CONFIG_DIR="), "sets the slot env");
assert!(s.contains("exec "), "replaces the process");
}
#[test]
fn an_explicit_config_dir_wins_over_the_default_pointer() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("if [ -z \"$CLAUDE_CONFIG_DIR\" ]"),
"the pointer only fills in when nothing chose a dir: {s}"
);
assert!(s.contains("/store/active-claude"), "{s}");
assert!(s.contains("CLAUDE_CONFIG_DIR="), "{s}");
}
#[test]
fn script_gets_its_proxy_from_swapdex_and_tolerates_none() {
let s = shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/bin/swapdex"),
);
assert!(
s.contains("'/bin/swapdex' proxy --ensure"),
"asks swapdex by absolute path, so the user starts nothing: {s}"
);
assert!(
s.contains("ANTHROPIC_BASE_URL"),
"points claude at the proxy"
);
assert!(
s.contains("http://127.0.0.1:$port"),
"loopback only, port from swapdex"
);
assert!(
s.contains("2>/dev/null") && s.contains("if [ -n \"$port\" ]"),
"no proxy is not an error - claude still runs: {s}"
);
}
#[test]
fn script_quotes_paths_with_spaces() {
let s = shim_script(
Path::new("/a b/active-claude"),
Path::new("/c d/claude"),
Path::new("/e f/swapdex"),
);
assert!(s.contains("'/a b/active-claude'"), "pointer is quoted");
assert!(s.contains("'/e f/swapdex'"), "swapdex path is quoted");
assert!(s.contains("'/c d/claude'"), "real claude is quoted");
}
#[test]
fn recognizes_our_own_shim_by_marker() {
let dir = tempfile::tempdir().unwrap();
let shim = dir.path().join("claude");
std::fs::write(
&shim,
shim_script(Path::new("/p"), Path::new("/real"), Path::new("/sx")),
)
.unwrap();
assert!(is_our_shim(&shim), "our shim is recognized by its marker");
let real = dir.path().join("real-claude");
std::fs::write(&real, "#!/bin/sh\nexec node /opt/claude \"$@\"\n").unwrap();
assert!(!is_our_shim(&real), "a real claude is not flagged");
}
}
pub fn swapdex_path_in(text: &str) -> Option<PathBuf> {
let at = text.find(" proxy --ensure")?;
let start = text[..at].rfind("$(")? + 2;
let token = text[start..at].trim();
let inner = token.strip_prefix('\'')?.strip_suffix('\'')?;
let path = inner.replace("'\\''", "'");
(!path.is_empty()).then(|| PathBuf::from(path))
}
#[cfg(test)]
mod embedded_path_tests {
use super::*;
use std::path::Path;
#[test]
fn the_path_comes_back_out_of_a_shim_we_wrote() {
for shim in [
shim_script(
Path::new("/store/active-claude"),
Path::new("/usr/bin/claude"),
Path::new("/opt/homebrew/bin/swapdex"),
),
codex_shim_script(
Path::new("/store/active-codex"),
Path::new("/usr/bin/codex"),
Path::new("/opt/homebrew/bin/swapdex"),
),
] {
assert_eq!(
swapdex_path_in(&shim).as_deref(),
Some(Path::new("/opt/homebrew/bin/swapdex"))
);
}
}
#[test]
fn a_quoted_path_survives_the_round_trip() {
let odd = Path::new("/Users/o'brien/.local/bin/swapdex");
let shim = codex_shim_script(Path::new("/p"), Path::new("/usr/bin/codex"), odd);
assert_eq!(swapdex_path_in(&shim).as_deref(), Some(odd));
}
#[test]
fn something_that_is_not_our_shim_yields_nothing() {
assert_eq!(
swapdex_path_in("#!/bin/sh\nexec /usr/bin/claude \"$@\"\n"),
None
);
}
}
pub fn swapdex_copies_on(path_var: &str) -> Vec<PathBuf> {
let mut out: Vec<PathBuf> = Vec::new();
for dir in path_var.split(':').filter(|d| !d.is_empty()) {
let cand = Path::new(dir).join("swapdex");
if !cand.is_file() {
continue;
}
let real = std::fs::canonicalize(&cand).unwrap_or(cand);
if !out.contains(&real) {
out.push(real);
}
}
out
}
#[cfg(test)]
mod copies_tests {
use super::*;
#[test]
fn two_entries_for_one_file_are_one_install() {
let root = tempfile::tempdir().unwrap();
let real = root.path().join("cellar");
std::fs::create_dir_all(&real).unwrap();
std::fs::write(real.join("swapdex"), b"#!/bin/sh\n").unwrap();
let linked = root.path().join("bin");
std::fs::create_dir_all(&linked).unwrap();
std::os::unix::fs::symlink(real.join("swapdex"), linked.join("swapdex")).unwrap();
let path = format!("{}:{}", linked.display(), real.display());
assert_eq!(
swapdex_copies_on(&path).len(),
1,
"a symlink is not a second install"
);
}
#[test]
fn two_real_files_are_two_installs_in_path_order() {
let root = tempfile::tempdir().unwrap();
let (a, b) = (root.path().join("npm"), root.path().join("brew"));
for d in [&a, &b] {
std::fs::create_dir_all(d).unwrap();
std::fs::write(d.join("swapdex"), b"#!/bin/sh\n").unwrap();
}
let found = swapdex_copies_on(&format!("{}:{}", a.display(), b.display()));
assert_eq!(found.len(), 2, "both are real, and one shadows the other");
let a_real = std::fs::canonicalize(&a).unwrap();
assert!(
found[0].starts_with(&a_real),
"the one that wins comes first"
);
}
#[test]
fn nothing_installed_is_not_a_problem() {
assert!(swapdex_copies_on("/nonexistent-a:/nonexistent-b").is_empty());
}
}
#[cfg(test)]
fn shim_marker_line() -> String {
format!("#!/bin/sh\n# {SHIM_MARKER_CODEX}\n")
}
pub fn real_tool(paths: &Paths, tool: &str) -> Option<PathBuf> {
find_real(&shim_bin_dir(paths), crate::commands::tool_binary(tool))
}
#[cfg(test)]
mod real_tool_tests {
use super::*;
#[test]
fn the_shim_dir_is_stepped_over() {
let root = tempfile::tempdir().unwrap();
let paths = Paths::rooted(root.path());
let dir = shim_bin_dir(&paths);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("codex"), shim_marker_line()).unwrap();
if let Some(found) = real_tool(&paths, "codex") {
assert!(
!found.starts_with(&dir),
"resolved {} inside the shim dir",
found.display()
);
}
}
}
#[cfg(test)]
mod shadow_tests {
use super::*;
#[test]
fn a_shim_that_loses_the_path_is_reported_as_shadowed() {
let shim = std::path::Path::new("/home/u/.local/share/swapdex/bin");
assert_eq!(
path_verdict(shim, &["/home/u/.local/share/swapdex/bin", "/usr/bin"]),
PathVerdict::Wins
);
assert_eq!(
path_verdict_with(
shim,
&["/home/u/.local/bin", "/home/u/.local/share/swapdex/bin"],
&|d| d == "/home/u/.local/bin"
),
PathVerdict::Shadowed("/home/u/.local/bin".into())
);
assert_eq!(
path_verdict(shim, &["/usr/bin", "/bin"]),
PathVerdict::Absent
);
}
#[test]
fn an_earlier_directory_without_the_tool_does_not_shadow() {
let shim = std::path::Path::new("/shim/bin");
assert_eq!(
path_verdict_with(shim, &["/empty", "/shim/bin"], &|d| d != "/empty"),
PathVerdict::Wins
);
}
}
#[cfg(test)]
mod pin_base_url_tests {
use super::*;
#[test]
fn a_base_url_is_pinned_only_when_a_service_keeps_the_proxy_alive() {
assert_eq!(pin_verdict(true, 8787), PinVerdict::Pin(8787));
assert_eq!(pin_verdict(false, 8787), PinVerdict::RefuseNoService);
}
#[test]
fn pinning_preserves_every_other_setting() {
let before = serde_json::json!({
"model": "opus",
"env": {"FOO": "1"},
"permissions": {"allow": ["Bash"]}
});
let after = with_base_url(&before, 8787);
assert_eq!(after["model"], "opus");
assert_eq!(after["permissions"]["allow"][0], "Bash");
assert_eq!(after["env"]["FOO"], "1");
assert_eq!(after["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:8787");
}
#[test]
fn pinning_works_on_settings_that_have_no_env_yet() {
let after = with_base_url(&serde_json::json!({"model": "opus"}), 9001);
assert_eq!(after["env"]["ANTHROPIC_BASE_URL"], "http://127.0.0.1:9001");
assert_eq!(after["model"], "opus");
}
}
#[cfg(test)]
mod pinned_port_tests {
use super::*;
#[test]
fn the_pinned_port_can_be_read_back_out_of_settings() {
let pinned = serde_json::json!({
"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:8787"},
"model": "opus"
});
assert_eq!(pinned_port(&pinned), Some(8787));
let other_port = serde_json::json!({
"env": {"ANTHROPIC_BASE_URL": "http://127.0.0.1:9001"}
});
assert_eq!(pinned_port(&other_port), Some(9001));
assert_eq!(pinned_port(&serde_json::json!({"model": "opus"})), None);
assert_eq!(
pinned_port(
&serde_json::json!({"env": {"ANTHROPIC_BASE_URL": "https://api.anthropic.com"}})
),
None
);
}
}