956 lines
35 KiB
Swift
956 lines
35 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
@testable import CodexBarCore
|
|
|
|
struct PathBuilderTests {
|
|
@Test
|
|
func `merges login shell path when available`() {
|
|
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 `falls back to existing path when no login path`() {
|
|
let seeded = PathBuilder.effectivePATH(
|
|
purposes: [.tty],
|
|
env: ["PATH": "/custom/bin:/usr/bin"],
|
|
loginPATH: nil)
|
|
#expect(seeded == "/custom/bin:/usr/bin")
|
|
}
|
|
|
|
@Test
|
|
func `uses fallback when no path available`() {
|
|
let seeded = PathBuilder.effectivePATH(
|
|
purposes: [.tty],
|
|
env: [:],
|
|
loginPATH: nil)
|
|
#expect(seeded == "/usr/bin:/bin:/usr/sbin:/sbin")
|
|
}
|
|
|
|
@Test
|
|
func `debug snapshot async matches sync`() async {
|
|
let env = [
|
|
"CODEX_CLI_PATH": "/usr/bin/true",
|
|
"CLAUDE_CLI_PATH": "/usr/bin/true",
|
|
"GEMINI_CLI_PATH": "/usr/bin/true",
|
|
"PATH": "/usr/bin:/bin",
|
|
]
|
|
let sync = PathBuilder.debugSnapshot(purposes: [.rpc], env: env, home: "/tmp")
|
|
let async = await PathBuilder.debugSnapshotAsync(purposes: [.rpc], env: env, home: "/tmp")
|
|
#expect(async == sync)
|
|
}
|
|
|
|
@Test
|
|
func `login shell cache retries after timed out nil capture`() async {
|
|
let capture = LoginShellPathCaptureStub([
|
|
nil,
|
|
["/login/bin", "/usr/bin"],
|
|
])
|
|
|
|
let cache = LoginShellPathCache { _, _ in capture.next() }
|
|
let firstResult: [String]? = await withCheckedContinuation { continuation in
|
|
cache.captureOnce(shell: "/unused", timeout: 0.01) { result in
|
|
continuation.resume(returning: result)
|
|
}
|
|
}
|
|
|
|
#expect(firstResult == nil)
|
|
#expect(cache.current == nil)
|
|
|
|
let recovered = cache.currentOrCapture(shell: "/unused", timeout: 2.0)
|
|
#expect(recovered == ["/login/bin", "/usr/bin"])
|
|
#expect(cache.current == ["/login/bin", "/usr/bin"])
|
|
#expect(capture.callCount == 2)
|
|
}
|
|
|
|
@Test
|
|
func `shell runner drains noisy stdout and stderr`() throws {
|
|
let script = """
|
|
i=0
|
|
while [ "$i" -lt 4000 ]; do
|
|
printf 'out-%04d\\n' "$i"
|
|
printf 'err-%04d\\n' "$i" >&2
|
|
i=$((i + 1))
|
|
done
|
|
printf '__CODEXBAR_DONE__\\n'
|
|
"""
|
|
let data = try #require(ShellCommandLocator.test_runShellCommand(
|
|
shell: "/bin/sh",
|
|
arguments: ["-c", script],
|
|
timeout: 4.0))
|
|
let output = try #require(String(data: data, encoding: .utf8))
|
|
|
|
#expect(output.contains("out-3999"))
|
|
#expect(output.contains("__CODEXBAR_DONE__"))
|
|
}
|
|
|
|
@Test
|
|
func `shell runner terminates background children after normal exit`() throws {
|
|
let marker = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("codexbar-shell-runner-\(UUID().uuidString)")
|
|
.path
|
|
let escapedMarker = Self.shellSingleQuoted(marker)
|
|
let script = """
|
|
(
|
|
trap '' TERM
|
|
touch \(escapedMarker)
|
|
while :; do sleep 1; done
|
|
) &
|
|
printf '%s\\n' "$!"
|
|
"""
|
|
let data = try #require(ShellCommandLocator.test_runShellCommand(
|
|
shell: "/bin/sh",
|
|
arguments: ["-c", script],
|
|
timeout: 2.0))
|
|
let pidText = try #require(String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
let pid = try #require(pid_t(pidText))
|
|
|
|
defer {
|
|
kill(pid, SIGKILL)
|
|
try? FileManager.default.removeItem(atPath: marker)
|
|
}
|
|
|
|
let deadline = Date().addingTimeInterval(2.0)
|
|
while kill(pid, 0) == 0, Date() < deadline {
|
|
usleep(50000 as useconds_t)
|
|
}
|
|
|
|
#expect(kill(pid, 0) != 0)
|
|
}
|
|
|
|
@Test
|
|
func `resolves codex from env override`() {
|
|
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 `resolves codex from login path`() {
|
|
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 `resolves codex from env path`() {
|
|
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 `resolves codex from bundled ChatGPT app`() {
|
|
let appPath = "/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [appPath])
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/missing/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { _, _ in true },
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == appPath)
|
|
}
|
|
|
|
@Test
|
|
func `resolves codex from user bundled ChatGPT app`() {
|
|
let appPath = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [appPath])
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/missing/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { _, _ in true },
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == appPath)
|
|
}
|
|
|
|
@Test
|
|
func `prefers bundled ChatGPT app over legacy Codex app within one scope`() {
|
|
let chatGPTPath = "/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let codexPath = "/Applications/Codex.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [chatGPTPath, codexPath])
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/missing/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { _, _ in true },
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == chatGPTPath)
|
|
}
|
|
|
|
@Test
|
|
func `preserves user app precedence over system ChatGPT app`() {
|
|
let userCodexPath = "/Users/test/Applications/Codex.app/Contents/Resources/codex"
|
|
let systemChatGPTPath = "/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [userCodexPath, systemChatGPTPath])
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/missing/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { _, _ in true },
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == userCodexPath)
|
|
}
|
|
|
|
@Test
|
|
func `skips blocked ChatGPT app and falls back to legacy Codex app`() {
|
|
let chatGPTPath = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let codexPath = "/Users/test/Applications/Codex.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [chatGPTPath, codexPath])
|
|
var checked: [String] = []
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/missing/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { path, _ in
|
|
checked.append(path)
|
|
return path != chatGPTPath
|
|
},
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == codexPath)
|
|
#expect(checked == [chatGPTPath, codexPath])
|
|
}
|
|
|
|
@Test
|
|
func `skips blocked codex path and falls back to signed app binary`() {
|
|
let blockedPath = "/usr/local/bin/codex"
|
|
let appPath = "/Applications/Codex.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [blockedPath, appPath])
|
|
var checked: [String] = []
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["PATH": "/usr/local/bin"],
|
|
loginPATH: nil,
|
|
commandV: { _, _, _, _ in nil },
|
|
aliasResolver: { _, _, _, _, _ in nil },
|
|
launchCandidateFilter: { path, _ in
|
|
checked.append(path)
|
|
return path != blockedPath
|
|
},
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == appPath)
|
|
#expect(checked == [blockedPath, appPath])
|
|
}
|
|
|
|
@Test
|
|
func `explicit codex override bypasses launch candidate fallback`() {
|
|
let overridePath = "/custom/bin/codex"
|
|
let appPath = "/Applications/Codex.app/Contents/Resources/codex"
|
|
let fm = MockFileManager(executables: [overridePath, appPath])
|
|
var checked: [String] = []
|
|
|
|
let resolved = BinaryLocator.resolveCodexBinary(
|
|
env: ["CODEX_CLI_PATH": overridePath],
|
|
loginPATH: nil,
|
|
launchCandidateFilter: { path, _ in
|
|
checked.append(path)
|
|
return false
|
|
},
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
|
|
#expect(resolved == overridePath)
|
|
#expect(checked.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `Codex CLI strategy availability uses filtered binary resolution`() {
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let unavailable = CodexCLIUsageStrategy.resolvedBinary(
|
|
env: ["PATH": "/missing/bin", "SHELL": "/bin/sh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: MockFileManager(executables: []),
|
|
home: "/home/test")
|
|
#expect(unavailable == nil)
|
|
|
|
let available = CodexCLIUsageStrategy.resolvedBinary(
|
|
env: ["PATH": "/tools/bin", "SHELL": "/bin/sh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: MockFileManager(executables: ["/tools/bin/codex"]),
|
|
home: "/home/test")
|
|
#expect(available == "/tools/bin/codex")
|
|
}
|
|
|
|
#if os(macOS)
|
|
@Test
|
|
func `Codex launch preflight allows quarantined notarized native binary`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/Applications/Codex.app/Contents/Resources/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { _ in .init(output: "accepted\nsource=Notarized Developer ID", exitStatus: 0) },
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks malware attribute before assessment`() {
|
|
var assessed = false
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/Applications/Codex.app/Contents/Resources/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.malware" },
|
|
spctlAssessment: { _ in
|
|
assessed = true
|
|
return .init(output: "accepted\nsource=Notarized Developer ID", exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
#expect(!assessed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight validates containing app bundle`() {
|
|
let executable = "/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Applications/ChatGPT.app"
|
|
var assessedPaths: [String] = []
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { path, name in
|
|
path == bundle && name == "com.apple.quarantine"
|
|
},
|
|
spctlAssessment: { path in
|
|
assessedPaths.append(path)
|
|
return .init(output: "\(path): accepted\nsource=Notarized Developer ID", exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { path in path == bundle },
|
|
isMachOExecutable: { path in path == executable })
|
|
|
|
#expect(allowed)
|
|
#expect(assessedPaths == [bundle])
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks unexpected app signing identity`() {
|
|
let executable = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Users/test/Applications/ChatGPT.app"
|
|
var assessed = false
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { _ in
|
|
assessed = true
|
|
return .init(output: "accepted", exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { path in
|
|
#expect(path == bundle)
|
|
return false
|
|
},
|
|
isMachOExecutable: { path in path == executable })
|
|
|
|
#expect(!allowed)
|
|
#expect(!assessed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks rejected containing app bundle`() {
|
|
let executable = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Users/test/Applications/ChatGPT.app"
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { path in
|
|
#expect(path == bundle)
|
|
return .init(output: "\(path): rejected\nsource=no usable signature", exitStatus: 3)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { path in path == executable })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight requires successful app bundle assessment`() {
|
|
let executable = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Users/test/Applications/ChatGPT.app"
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { path in
|
|
#expect(path == bundle)
|
|
return .init(output: "\(path): accepted", exitStatus: 1)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { path in path == executable })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight rejects indeterminate app assessment`() {
|
|
let executable = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Users/test/Applications/ChatGPT.app"
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { path in
|
|
#expect(path == bundle)
|
|
return .init(output: "internal code signing error", exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { path in path == executable })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight fails closed when app bundle cannot be assessed`() {
|
|
let executable = "/Users/test/Applications/ChatGPT.app/Contents/Resources/codex"
|
|
let bundle = "/Users/test/Applications/ChatGPT.app"
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable,
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { path, name in
|
|
path == bundle && name == "com.apple.quarantine"
|
|
},
|
|
spctlAssessment: { path in
|
|
#expect(path == bundle)
|
|
return nil
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in false })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks app bundled executable symlink escaping the bundle`() throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let bundle = root.appendingPathComponent("ChatGPT.app")
|
|
let resources = bundle.appendingPathComponent("Contents/Resources")
|
|
let executable = resources.appendingPathComponent("codex")
|
|
let escapedTarget = root.appendingPathComponent("outside-codex")
|
|
try FileManager.default.createDirectory(at: resources, withIntermediateDirectories: true)
|
|
try Data().write(to: escapedTarget)
|
|
try FileManager.default.createSymbolicLink(at: executable, withDestinationURL: escapedTarget)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
var assessed = false
|
|
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: executable.path,
|
|
fileManager: FileManager.default,
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { _ in
|
|
assessed = true
|
|
return .init(output: "accepted", exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
#expect(!assessed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks quarantined script without native assessment`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/opt/homebrew/bin/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { _ in nil },
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in false })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks revoked assessment`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/Applications/Codex.app/Contents/Resources/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { _ in .init(output: "rejected\nCSSMERR_TP_CERT_REVOKED", exitStatus: 3) },
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks generic Gatekeeper rejection`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/opt/homebrew/bin/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, _ in false },
|
|
spctlAssessment: { _ in .init(output: "rejected\nsource=no usable signature", exitStatus: 3) },
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight allows valid signed command line binary assessment`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/opt/homebrew/bin/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { path in
|
|
.init(
|
|
output: "\(path): rejected (the code is valid but does not seem to be an app)",
|
|
exitStatus: 3)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight blocks revoked assessment even with non app rejection text`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/opt/homebrew/bin/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { _ in
|
|
.init(
|
|
output: """
|
|
rejected (the code is valid but does not seem to be an app)
|
|
CSSMERR_TP_CERT_REVOKED
|
|
""",
|
|
exitStatus: 3)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight ignores benign text in path when verdict is generic rejection`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/tmp/code is valid but does not seem to be an app/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { path in
|
|
.init(output: "\(path): rejected\nsource=no usable signature", exitStatus: 3)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight ignores benign text before verdict separator`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/tmp/x: code is valid but does not seem to be an app/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { path in
|
|
.init(output: "\(path): rejected\nsource=no usable signature", exitStatus: 3)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(!allowed)
|
|
}
|
|
|
|
@Test
|
|
func `Codex launch preflight ignores blocked words in accepted path and source fields`() {
|
|
let allowed = CodexLaunchPreflight.isLaunchCandidateAllowed(
|
|
path: "/tmp/rejected/quarantine/codex",
|
|
fileManager: MockFileManager(executables: []),
|
|
hasExtendedAttribute: { _, name in name == "com.apple.quarantine" },
|
|
spctlAssessment: { path in
|
|
.init(
|
|
output: """
|
|
\(path): accepted
|
|
source=revoked quarantine marker
|
|
origin=malware test fixture
|
|
""",
|
|
exitStatus: 0)
|
|
},
|
|
appSignatureIsTrusted: { _ in true },
|
|
isMachOExecutable: { _ in true })
|
|
|
|
#expect(allowed)
|
|
}
|
|
#endif
|
|
|
|
@Test
|
|
func `resolves codex from interactive shell`() {
|
|
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 `resolves claude from interactive shell`() {
|
|
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 `resolves gemini from interactive shell`() {
|
|
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 `resolves claude from login path`() {
|
|
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 `resolves claude from alias when other lookups fail`() {
|
|
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 `resolves codex from alias when other lookups fail`() {
|
|
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 `resolves claude from well-known cmux path when shell lookups fail`() {
|
|
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
|
|
let fm = MockFileManager(executables: [cmuxPath])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == cmuxPath)
|
|
}
|
|
|
|
@Test
|
|
func `resolves claude from well-known home dir path`() {
|
|
let homePath = "/Users/test/.claude/bin/claude"
|
|
let fm = MockFileManager(executables: [homePath])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == homePath)
|
|
}
|
|
|
|
@Test
|
|
func `resolves claude from native installer path`() {
|
|
let nativePath = "/Users/test/.local/bin/claude"
|
|
let fm = MockFileManager(executables: [nativePath])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == nativePath)
|
|
}
|
|
|
|
@Test
|
|
func `prefers migrated local claude path over legacy home dir path`() {
|
|
let migratedPath = "/Users/test/.claude/local/claude"
|
|
let legacyPath = "/Users/test/.claude/bin/claude"
|
|
let fm = MockFileManager(executables: [migratedPath, legacyPath])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == migratedPath)
|
|
}
|
|
|
|
@Test
|
|
func `prefers user managed well-known path over cmux path`() {
|
|
let homePath = "/Users/test/.claude/bin/claude"
|
|
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
|
|
let fm = MockFileManager(executables: [homePath, cmuxPath])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == homePath)
|
|
}
|
|
|
|
@Test
|
|
func `prefers homebrew arm path over usr local fallback`() {
|
|
let fm = MockFileManager(executables: [
|
|
"/opt/homebrew/bin/claude",
|
|
"/usr/local/bin/claude",
|
|
])
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in nil }
|
|
let aliasResolver: (String, String?, TimeInterval, FileManager, String) -> String? = { _, _, _, _, _ in nil }
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
aliasResolver: aliasResolver,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(resolved == "/opt/homebrew/bin/claude")
|
|
}
|
|
|
|
@Test
|
|
func `prefers well-known paths over interactive shell lookup`() {
|
|
let shellPath = "/custom/bin/claude"
|
|
let cmuxPath = "/Applications/cmux.app/Contents/Resources/bin/claude"
|
|
let fm = MockFileManager(executables: [shellPath, cmuxPath])
|
|
var shellLookupCalled = false
|
|
let commandV: (String, String?, TimeInterval, FileManager) -> String? = { _, _, _, _ in
|
|
shellLookupCalled = true
|
|
return shellPath
|
|
}
|
|
|
|
let resolved = BinaryLocator.resolveClaudeBinary(
|
|
env: ["SHELL": "/bin/zsh"],
|
|
loginPATH: nil,
|
|
commandV: commandV,
|
|
fileManager: fm,
|
|
home: "/Users/test")
|
|
#expect(!shellLookupCalled)
|
|
#expect(resolved == cmuxPath)
|
|
}
|
|
|
|
@Test
|
|
func `skips alias when command V resolves`() {
|
|
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 static func shellSingleQuoted(_ value: String) -> String {
|
|
"'\(value.replacingOccurrences(of: "'", with: "'\\''"))'"
|
|
}
|
|
}
|
|
|
|
private final class LoginShellPathCaptureStub: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var results: [[String]?]
|
|
private var callCountStorage = 0
|
|
|
|
var callCount: Int {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.callCountStorage
|
|
}
|
|
|
|
init(_ results: [[String]?]) {
|
|
self.results = results
|
|
}
|
|
|
|
func next() -> [String]? {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
self.callCountStorage += 1
|
|
return self.results.isEmpty ? nil : self.results.removeFirst()
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|