461 lines · 21.8 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBar@testable import CodexBarCore @Suite(.serialized)struct ClaudeDebugDiagnosticsTests {    private func makeCredentialsData(        accessToken: String,        expiresAt: Date,        refreshToken: String? = nil) -> Data    {        let refreshTokenLine = if let refreshToken {            """                "refreshToken": "\(refreshToken)",            """        } else {            ""        }        let json = """        {          "claudeAiOauth": {            "accessToken": "\(accessToken)",        \(refreshTokenLine)            "expiresAt": \(Int(expiresAt.timeIntervalSince1970 * 1000)),            "scopes": ["user:profile"]          }        }        """        return Data(json.utf8)    }     @Test    func `debug log uses planner derived order and reasons`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let credentialsURL = tempDir.appendingPathComponent("credentials.json")        let credsJSON = """        {          "claudeAiOauth": {            "accessToken": "oauth-token",            "expiresAt": \(Int(Date(timeIntervalSinceNow: 3600).timeIntervalSince1970 * 1000)),            "scopes": ["user:profile"]          }        }        """        try Data(credsJSON.utf8).write(to: credentialsURL)         let store = try await MainActor.run { () -> UsageStore in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .auto            settings.claudeCookieSource = .manual            settings.claudeCookieHeader = "sessionKey=sk-ant-session-token"             return UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)        }         let text = await KeychainCacheStore.withServiceOverrideForTesting(service) {            KeychainCacheStore.setTestStoreForTesting(true)            defer { KeychainCacheStore.setTestStoreForTesting(false) }             return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                    await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(credentialsURL) {                        await store.debugLog(for: .claude)                    }                }            }        }         #expect(text.contains("planner_order=oauth→cli→web"))        #expect(text.contains("planner_selected=oauth"))        #expect(text.contains("planner_no_source=false"))        #expect(text.contains("planner_step.oauth=available reason=app-auto-preferred-oauth"))        #expect(text.contains("planner_step.cli="))        #expect(text.contains("reason=app-auto-fallback-cli"))        #expect(text.contains("planner_step.web=available reason=app-auto-fallback-web"))        #expect(!text.contains("auto_heuristic="))    }     @Test    func `debug log reports no planner selected source when auto has no available sources`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let missingCredentialsURL = tempDir.appendingPathComponent("missing-credentials.json")         let store = try await MainActor.run { () -> UsageStore in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .auto            settings.claudeCookieSource = .off            settings.claudeWebExtrasEnabled = true             return UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)        }         let text = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: nil,                                    fingerprint: nil)                                {                                    await store.debugLog(for: .claude)                                }                            }                        }                    }                }            }        }         #expect(text.contains("planner_selected=none"))        #expect(text.contains("planner_no_source=true"))        #expect(text.contains("No planner-selected Claude source."))        #expect(!text.contains("web_extras=enabled"))    }     @Test    func `debug Claude dump returns recorded parse dumps`() async {        await ClaudeStatusProbe._replaceDumpsForTesting([            "dump one",            "dump two",        ])         let store = await MainActor.run {            UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: SettingsStore(                    userDefaults: UserDefaults(),                    configStore: testConfigStore(suiteName: "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"),                    zaiTokenStore: NoopZaiTokenStore()))        }        let text = await store.debugClaudeDump()         #expect(text.contains("dump one"))        #expect(text.contains("dump two"))        #expect(!text.contains("planner_order="))        await ClaudeStatusProbe._replaceDumpsForTesting([])    }     @Test    func `debug log uses runtime OAuth availability for token account routing`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let missingCredentialsURL = tempDir.appendingPathComponent("missing-credentials.json")         let store = try await MainActor.run { () -> UsageStore in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .auto            settings.claudeCookieSource = .off            settings.addTokenAccount(                provider: .claude,                label: "OAuth Account",                token: "sk-ant-oat-test-token")             return UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)        }         let text = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: nil,                                    fingerprint: nil)                                {                                    await store.debugLog(for: .claude)                                }                            }                        }                    }                }            }        }         #expect(text.contains("planner_selected=oauth"))        #expect(text.contains("hasOAuthCredentials=true"))        #expect(text.contains("oauthCredentialOwner=environment"))        #expect(text.contains("oauthCredentialSource=environment"))        #expect(!text.contains("planner_selected=none"))    }     @Test    func `debug log preserves CLI probe overrides across detached work`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let missingCredentialsURL = tempDir.appendingPathComponent("missing-credentials.json")        let store = try await MainActor.run { () -> UsageStore in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .cli            settings.claudeCookieSource = .off             return UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)        }         let fetchOverride: ClaudeStatusProbe.FetchOverride = { resolved, _, _ in            #expect(resolved == "/usr/bin/true")            return ClaudeStatusSnapshot(                sessionPercentLeft: 76,                weeklyPercentLeft: 55,                opusPercentLeft: nil,                accountEmail: "cli@example.com",                accountOrganization: "CLI Org",                loginMethod: "cli",                primaryResetDescription: "Mar 7 at 1pm",                secondaryResetDescription: nil,                opusResetDescription: nil,                rawText: "probe raw")        }         let text = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/usr/bin/true") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: nil,                                    fingerprint: nil)                                {                                    await ClaudeStatusProbe.withFetchOverrideForTesting(fetchOverride) {                                        await store.debugLog(for: .claude)                                    }                                }                            }                        }                    }                }            }        }         #expect(text.contains("planner_selected=cli"))        #expect(text.contains("session_left=76.0 weekly_left=55.0"))        #expect(text.contains("email cli@example.com"))    }     @Test    func `debug log uses user initiated interaction for OAuth prompt gate`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let missingCredentialsURL = tempDir.appendingPathComponent("missing-credentials.json")        let securityData = self.makeCredentialsData(            accessToken: "user-initiated-oauth",            expiresAt: Date(timeIntervalSinceNow: 3600))        let deniedStore = ClaudeOAuthKeychainAccessGate.DeniedUntilStore()        deniedStore.deniedUntil = Date(timeIntervalSinceNow: 300)         let store = try await MainActor.run { () -> UsageStore in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .auto            settings.claudeCookieSource = .off             return UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)        }         let text = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthKeychainAccessGate.withDeniedUntilStoreOverrideForTesting(deniedStore) {                                await ClaudeOAuthKeychainPromptPreference                                    .withTaskOverrideForTesting(.onlyOnUserAction) {                                        await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(                                            .securityCLIExperimental)                                        {                                            await ClaudeOAuthCredentialsStore.withSecurityCLIReadOverrideForTesting(                                                .data(securityData))                                            {                                                await ProviderInteractionContext.$current.withValue(.userInitiated) {                                                    await store.debugLog(for: .claude)                                                }                                            }                                        }                                    }                            }                        }                    }                }            }        }         #expect(text.contains("planner_selected=oauth"))        #expect(text.contains("hasOAuthCredentials=true"))    }     @Test    func `debug log invalidates cached planner output when Claude settings change`() async throws {        let suite = "ClaudeDebugDiagnosticsTests-\(UUID().uuidString)"        let service = "com.steipete.codexbar.cache.tests.\(UUID().uuidString)"        let tempDir = FileManager.default.temporaryDirectory            .appendingPathComponent(UUID().uuidString, isDirectory: true)        try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true)        let missingCredentialsURL = tempDir.appendingPathComponent("missing-credentials.json")        let storeAndSettings = try await MainActor.run { () -> (UsageStore, SettingsStore) in            let defaults = try #require(UserDefaults(suiteName: suite))            defaults.removePersistentDomain(forName: suite)            let configStore = testConfigStore(suiteName: suite)            let settings = SettingsStore(                userDefaults: defaults,                configStore: configStore,                zaiTokenStore: NoopZaiTokenStore())            settings.claudeUsageDataSource = .cli            settings.claudeCookieSource = .off             let store = UsageStore(                fetcher: UsageFetcher(),                browserDetection: BrowserDetection(cacheTTL: 0),                settings: settings)            return (store, settings)        }        let store = storeAndSettings.0        let settings = storeAndSettings.1        let fetchOverride: ClaudeStatusProbe.FetchOverride = { _, _, _ in            ClaudeStatusSnapshot(                sessionPercentLeft: 80,                weeklyPercentLeft: 60,                opusPercentLeft: nil,                accountEmail: "cache@example.com",                accountOrganization: nil,                loginMethod: "cli",                primaryResetDescription: "Mar 7 at 1pm",                secondaryResetDescription: nil,                opusResetDescription: nil,                rawText: "probe raw")        }         let first = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/usr/bin/true") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(false) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: nil,                                    fingerprint: nil)                                {                                    await ClaudeStatusProbe.withFetchOverrideForTesting(fetchOverride) {                                        await store.debugLog(for: .claude)                                    }                                }                            }                        }                    }                }            }        }         await MainActor.run {            settings.claudeUsageDataSource = .auto        }        await Task.yield()        await Task.yield()         let second = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {            await KeychainCacheStore.withServiceOverrideForTesting(service) {                KeychainCacheStore.setTestStoreForTesting(true)                defer { KeychainCacheStore.setTestStoreForTesting(false) }                 return await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {                    await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {                        await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {                            await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) {                                await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(                                    data: nil,                                    fingerprint: nil)                                {                                    await store.debugLog(for: .claude)                                }                            }                        }                    }                }            }        }         #expect(first.contains("planner_selected=cli"))        #expect(second.contains("planner_selected=none"))    }}