import Foundationimport Testing@testable import CodexBar@testable import CodexBarCore struct ClaudeUsageTests { private actor AsyncCounter { private var value = 0 func increment() -> Int { self.value += 1 return self.value } func current() -> Int { self.value } } private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse { let json = """ { "five_hour": { "utilization": 7, "resets_at": "2025-12-23T16:00:00.000Z" }, "seven_day": { "utilization": 21, "resets_at": "2025-12-29T23:00:00.000Z" } } """ return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8)) } @Test func `parses usage JSON with sonnet limit`() { let json = """ { "ok": true, "session_5h": { "pct_used": 1, "resets": "11am (Europe/Vienna)" }, "week_all_models": { "pct_used": 8, "resets": "Nov 21 at 5am (Europe/Vienna)" }, "week_sonnet": { "pct_used": 0, "resets": "Nov 21 at 5am (Europe/Vienna)" } } """ let data = Data(json.utf8) let snap = ClaudeUsageFetcher.parse(json: data) #expect(snap != nil) #expect(snap?.primary.usedPercent == 1) #expect(snap?.primary.windowMinutes == 300) #expect(snap?.secondary?.usedPercent == 8) #expect(snap?.secondary?.windowMinutes == 10080) #expect(snap?.primary.resetDescription == "11am (Europe/Vienna)") } @Test func `oauth delegated retry retries once then succeeds`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride, operation: { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride .withValue(loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) }) } } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) #expect(snapshot.primary.usedPercent == 7) #expect(snapshot.secondary?.usedPercent == 21) } @Test func `oauth delegated retry second attempt still expired fails cleanly`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) do { let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } _ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) } } Issue.record("Expected delegated retry to fail when credentials remain expired") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("delegated Claude CLI refresh")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) } @Test func `oauth delegated retry auto mode cli unavailable fails fast`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let delegatedOverride: (@Sendable (Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .cliUnavailable } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) } } Issue.record("Expected delegated retry to fail fast when CLI is unavailable") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("Claude CLI is not available")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } // Auto-mode: should not attempt a second credential load. #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 1) } @Test func `oauth delegated retry auto mode attempted failed then non interactive reload succeeds`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] } let flags = FlagBox() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable (Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedFailed("no-change") } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride, operation: { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride .withValue(loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) }) } } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) #expect(snapshot.primary.usedPercent == 7) // User-initiated repair: if the delegated refresh couldn't sync silently, we may allow an interactive prompt // on the retry to help recovery. #expect(flags.allowKeychainPromptFlags.count == 2) #expect(flags.allowKeychainPromptFlags[1] == true) } @Test func `oauth delegated retry only on user action background suppresses delegation`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework, operation: { try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } }) Issue.record("Expected delegated refresh to be suppressed in background") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("background repair is suppressed")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 0) } @Test func `oauth delegated retry never background suppresses delegation even for CLI`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true, allowBackgroundDelegatedRefresh: true) let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework, operation: { try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } }) Issue.record("Expected delegated refresh to be suppressed for prompt policy 'never'") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("Delegated refresh is disabled by 'never' keychain policy")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 0) } @Test func `oauth bootstrap only on user action background startup allows interactive read when no cache`() async throws { final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] var allowBackgroundPromptBootstrapFlags: [Bool] = [] } let flags = FlagBox() let usageResponse = try Self.makeOAuthUsageResponse() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true, allowStartupBootstrapPrompt: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) flags.allowBackgroundPromptBootstrapFlags.append(ClaudeOAuthCredentialsStore.allowBackgroundPromptBootstrap) return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderRefreshContext.$current.withValue(.startup) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$hasCachedCredentialsOverride.withValue(false) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } } } #expect(flags.allowKeychainPromptFlags == [true]) #expect(flags.allowBackgroundPromptBootstrapFlags == [true]) #expect(snapshot.primary.usedPercent == 7) } @Test func `oauth delegated retry only on user action background allows delegation for CLI`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] } let flags = FlagBox() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: false, allowBackgroundDelegatedRefresh: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue(delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } }Lines 1–500 of 1507. Load more
import Foundation import Testing @testable import CodexBar @testable import CodexBarCore struct ClaudeUsageTests { private actor AsyncCounter { private var value = 0 func increment() -> Int { self.value += 1 return self.value } func current() -> Int { self.value } } private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse { let json = """ { "five_hour": { "utilization": 7, "resets_at": "2025-12-23T16:00:00.000Z" }, "seven_day": { "utilization": 21, "resets_at": "2025-12-29T23:00:00.000Z" } } """ return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8)) } @Test func `parses usage JSON with sonnet limit`() { let json = """ { "ok": true, "session_5h": { "pct_used": 1, "resets": "11am (Europe/Vienna)" }, "week_all_models": { "pct_used": 8, "resets": "Nov 21 at 5am (Europe/Vienna)" }, "week_sonnet": { "pct_used": 0, "resets": "Nov 21 at 5am (Europe/Vienna)" } } """ let data = Data(json.utf8) let snap = ClaudeUsageFetcher.parse(json: data) #expect(snap != nil) #expect(snap?.primary.usedPercent == 1) #expect(snap?.primary.windowMinutes == 300) #expect(snap?.secondary?.usedPercent == 8) #expect(snap?.secondary?.windowMinutes == 10080) #expect(snap?.primary.resetDescription == "11am (Europe/Vienna)") } @Test func `oauth delegated retry retries once then succeeds`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride, operation: { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride .withValue(loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) }) } } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) #expect(snapshot.primary.usedPercent == 7) #expect(snapshot.secondary?.usedPercent == 21) } @Test func `oauth delegated retry second attempt still expired fails cleanly`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) do { let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } _ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) } } Issue.record("Expected delegated retry to fail when credentials remain expired") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("delegated Claude CLI refresh")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) } @Test func `oauth delegated retry auto mode cli unavailable fails fast`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let delegatedOverride: (@Sendable (Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .cliUnavailable } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) } } Issue.record("Expected delegated retry to fail fast when CLI is unavailable") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("Claude CLI is not available")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } // Auto-mode: should not attempt a second credential load. #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 1) } @Test func `oauth delegated retry auto mode attempted failed then non interactive reload succeeds`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] } let flags = FlagBox() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable (Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedFailed("no-change") } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.userInitiated) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride, operation: { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride, operation: { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride .withValue(loadCredsOverride, operation: { try await fetcher.loadLatestUsage(model: "sonnet") }) }) }) } } #expect(await loadCounter.current() == 2) #expect(await delegatedCounter.current() == 1) #expect(snapshot.primary.usedPercent == 7) // User-initiated repair: if the delegated refresh couldn't sync silently, we may allow an interactive prompt // on the retry to help recovery. #expect(flags.allowKeychainPromptFlags.count == 2) #expect(flags.allowKeychainPromptFlags[1] == true) } @Test func `oauth delegated retry only on user action background suppresses delegation`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true) let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework, operation: { try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } }) Issue.record("Expected delegated refresh to be suppressed in background") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("background repair is suppressed")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 0) } @Test func `oauth delegated retry never background suppresses delegation even for CLI`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true, allowBackgroundDelegatedRefresh: true) let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, _ in _ = await loadCounter.increment() throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } do { _ = try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting( .securityFramework, operation: { try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.never) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue( delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue( loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } }) Issue.record("Expected delegated refresh to be suppressed for prompt policy 'never'") } catch let error as ClaudeUsageError { guard case let .oauthFailed(message) = error else { Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)") return } #expect(message.contains("Delegated refresh is disabled by 'never' keychain policy")) } catch { Issue.record("Expected ClaudeUsageError, got \(error)") } #expect(await loadCounter.current() == 1) #expect(await delegatedCounter.current() == 0) } @Test func `oauth bootstrap only on user action background startup allows interactive read when no cache`() async throws { final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] var allowBackgroundPromptBootstrapFlags: [Bool] = [] } let flags = FlagBox() let usageResponse = try Self.makeOAuthUsageResponse() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: true, allowStartupBootstrapPrompt: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) flags.allowBackgroundPromptBootstrapFlags.append(ClaudeOAuthCredentialsStore.allowBackgroundPromptBootstrap) return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderRefreshContext.$current.withValue(.startup) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$hasCachedCredentialsOverride.withValue(false) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } } } } #expect(flags.allowKeychainPromptFlags == [true]) #expect(flags.allowBackgroundPromptBootstrapFlags == [true]) #expect(snapshot.primary.usedPercent == 7) } @Test func `oauth delegated retry only on user action background allows delegation for CLI`() async throws { let loadCounter = AsyncCounter() let delegatedCounter = AsyncCounter() let usageResponse = try Self.makeOAuthUsageResponse() final class FlagBox: @unchecked Sendable { var allowKeychainPromptFlags: [Bool] = [] } let flags = FlagBox() let fetcher = ClaudeUsageFetcher( browserDetection: BrowserDetection(cacheTTL: 0), environment: [:], dataSource: .oauth, oauthKeychainPromptCooldownEnabled: false, allowBackgroundDelegatedRefresh: true) let fetchOverride: (@Sendable (String) async throws -> OAuthUsageResponse)? = { _ in usageResponse } let delegatedOverride: (@Sendable ( Date, TimeInterval, [String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome)? = { _, _, _ in _ = await delegatedCounter.increment() return .attemptedSucceeded } let loadCredsOverride: (@Sendable ( [String: String], Bool, Bool) async throws -> ClaudeOAuthCredentials)? = { _, allowKeychainPrompt, _ in flags.allowKeychainPromptFlags.append(allowKeychainPrompt) let call = await loadCounter.increment() if call == 1 { throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI } return ClaudeOAuthCredentials( accessToken: "fresh-token", refreshToken: "refresh-token", expiresAt: Date(timeIntervalSinceNow: 3600), scopes: ["user:profile"], rateLimitTier: nil) } let snapshot = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) { try await ProviderInteractionContext.$current.withValue(.background) { try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) { try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue(delegatedOverride) { try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) { try await fetcher.loadLatestUsage(model: "sonnet") } } } }