import CodexBarCoreimport Foundationimport Testing@testable import CodexBar @Suitestruct PathBuilderTests { @Test func mergesLoginShellPathWhenAvailable() { let seeded = PathBuilder.effectivePATH( purposes: [.rpc], env: ["PATH": "/custom/bin:/usr/bin"], loginPATH: ["/login/bin", "/login/alt"]) #expect(seeded == "/login/bin:/login/alt:/custom/bin:/usr/bin") } @Test func fallsBackToExistingPathWhenNoLoginPath() { let seeded = PathBuilder.effectivePATH( purposes: [.tty], env: ["PATH": "/custom/bin:/usr/bin"], loginPATH: nil) #expect(seeded == "/custom/bin:/usr/bin") } @Test func usesFallbackWhenNoPathAvailable() { let seeded = PathBuilder.effectivePATH( purposes: [.tty], env: [:], loginPATH: nil) #expect(seeded == "/usr/bin:/bin:/usr/sbin:/sbin") } @Test func resolvesCodexFromEnvOverride() { let overridePath = "/custom/bin/codex" let fm = MockFileManager(executables: [overridePath]) let resolved = BinaryLocator.resolveCodexBinary( env: ["CODEX_CLI_PATH": overridePath], loginPATH: nil, fileManager: fm, home: "/home/test") #expect(resolved == overridePath) } @Test func resolvesCodexFromLoginPath() { let fm = MockFileManager(executables: ["/login/bin/codex"]) let resolved = BinaryLocator.resolveCodexBinary( env: ["PATH": "/env/bin"], loginPATH: ["/login/bin"], fileManager: fm, home: "/home/test") #expect(resolved == "/login/bin/codex") } @Test func resolvesCodexFromEnvPath() { let fm = MockFileManager(executables: ["/env/bin/codex"]) let resolved = BinaryLocator.resolveCodexBinary( env: ["PATH": "/env/bin:/usr/bin"], loginPATH: nil, fileManager: fm, home: "/home/test") #expect(resolved == "/env/bin/codex") } @Test func resolvesCodexFromInteractiveShell() { let fm = MockFileManager(executables: ["/shell/bin/codex"]) let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in #expect(tool == "codex") #expect(shell == "/bin/zsh") #expect(timeout == 2.0) _ = fileManager return "/shell/bin/codex" } let resolved = BinaryLocator.resolveCodexBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, fileManager: fm, home: "/home/test") #expect(resolved == "/shell/bin/codex") } @Test func resolvesClaudeFromInteractiveShell() { let fm = MockFileManager(executables: ["/shell/bin/claude"]) let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in #expect(tool == "claude") #expect(shell == "/bin/zsh") #expect(timeout == 2.0) _ = fileManager return "/shell/bin/claude" } let resolved = BinaryLocator.resolveClaudeBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, fileManager: fm, home: "/home/test") #expect(resolved == "/shell/bin/claude") } @Test func resolvesGeminiFromInteractiveShell() { let fm = MockFileManager(executables: ["/shell/bin/gemini"]) let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in #expect(tool == "gemini") #expect(shell == "/bin/zsh") #expect(timeout == 2.0) _ = fileManager return "/shell/bin/gemini" } let resolved = BinaryLocator.resolveGeminiBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, fileManager: fm, home: "/home/test") #expect(resolved == "/shell/bin/gemini") } @Test func resolvesClaudeFromLoginPath() { let fm = MockFileManager(executables: ["/login/bin/claude"]) let resolved = BinaryLocator.resolveClaudeBinary( env: ["PATH": "/env/bin"], loginPATH: ["/login/bin"], fileManager: fm, home: "/home/test") #expect(resolved == "/login/bin/claude") } @Test func resolvesClaudeFromAliasWhenOtherLookupsFail() { let aliasPath = "/home/test/.claude/local/bin/claude" let fm = MockFileManager(executables: [aliasPath]) var aliasCalled = false let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { tool, shell, timeout, _, home in aliasCalled = true #expect(tool == "claude") #expect(shell == "/bin/zsh") #expect(timeout == 2.0) #expect(home == "/home/test") return aliasPath } let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil } let resolved = BinaryLocator.resolveClaudeBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, aliasResolver: aliasResolver, fileManager: fm, home: "/home/test") #expect(aliasCalled) #expect(resolved == aliasPath) } @Test func resolvesCodexFromAliasWhenOtherLookupsFail() { let aliasPath = "/home/test/.codex/bin/codex" let fm = MockFileManager(executables: [aliasPath]) var aliasCalled = false let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { tool, shell, timeout, _, home in aliasCalled = true #expect(tool == "codex") #expect(shell == "/bin/zsh") #expect(timeout == 2.0) #expect(home == "/home/test") return aliasPath } let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil } let resolved = BinaryLocator.resolveCodexBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, aliasResolver: aliasResolver, fileManager: fm, home: "/home/test") #expect(aliasCalled) #expect(resolved == aliasPath) } @Test func skipsAliasWhenCommandVResolves() { let path = "/shell/bin/claude" let fm = MockFileManager(executables: [path]) var aliasCalled = false let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in aliasCalled = true return "/alias/claude" } let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in path } let resolved = BinaryLocator.resolveClaudeBinary( env: ["SHELL": "/bin/zsh"], loginPATH: nil, commandV: commandV, aliasResolver: aliasResolver, fileManager: fm, home: "/home/test") #expect(!aliasCalled) #expect(resolved == path) }} private final class MockFileManager: FileManager { private let executables: Set<String> init(executables: Set<String>) { self.executables = executables } override func isExecutableFile(atPath path: String) -> Bool { self.executables.contains(path) }}
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@Suite
struct PathBuilderTests {
@Test
func mergesLoginShellPathWhenAvailable() {
let seeded = PathBuilder.effectivePATH(
purposes: [.rpc],
env: ["PATH": "/custom/bin:/usr/bin"],
loginPATH: ["/login/bin", "/login/alt"])
#expect(seeded == "/login/bin:/login/alt:/custom/bin:/usr/bin")
}
@Test
func fallsBackToExistingPathWhenNoLoginPath() {
let seeded = PathBuilder.effectivePATH(
purposes: [.tty],
env: ["PATH": "/custom/bin:/usr/bin"],
loginPATH: nil)
#expect(seeded == "/custom/bin:/usr/bin")
}
@Test
func usesFallbackWhenNoPathAvailable() {
let seeded = PathBuilder.effectivePATH(
purposes: [.tty],
env: [:],
loginPATH: nil)
#expect(seeded == "/usr/bin:/bin:/usr/sbin:/sbin")
}
@Test
func resolvesCodexFromEnvOverride() {
let overridePath = "/custom/bin/codex"
let fm = MockFileManager(executables: [overridePath])
let resolved = BinaryLocator.resolveCodexBinary(
env: ["CODEX_CLI_PATH": overridePath],
loginPATH: nil,
fileManager: fm,
home: "/home/test")
#expect(resolved == overridePath)
}
@Test
func resolvesCodexFromLoginPath() {
let fm = MockFileManager(executables: ["/login/bin/codex"])
let resolved = BinaryLocator.resolveCodexBinary(
env: ["PATH": "/env/bin"],
loginPATH: ["/login/bin"],
fileManager: fm,
home: "/home/test")
#expect(resolved == "/login/bin/codex")
}
@Test
func resolvesCodexFromEnvPath() {
let fm = MockFileManager(executables: ["/env/bin/codex"])
let resolved = BinaryLocator.resolveCodexBinary(
env: ["PATH": "/env/bin:/usr/bin"],
loginPATH: nil,
fileManager: fm,
home: "/home/test")
#expect(resolved == "/env/bin/codex")
}
@Test
func resolvesCodexFromInteractiveShell() {
let fm = MockFileManager(executables: ["/shell/bin/codex"])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in
#expect(tool == "codex")
#expect(shell == "/bin/zsh")
#expect(timeout == 2.0)
_ = fileManager
return "/shell/bin/codex"
}
let resolved = BinaryLocator.resolveCodexBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
fileManager: fm,
home: "/home/test")
#expect(resolved == "/shell/bin/codex")
}
@Test
func resolvesClaudeFromInteractiveShell() {
let fm = MockFileManager(executables: ["/shell/bin/claude"])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in
#expect(tool == "claude")
#expect(shell == "/bin/zsh")
#expect(timeout == 2.0)
_ = fileManager
return "/shell/bin/claude"
}
let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
fileManager: fm,
home: "/home/test")
#expect(resolved == "/shell/bin/claude")
}
@Test
func resolvesGeminiFromInteractiveShell() {
let fm = MockFileManager(executables: ["/shell/bin/gemini"])
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { tool, shell, timeout, fileManager in
#expect(tool == "gemini")
#expect(shell == "/bin/zsh")
#expect(timeout == 2.0)
_ = fileManager
return "/shell/bin/gemini"
}
let resolved = BinaryLocator.resolveGeminiBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
fileManager: fm,
home: "/home/test")
#expect(resolved == "/shell/bin/gemini")
}
@Test
func resolvesClaudeFromLoginPath() {
let fm = MockFileManager(executables: ["/login/bin/claude"])
let resolved = BinaryLocator.resolveClaudeBinary(
env: ["PATH": "/env/bin"],
loginPATH: ["/login/bin"],
fileManager: fm,
home: "/home/test")
#expect(resolved == "/login/bin/claude")
}
@Test
func resolvesClaudeFromAliasWhenOtherLookupsFail() {
let aliasPath = "/home/test/.claude/local/bin/claude"
let fm = MockFileManager(executables: [aliasPath])
var aliasCalled = false
let aliasResolver: (String, String?, TimeInterval, FileManager, String)
-> String? = { tool, shell, timeout, _, home in
aliasCalled = true
#expect(tool == "claude")
#expect(shell == "/bin/zsh")
#expect(timeout == 2.0)
#expect(home == "/home/test")
return aliasPath
}
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in
nil
}
let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/home/test")
#expect(aliasCalled)
#expect(resolved == aliasPath)
}
@Test
func resolvesCodexFromAliasWhenOtherLookupsFail() {
let aliasPath = "/home/test/.codex/bin/codex"
let fm = MockFileManager(executables: [aliasPath])
var aliasCalled = false
let aliasResolver: (String, String?, TimeInterval, FileManager, String)
-> String? = { tool, shell, timeout, _, home in
aliasCalled = true
#expect(tool == "codex")
#expect(shell == "/bin/zsh")
#expect(timeout == 2.0)
#expect(home == "/home/test")
return aliasPath
}
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in
nil
}
let resolved = BinaryLocator.resolveCodexBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/home/test")
#expect(aliasCalled)
#expect(resolved == aliasPath)
}
@Test
func skipsAliasWhenCommandVResolves() {
let path = "/shell/bin/claude"
let fm = MockFileManager(executables: [path])
var aliasCalled = false
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in
aliasCalled = true
return "/alias/claude"
}
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in
path
}
let resolved = BinaryLocator.resolveClaudeBinary(
env: ["SHELL": "/bin/zsh"],
loginPATH: nil,
commandV: commandV,
aliasResolver: aliasResolver,
fileManager: fm,
home: "/home/test")
#expect(!aliasCalled)
#expect(resolved == path)
}
}
private final class MockFileManager: FileManager {
private let executables: Set<String>
init(executables: Set<String>) {
self.executables = executables
}
override func isExecutableFile(atPath path: String) -> Bool {
self.executables.contains(path)
}
}