Files
steipete--codexbar/Tests/CodexBarTests/ClaudeOAuthCredentialsStoreNeverPromptCacheTests.swift
2026-07-13 12:22:33 +08:00

694 lines
32 KiB
Swift

import Foundation
import Security
import Testing
@testable import CodexBarCore
@Suite(.serialized)
struct ClaudeOAuthCredentialsStoreNeverPromptCacheTests {
private struct TestState {
let cacheKey = KeychainCacheStore.Key.oauth(provider: .claude)
let pendingStore: ClaudeOAuthCredentialsStore.PendingCacheClearMemoryStore
let recorder: ClaudeOAuthCredentialsStore.OAuthCacheOperationRecorder
}
private func makeCredentialsData(accessToken: String, expiresAt: Date, refreshToken: String? = nil) -> Data {
let millis = Int(expiresAt.timeIntervalSince1970 * 1000)
let refreshField: String = {
guard let refreshToken else { return "" }
return ",\n \"refreshToken\": \"\(refreshToken)\""
}()
let json = """
{
"claudeAiOauth": {
"accessToken": "\(accessToken)",
"expiresAt": \(millis),
"scopes": ["user:profile"]\(refreshField)
}
}
"""
return Data(json.utf8)
}
private func withTestState<T>(_ operation: (TestState) throws -> T) throws -> T {
let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"
let pendingStore = ClaudeOAuthCredentialsStore.PendingCacheClearMemoryStore()
let recorder = ClaudeOAuthCredentialsStore.OAuthCacheOperationRecorder()
let fingerprintStore = ClaudeOAuthCredentialsStore.ClaudeKeychainFingerprintStore()
let state = TestState(pendingStore: pendingStore, recorder: recorder)
return try KeychainCacheStore.withServiceOverrideForTesting(service) {
KeychainCacheStore.setTestStoreForTesting(true)
defer { KeychainCacheStore.setTestStoreForTesting(false) }
return try KeychainAccessGate.withTaskOverrideForTesting(false) {
try ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(false) {
try ClaudeOAuthCredentialsStore.withPendingCacheClearStoreOverrideForTesting(pendingStore) {
try ClaudeOAuthCredentialsStore.withOAuthCacheOperationRecorderForTesting(recorder) {
try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
try ClaudeOAuthCredentialsStore
.withClaudeKeychainFingerprintStoreOverrideForTesting(fingerprintStore) {
try operation(state)
}
}
}
}
}
}
}
}
}
private func withCredentialsFile<T>(
data: Data?,
operation: (URL) throws -> T) throws -> T
{
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDirectory) }
let fileURL = tempDirectory.appendingPathComponent("credentials.json")
if let data {
try data.write(to: fileURL)
}
return try ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(fileURL) {
try operation(fileURL)
}
}
private func seedCache(
_ state: TestState,
accessToken: String,
storedAt: Date = Date())
{
let data = self.makeCredentialsData(
accessToken: accessToken,
expiresAt: Date(timeIntervalSinceNow: 3600))
let stored = ClaudeOAuthCredentialsStore.withOAuthCacheOperationRecorderForTesting(nil) {
KeychainCacheStore.storeResult(
key: state.cacheKey,
entry: ClaudeOAuthCredentialsStore.CacheEntry(data: data, storedAt: storedAt))
}
#expect(stored)
}
private func cachedToken(_ state: TestState) throws -> String? {
try ClaudeOAuthCredentialsStore.withOAuthCacheOperationRecorderForTesting(nil) {
switch KeychainCacheStore.load(
key: state.cacheKey,
as: ClaudeOAuthCredentialsStore.CacheEntry.self)
{
case let .found(entry):
return try ClaudeOAuthCredentials.parse(data: entry.data).accessToken
case .missing:
return nil
case .invalid, .temporarilyUnavailable:
Issue.record("Expected a valid or missing test cache entry")
return nil
}
}
}
private func runDefaults(_ arguments: [String]) throws -> (status: Int32, output: String) {
let process = Process()
let output = Pipe()
process.executableURL = URL(fileURLWithPath: "/usr/bin/defaults")
process.arguments = arguments
process.standardOutput = output
process.standardError = output
try process.run()
process.waitUntilExit()
let data = output.fileHandleForReading.readDataToEndOfFile()
return (process.terminationStatus, String(data: data, encoding: .utf8) ?? "")
}
@Test
func `never mode loads the credentials file with zero oauth cache IO`() throws {
try self.withTestState { state in
let fileData = self.makeCredentialsData(
accessToken: "file-token",
expiresAt: Date(timeIntervalSinceNow: 3600))
try self.withCredentialsFile(data: fileData) { _ in
self.seedCache(state, accessToken: "cached-token")
let credentials = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
}
}
#expect(credentials.accessToken == "file-token")
#expect(state.recorder.operations.isEmpty)
#expect(state.pendingStore.isPending)
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
}
}
}
@Test
func `never mode file invalidation records a tombstone without oauth cache IO`() throws {
try self.withTestState { state in
let initialData = self.makeCredentialsData(
accessToken: "initial-token",
expiresAt: Date(timeIntervalSinceNow: 3600))
try self.withCredentialsFile(data: initialData) { fileURL in
self.seedCache(state, accessToken: "cached-token")
let initialChange = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()
}
#expect(initialChange)
let updatedData = self.makeCredentialsData(
accessToken: "updated-token-with-a-different-size",
expiresAt: Date(timeIntervalSinceNow: 7200))
try updatedData.write(to: fileURL)
let changed = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()
}
let changedAgain = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()
}
#expect(changed)
#expect(!changedAgain)
#expect(state.recorder.operations.isEmpty)
#expect(state.pendingStore.isPending)
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
}
}
}
@Test
func `never mode has cached credentials ignores stale oauth cache with zero IO`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
let hasCached = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ProviderInteractionContext.$current.withValue(.background) {
ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: [:])
}
}
#expect(!hasCached)
#expect(state.recorder.operations.isEmpty)
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
}
}
}
@Test
func `has cached credentials ignores stale oauth cache when pending clear fails`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCache()
}
let hasCached = KeychainCacheStore.withClearFailureStatusOverrideForTesting(
errSecInteractionNotAllowed)
{
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
ProviderInteractionContext.$current.withValue(.background) {
ClaudeOAuthCredentialsStore.hasCachedCredentials(environment: [:])
}
}
}
#expect(!hasCached)
#expect(state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear])
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
}
}
}
@Test
func `leaving never mode clears stale oauth cache before repopulating from file`() throws {
try self.withTestState { state in
let fileData = self.makeCredentialsData(
accessToken: "file-token-new",
expiresAt: Date(timeIntervalSinceNow: 3600))
try self.withCredentialsFile(data: fileData) { _ in
self.seedCache(
state,
accessToken: "cached-token",
storedAt: Date(timeIntervalSince1970: 0))
_ = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCacheIfCredentialsFileChanged()
}
#expect(state.pendingStore.isPending)
#expect(state.recorder.operations.isEmpty)
let credentials = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(
.onlyOnUserAction)
{
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
}
}
#expect(credentials.accessToken == "file-token-new")
#expect(!state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .load, .store])
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "file-token-new")
}
}
}
@Test
func `logout under never mode clears stale oauth cache after access is reenabled`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCache()
}
#expect(state.pendingStore.isPending)
#expect(state.recorder.operations.isEmpty)
let staleToken = try self.cachedToken(state)
#expect(staleToken == "cached-token")
do {
_ = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
}
}
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
} catch let error as ClaudeOAuthCredentialsError {
guard case .notFound = error else {
Issue.record("Expected .notFound, got \(error)")
return
}
}
#expect(!state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .load])
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == nil)
}
}
}
@Test
func `pending oauth cache clear retries after a temporarily unavailable delete`() throws {
try self.withTestState { state in
let fileData = self.makeCredentialsData(
accessToken: "file-token-new",
expiresAt: Date(timeIntervalSinceNow: 3600))
try self.withCredentialsFile(data: fileData) { _ in
self.seedCache(state, accessToken: "cached-token")
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCache()
}
let first = try KeychainCacheStore.withClearFailureStatusOverrideForTesting(
errSecInteractionNotAllowed)
{
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
}
}
}
#expect(first.accessToken == "file-token-new")
#expect(state.pendingStore.isPending)
let staleToken = try self.cachedToken(state)
#expect(staleToken == "cached-token")
let second = try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(environment: [:], allowKeychainPrompt: false)
}
}
#expect(second.accessToken == "file-token-new")
#expect(!state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .clear, .load, .store])
let refreshedToken = try self.cachedToken(state)
#expect(refreshedToken == "file-token-new")
}
}
}
@Test
func `replacement store failure after successful clear keeps tombstone and cache missing`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCache()
}
let syncData = self.makeCredentialsData(
accessToken: "sync-token",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "sync-refresh-token")
let synced = KeychainCacheStore.withStoreFailureStatusOverrideForTesting(
errSecInteractionNotAllowed)
{
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
ProviderInteractionContext.$current.withValue(.userInitiated) {
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: syncData,
fingerprint: nil)
{
ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt()
}
}
}
}
#expect(synced)
#expect(state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .store])
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == nil)
}
}
}
@Test
func `replacement store failure after failed clear keeps tombstone and stale cache`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.invalidateCache()
}
let syncData = self.makeCredentialsData(
accessToken: "sync-token",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "sync-refresh-token")
let synced = KeychainCacheStore.withClearFailureStatusOverrideForTesting(
errSecInteractionNotAllowed)
{
KeychainCacheStore.withStoreFailureStatusOverrideForTesting(errSecInteractionNotAllowed) {
ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
ProviderInteractionContext.$current.withValue(.userInitiated) {
ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: syncData,
fingerprint: nil)
{
ClaudeOAuthCredentialsStore.syncFromClaudeKeychainWithoutPrompt()
}
}
}
}
}
#expect(synced)
#expect(state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .store])
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
}
}
}
@Test
func `bundled CLI resolves the owning app prompt policy domain`() throws {
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let appURL = tempDirectory.appendingPathComponent("CodexBar.app", isDirectory: true)
let contentsURL = appURL.appendingPathComponent("Contents", isDirectory: true)
let helpersURL = contentsURL.appendingPathComponent("Helpers", isDirectory: true)
let macOSURL = contentsURL.appendingPathComponent("MacOS", isDirectory: true)
let binURL = tempDirectory.appendingPathComponent("bin", isDirectory: true)
try FileManager.default.createDirectory(at: helpersURL, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: macOSURL, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: binURL, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: tempDirectory) }
let info: [String: Any] = [
"CFBundleExecutable": "CodexBar",
"CFBundleIdentifier": ClaudeOAuthKeychainPromptPreference.debugApplicationDefaultsDomain,
"CFBundlePackageType": "APPL",
]
let infoData = try PropertyListSerialization.data(
fromPropertyList: info,
format: .xml,
options: 0)
try infoData.write(to: contentsURL.appendingPathComponent("Info.plist"))
try Data().write(to: macOSURL.appendingPathComponent("CodexBar"))
let helperURL = helpersURL.appendingPathComponent("CodexBarCLI")
try Data().write(to: helperURL)
let symlinkURL = binURL.appendingPathComponent("codexbar")
try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: helperURL)
let bundledCLIDomain = ClaudeOAuthKeychainPromptPreference.resolveApplicationDefaultsDomain(
bundleIdentifier: nil,
bundleURL: nil,
executableURL: nil,
invocationURL: symlinkURL)
#expect(bundledCLIDomain == ClaudeOAuthKeychainPromptPreference.debugApplicationDefaultsDomain)
let debugWidgetDomain = ClaudeOAuthKeychainPromptPreference.resolveApplicationDefaultsDomain(
bundleIdentifier: "com.steipete.codexbar.debug.widget",
bundleURL: nil,
executableURL: nil,
invocationURL: nil)
#expect(debugWidgetDomain == ClaudeOAuthKeychainPromptPreference.debugApplicationDefaultsDomain)
let standaloneDomain = ClaudeOAuthKeychainPromptPreference.resolveApplicationDefaultsDomain(
bundleIdentifier: nil,
bundleURL: nil,
executableURL: URL(fileURLWithPath: "/usr/local/bin/codexbar"),
invocationURL: nil)
#expect(standaloneDomain == ClaudeOAuthKeychainPromptPreference.releaseApplicationDefaultsDomain)
let testProcessDomain = ClaudeOAuthKeychainPromptPreference.resolveApplicationDefaultsDomain(
bundleIdentifier: nil,
bundleURL: Bundle.main.bundleURL,
executableURL: Bundle.main.executableURL,
invocationURL: CommandLine.arguments.first.map(URL.init(fileURLWithPath:)),
bundleIdentifierForApp: { _ in nil })
#expect(testProcessDomain == ClaudeOAuthKeychainPromptPreference.releaseApplicationDefaultsDomain)
}
@Test
func `shared tombstone propagates across process boundaries`() throws {
let domain = "ClaudeOAuthPendingCacheTests.\(UUID().uuidString)"
let key = "pending"
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let lockURL = tempDirectory.appendingPathComponent("cache.lock")
let userDefaults = try #require(UserDefaults(suiteName: domain))
defer {
userDefaults.removePersistentDomain(forName: domain)
userDefaults.synchronize()
try? FileManager.default.removeItem(at: tempDirectory)
}
let store = ClaudeOAuthPendingCacheClearUserDefaultsStore(
domain: domain,
key: key,
lockURL: lockURL)
store.markPending()
let childRead = try self.runDefaults(["read", domain, key])
#expect(childRead.status == 0)
#expect(!childRead.output.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
let childDelete = try self.runDefaults(["delete", domain, key])
#expect(childDelete.status == 0)
#expect(!store.isPending)
let childWrite = try self.runDefaults(["write", domain, key, UUID().uuidString])
#expect(childWrite.status == 0)
#expect(store.isPending)
store.withCacheTransaction { pending in
pending = false
}
let childReadAfterResolution = try self.runDefaults(["read", domain, key])
#expect(childReadAfterResolution.status != 0)
}
@Test
func `newer tombstone survives an older cache transaction`() throws {
let domain = "ClaudeOAuthPendingCacheRaceTests.\(UUID().uuidString)"
let key = "pending"
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let lockURL = tempDirectory.appendingPathComponent("cache.lock")
let userDefaults = try #require(UserDefaults(suiteName: domain))
defer {
userDefaults.removePersistentDomain(forName: domain)
userDefaults.synchronize()
try? FileManager.default.removeItem(at: tempDirectory)
}
let store = ClaudeOAuthPendingCacheClearUserDefaultsStore(
domain: domain,
key: key,
lockURL: lockURL)
store.markPending()
let newerGeneration = UUID().uuidString
var childWriteStatus: Int32?
store.withCacheTransaction { pending in
childWriteStatus = try? self.runDefaults(["write", domain, key, newerGeneration]).status
pending = false
}
userDefaults.synchronize()
#expect(childWriteStatus == 0)
#expect(userDefaults.string(forKey: key) == newerGeneration)
#expect(store.isPending)
}
@Test
func `legacy boolean tombstone remains pending until cache resolution`() throws {
let domain = "ClaudeOAuthPendingCacheLegacyTests.\(UUID().uuidString)"
let key = "pending"
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let userDefaults = try #require(UserDefaults(suiteName: domain))
defer {
userDefaults.removePersistentDomain(forName: domain)
userDefaults.synchronize()
try? FileManager.default.removeItem(at: tempDirectory)
}
userDefaults.set(true, forKey: key)
userDefaults.synchronize()
let store = ClaudeOAuthPendingCacheClearUserDefaultsStore(
domain: domain,
key: key,
lockURL: tempDirectory.appendingPathComponent("cache.lock"))
#expect(store.isPending)
store.withCacheTransaction { pending in
pending = false
}
#expect(!store.isPending)
}
@Test
func `cache transaction fails closed when its lock is unavailable`() throws {
let domain = "ClaudeOAuthPendingCacheLockFailureTests.\(UUID().uuidString)"
let key = "pending"
let tempDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
let nonDirectoryURL = tempDirectory.appendingPathComponent("not-a-directory")
try Data().write(to: nonDirectoryURL)
let userDefaults = try #require(UserDefaults(suiteName: domain))
defer {
userDefaults.removePersistentDomain(forName: domain)
userDefaults.synchronize()
try? FileManager.default.removeItem(at: tempDirectory)
}
let store = ClaudeOAuthPendingCacheClearUserDefaultsStore(
domain: domain,
key: key,
lockURL: nonDirectoryURL.appendingPathComponent("cache.lock"))
var operationCalled = false
store.withCacheTransaction { _ in
operationCalled = true
}
userDefaults.synchronize()
#expect(!operationCalled)
#expect(userDefaults.string(forKey: key) != nil)
#expect(store.isPending)
}
@Test
func `never mode bypasses oauth cache while preserving experimental security CLI reader`() throws {
try self.withTestState { state in
try self.withCredentialsFile(data: nil) { _ in
self.seedCache(state, accessToken: "cached-token")
let securityData = self.makeCredentialsData(
accessToken: "security-cli-token",
expiresAt: Date(timeIntervalSinceNow: 3600),
refreshToken: "security-cli-refresh-token")
let credentials = try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental)
{
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(.data(securityData)) {
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
}
}
#expect(credentials.accessToken == "security-cli-token")
#expect(state.recorder.operations.isEmpty)
#expect(state.pendingStore.isPending)
let cachedToken = try self.cachedToken(state)
#expect(cachedToken == "cached-token")
do {
_ = try ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental)
{
try ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(.data(nil)) {
try ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: Data(),
fingerprint: nil)
{
try ProviderInteractionContext.$current.withValue(.background) {
try ClaudeOAuthCredentialsStore.load(
environment: [:],
allowKeychainPrompt: false)
}
}
}
}
}
}
Issue.record("Expected ClaudeOAuthCredentialsError.notFound")
} catch let error as ClaudeOAuthCredentialsError {
guard case .notFound = error else {
Issue.record("Expected .notFound, got \(error)")
return
}
}
#expect(!state.pendingStore.isPending)
#expect(state.recorder.operations == [.clear, .load])
let clearedToken = try self.cachedToken(state)
#expect(clearedToken == nil)
let mcpOnly = Data(#"{"mcpOAuth":{"plugin:test":{"accessToken":"synthetic"}}}"#.utf8)
let isMcpOnly = ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) {
ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(.data(mcpOnly)) {
ClaudeOAuthCredentialsStore.isMcpOAuthOnlyClaudeKeychainPayloadPresent(
interaction: .background,
readStrategy: .securityCLIExperimental,
keychainAccessDisabled: true,
environment: [
KeychainAccessGate.disableAccessEnvironmentKey: "1",
ClaudeOAuthCredentialsStore.isolatedSecurityCLIKeychainEnvironmentKey:
"/tmp/codexbar-test.keychain-db",
])
}
}
#expect(isMcpOnly)
}
}
}
}