Files
2026-07-13 12:22:33 +08:00

1511 lines
62 KiB
Swift

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
}
}
@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, Bool) 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, Bool) 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, Bool) 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, Bool) 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")
}
}
}
}
}
#expect(await loadCounter.current() == 2)
#expect(await delegatedCounter.current() == 1)
#expect(snapshot.primary.usedPercent == 7)
#expect(flags.allowKeychainPromptFlags.allSatisfy { !$0 })
}
@Test
func `parses usage JSON when weekly missing`() {
let json = """
{
"ok": true,
"session_5h": { "pct_used": 4, "resets": "11am (Europe/Vienna)" }
}
"""
let data = Data(json.utf8)
let snap = ClaudeUsageFetcher.parse(json: data)
#expect(snap != nil)
#expect(snap?.primary.usedPercent == 4)
#expect(snap?.secondary == nil)
}
@Test
func `parses legacy opus and account`() {
let json = """
{
"ok": true,
"session_5h": { "pct_used": 2, "resets": "10:59pm (Europe/Vienna)" },
"week_all_models": { "pct_used": 13, "resets": "Nov 21 at 4:59am (Europe/Vienna)" },
"week_opus": { "pct_used": 0, "resets": "" },
"account_email": " steipete@gmail.com ",
"account_org": ""
}
"""
let data = Data(json.utf8)
let snap = ClaudeUsageFetcher.parse(json: data)
#expect(snap?.opus?.usedPercent == 0)
#expect(snap?.opus?.windowMinutes == 10080)
#expect(snap?.opus?.resetDescription?.isEmpty == true)
#expect(snap?.accountEmail == "steipete@gmail.com")
#expect(snap?.accountOrganization == nil)
}
@Test
func `parses usage JSON when only sonnet limit is present`() {
let json = """
{
"ok": true,
"session_5h": { "pct_used": 3, "resets": "11am (Europe/Vienna)" },
"week_all_models": { "pct_used": 9, "resets": "Nov 21 at 5am (Europe/Vienna)" },
"week_sonnet_only": { "pct_used": 12, "resets": "Nov 22 at 5am (Europe/Vienna)" }
}
"""
let data = Data(json.utf8)
let snap = ClaudeUsageFetcher.parse(json: data)
#expect(snap?.secondary?.usedPercent == 9)
#expect(snap?.opus?.usedPercent == 12)
#expect(snap?.opus?.resetDescription == "Nov 22 at 5am (Europe/Vienna)")
}
@Test
func `trims account fields`() throws {
let cases: [[String: String?]] = [
["email": " steipete@gmail.com ", "org": " Org "],
["email": "", "org": " Claude Max Account "],
["email": nil, "org": " "],
]
for entry in cases {
var payload = [
"ok": true,
"session_5h": ["pct_used": 0, "resets": ""],
"week_all_models": ["pct_used": 0, "resets": ""],
] as [String: Any]
if let email = entry["email"] {
payload["account_email"] = email
}
if let org = entry["org"] {
payload["account_org"] = org
}
let data = try JSONSerialization.data(withJSONObject: payload)
let snap = ClaudeUsageFetcher.parse(json: data)
let emailRaw: String? = entry["email"] ?? String?.none
let expectedEmail = emailRaw?.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedEmail = (expectedEmail?.isEmpty ?? true) ? nil : expectedEmail
#expect(snap?.accountEmail == normalizedEmail)
let orgRaw: String? = entry["org"] ?? String?.none
let expectedOrg = orgRaw?.trimmingCharacters(in: .whitespacesAndNewlines)
let normalizedOrg = (expectedOrg?.isEmpty ?? true) ? nil : expectedOrg
#expect(snap?.accountOrganization == normalizedOrg)
}
}
@Test
func `live claude fetch PTY`() async throws {
guard ProcessInfo.processInfo.environment["LIVE_CLAUDE_FETCH"] == "1" else {
return
}
let fetcher = ClaudeUsageFetcher(browserDetection: BrowserDetection(cacheTTL: 0), dataSource: .cli)
do {
let snap = try await fetcher.loadLatestUsage()
let opusUsed = snap.opus?.usedPercent ?? -1
let weeklyUsed = snap.secondary?.usedPercent ?? -1
let email = snap.accountEmail ?? "nil"
let org = snap.accountOrganization ?? "nil"
print(
"""
Live Claude usage (PTY):
session used \(snap.primary.usedPercent)%
week used \(weeklyUsed)%
opus \(opusUsed)%
email \(email) org \(org)
""")
#expect(snap.primary.usedPercent >= 0)
} catch {
// Dump raw CLI text captured via `script` to help debug.
let raw = try Self.captureClaudeUsageRaw(timeout: 15)
print("RAW CLAUDE OUTPUT BEGIN\n\(raw)\nRAW CLAUDE OUTPUT END")
throw error
}
}
private static func captureClaudeUsageRaw(timeout: TimeInterval) throws -> String {
let process = Process()
process.launchPath = "/usr/bin/script"
process.arguments = [
"-q",
"/dev/null",
"claude",
"/usage",
"--allowed-tools",
"",
]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = Pipe()
process.standardInput = nil
try process.run()
DispatchQueue.global().asyncAfter(deadline: .now() + timeout) {
if process.isRunning {
process.terminate()
}
}
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: .utf8) ?? ""
}
// MARK: - Web API tests
@Test
func `live claude fetch web API`() async throws {
// Set LIVE_CLAUDE_WEB_FETCH=1 to run this test with real browser cookies
guard ProcessInfo.processInfo.environment["LIVE_CLAUDE_WEB_FETCH"] == "1" else {
return
}
let fetcher = ClaudeUsageFetcher(browserDetection: BrowserDetection(cacheTTL: 0), dataSource: .web)
let snap = try await fetcher.loadLatestUsage()
let weeklyUsed = snap.secondary?.usedPercent ?? -1
let opusUsed = snap.opus?.usedPercent ?? -1
print(
"""
Live Claude usage (Web API):
session used \(snap.primary.usedPercent)%
week used \(weeklyUsed)%
opus \(opusUsed)%
login method: \(snap.loginMethod ?? "nil")
""")
#expect(snap.primary.usedPercent >= 0)
}
@Test
func `claude web API has session key check`() {
// Quick check that hasSessionKey returns a boolean (doesn't crash)
let hasKey = ClaudeWebAPIFetcher.hasSessionKey(browserDetection: BrowserDetection(cacheTTL: 0))
// We can't assert the value since it depends on the test environment
#expect(hasKey == true || hasKey == false)
}
@Test
func `parses claude web API usage response`() throws {
let json = """
{
"five_hour": { "utilization": 9, "resets_at": "2025-12-23T16:00:00.000Z" },
"seven_day": { "utilization": 4, "resets_at": "2025-12-29T23:00:00.000Z" },
"seven_day_opus": { "utilization": 1 }
}
"""
let data = Data(json.utf8)
let parsed = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data)
#expect(parsed.sessionPercentUsed == 9)
#expect(parsed.weeklyPercentUsed == 4)
#expect(parsed.opusPercentUsed == 1)
#expect(parsed.sessionResetsAt != nil)
#expect(parsed.weeklyResetsAt != nil)
}
@Test
func `parses claude web API usage response when weekly missing`() throws {
let json = """
{
"five_hour": { "utilization": 9, "resets_at": "2025-12-23T16:00:00.000Z" }
}
"""
let data = Data(json.utf8)
let parsed = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data)
#expect(parsed.sessionPercentUsed == 9)
#expect(parsed.weeklyPercentUsed == nil)
}
@Test
func `parses claude web API overage spend limit`() {
let json = """
{
"monthly_credit_limit": 2000,
"currency": "EUR",
"used_credits": 0,
"is_enabled": true
}
"""
let data = Data(json.utf8)
let cost = ClaudeWebAPIFetcher._parseOverageSpendLimitForTesting(data)
#expect(cost != nil)
#expect(cost?.currencyCode == "EUR")
#expect(cost?.limit == 20)
#expect(cost?.used == 0)
#expect(cost?.period == "Monthly cap")
}
@Test
func `parses claude web API overage spend limit cents`() {
let json = """
{
"monthly_credit_limit": 12345,
"currency": "USD",
"used_credits": 6789,
"is_enabled": true
}
"""
let data = Data(json.utf8)
let cost = ClaudeWebAPIFetcher._parseOverageSpendLimitForTesting(data)
#expect(cost?.currencyCode == "USD")
#expect(cost?.limit == 123.45)
#expect(cost?.used == 67.89)
}
@Test
func `parses claude web API organizations response`() throws {
let json = """
[
{ "uuid": "org-123", "name": "Example Org", "capabilities": [] }
]
"""
let data = Data(json.utf8)
let org = try ClaudeWebAPIFetcher._parseOrganizationsResponseForTesting(data)
#expect(org.id == "org-123")
#expect(org.name == "Example Org")
}
@Test
func `parses claude web API organizations prefers chat capability over api only`() throws {
let json = """
[
{ "uuid": "org-api", "name": "API Org", "capabilities": ["api"] },
{ "uuid": "org-chat", "name": "Chat Org", "capabilities": ["chat"] }
]
"""
let data = Data(json.utf8)
let org = try ClaudeWebAPIFetcher._parseOrganizationsResponseForTesting(data)
#expect(org.id == "org-chat")
#expect(org.name == "Chat Org")
}
@Test
func `parses claude web API organizations prefers hybrid chat org`() throws {
let json = """
[
{ "uuid": "org-api", "name": "API Org", "capabilities": ["api"] },
{ "uuid": "org-hybrid", "name": "Hybrid Org", "capabilities": ["api", "chat"] }
]
"""
let data = Data(json.utf8)
let org = try ClaudeWebAPIFetcher._parseOrganizationsResponseForTesting(data)
#expect(org.id == "org-hybrid")
#expect(org.name == "Hybrid Org")
}
@Test
func `parses claude web API account info`() {
let json = """
{
"email_address": "steipete@gmail.com",
"memberships": [
{
"organization": {
"uuid": "org-123",
"name": "Example Org",
"rate_limit_tier": "default_claude_max_20x",
"billing_type": "stripe_subscription"
}
}
]
}
"""
let data = Data(json.utf8)
let info = ClaudeWebAPIFetcher._parseAccountInfoForTesting(data, orgId: "org-123")
#expect(info?.email == "steipete@gmail.com")
#expect(info?.loginMethod == "Claude Max 20x")
}
@Test
func `parses claude web API account info selects matching org`() {
let json = """
{
"email_address": "steipete@gmail.com",
"memberships": [
{
"organization": {
"uuid": "org-other",
"name": "Other Org",
"rate_limit_tier": "claude_pro",
"billing_type": "stripe_subscription"
}
},
{
"organization": {
"uuid": "org-123",
"name": "Example Org",
"rate_limit_tier": "claude_team",
"billing_type": "stripe_subscription"
}
}
]
}
"""
let data = Data(json.utf8)
let info = ClaudeWebAPIFetcher._parseAccountInfoForTesting(data, orgId: "org-123")
#expect(info?.loginMethod == "Claude Team")
}
@Test
func `parses claude web API account info falls back to first membership`() {
let json = """
{
"email_address": "steipete@gmail.com",
"memberships": [
{
"organization": {
"uuid": "org-first",
"name": "First Org",
"rate_limit_tier": "claude_enterprise",
"billing_type": "invoice"
}
},
{
"organization": {
"uuid": "org-second",
"name": "Second Org",
"rate_limit_tier": "claude_pro",
"billing_type": "stripe_subscription"
}
}
]
}
"""
let data = Data(json.utf8)
let info = ClaudeWebAPIFetcher._parseAccountInfoForTesting(data, orgId: nil)
#expect(info?.loginMethod == "Claude Enterprise")
}
@Test
func `claude usage fetcher init with data sources`() {
// Verify we can create fetchers with both configurations
let browserDetection = BrowserDetection(cacheTTL: 0)
let defaultFetcher = ClaudeUsageFetcher(browserDetection: browserDetection)
let webFetcher = ClaudeUsageFetcher(browserDetection: browserDetection, dataSource: .web)
let cliFetcher = ClaudeUsageFetcher(browserDetection: browserDetection, dataSource: .cli)
// Both should be valid instances (no crashes)
let defaultVersion = defaultFetcher.detectVersion()
let webVersion = webFetcher.detectVersion()
let cliVersion = cliFetcher.detectVersion()
#expect(defaultVersion?.isEmpty != true)
#expect(webVersion?.isEmpty != true)
#expect(cliVersion?.isEmpty != true)
}
}
extension ClaudeUsageTests {
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))
}
}
struct ClaudeOAuthUsageMappingTests {
@Test
func `oauth usage falls back to weekly window when five hour is absent`() throws {
let json = """
{
"seven_day": { "utilization": 42, "resets_at": "2025-12-29T23:00:00.000Z" },
"seven_day_sonnet": { "utilization": 17, "resets_at": "2025-12-29T23:00:00.000Z" }
}
"""
let snapshot = try ClaudeUsageFetcher._mapOAuthUsageForTesting(Data(json.utf8))
#expect(snapshot.primary.usedPercent == 42)
#expect(snapshot.primary.windowMinutes == 7 * 24 * 60)
#expect(snapshot.secondary?.usedPercent == 42)
#expect(snapshot.opus?.usedPercent == 17)
}
@Test
func `oauth usage falls back when five hour has no utilization`() throws {
let json = """
{
"five_hour": { "resets_at": "2025-12-23T16:00:00.000Z" },
"seven_day": { "utilization": 9, "resets_at": "2025-12-29T23:00:00.000Z" }
}
"""
let snapshot = try ClaudeUsageFetcher._mapOAuthUsageForTesting(Data(json.utf8))
#expect(snapshot.primary.usedPercent == 9)
#expect(snapshot.primary.windowMinutes == 7 * 24 * 60)
}
@Test
func `oauth usage throws when no usable windows are present`() {
let json = "{}"
#expect(throws: ClaudeUsageError.self) {
try ClaudeUsageFetcher._mapOAuthUsageForTesting(Data(json.utf8))
}
}
}
@Suite(.serialized)
struct ClaudeAutoFetcherCharacterizationTests {
private final class RequestLog: @unchecked Sendable {
private var paths: [String] = []
private let lock = NSLock()
func append(_ path: String) {
self.lock.lock()
defer { self.lock.unlock() }
self.paths.append(path)
}
func current() -> [String] {
self.lock.lock()
defer { self.lock.unlock() }
return self.paths
}
}
private final class InvocationLog: @unchecked Sendable {
let url: URL
init(url: URL) {
self.url = url
}
func contents() -> String {
(try? String(contentsOf: self.url, encoding: .utf8)) ?? ""
}
}
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))
}
private static func makeFakeClaudeCLI(logURL: URL) throws -> URL {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent("claude-auto-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let scriptURL = directory.appendingPathComponent("claude")
let script = """
#!/bin/sh
LOG_FILE='\(logURL.path)'
while IFS= read -r line; do
case "$line" in
*"/usage"*)
printf 'usage\\n' >> "$LOG_FILE"
cat <<'EOF'
Current session
93% left
Dec 23 at 4:00PM
Current week (all models)
79% left
Dec 29 at 11:00PM
EOF
;;
*"/status"*)
printf 'status\\n' >> "$LOG_FILE"
cat <<'EOF'
Account: cli@example.com
Org: CLI Org
EOF
;;
esac
done
"""
try script.write(to: scriptURL, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes(
[.posixPermissions: NSNumber(value: Int16(0o755))],
ofItemAtPath: scriptURL.path)
return scriptURL
}
private func withNoOAuthCredentials<T>(operation: () async throws -> T) async rethrows -> T {
let missingCredentialsURL = FileManager.default.temporaryDirectory
.appendingPathComponent("missing-claude-creds-\(UUID().uuidString).json")
return try await KeychainCacheStore.withServiceOverrideForTesting("rat-107-\(UUID().uuidString)") {
KeychainCacheStore.setTestStoreForTesting(true)
defer { KeychainCacheStore.setTestStoreForTesting(false) }
return try await ClaudeOAuthCredentialsStore.withIsolatedMemoryCacheForTesting {
try await ClaudeOAuthCredentialsStore.withIsolatedCredentialsFileTrackingForTesting {
try await ClaudeOAuthCredentialsStore.withCredentialsURLOverrideForTesting(missingCredentialsURL) {
try await ClaudeOAuthCredentialsStore.withKeychainAccessOverrideForTesting(true) {
try await ClaudeOAuthCredentialsStore.withClaudeKeychainOverridesForTesting(
data: nil,
fingerprint: nil)
{
try await operation()
}
}
}
}
}
}
}
private func withClaudeWebStub<T>(
handler: @escaping (URLRequest) throws -> (HTTPURLResponse, Data),
operation: () async throws -> T) async rethrows -> T
{
let registered = URLProtocol.registerClass(ClaudeAutoFetcherStubURLProtocol.self)
ClaudeAutoFetcherStubURLProtocol.handler = handler
defer {
if registered {
URLProtocol.unregisterClass(ClaudeAutoFetcherStubURLProtocol.self)
}
ClaudeAutoFetcherStubURLProtocol.handler = nil
}
return try await operation()
}
fileprivate static func makeJSONResponse(
url: URL,
body: String,
statusCode: Int = 200) -> (HTTPURLResponse, Data)
{
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": "application/json"])!
return (response, Data(body.utf8))
}
@Test
func `auto prefers OAuth even when web and CLI appear available`() async throws {
let usageResponse = try Self.makeOAuthUsageResponse()
let cliLogURL = FileManager.default.temporaryDirectory
.appendingPathComponent("claude-auto-cli-log-\(UUID().uuidString).txt")
let log = InvocationLog(url: cliLogURL)
let fakeCLI = try Self.makeFakeClaudeCLI(logURL: cliLogURL)
let webRequests = RequestLog()
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: [
ClaudeOAuthCredentialsStore.environmentTokenKey: "oauth-token",
ClaudeOAuthCredentialsStore.environmentScopesKey: "user:profile",
],
runtime: .app,
dataSource: .auto,
manualCookieHeader: "sessionKey=sk-ant-session-token")
try await ClaudeCLISession.withIsolatedSessionForTesting {
try await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting(fakeCLI.path) {
try await self.withClaudeWebStub(handler: { request in
webRequests.append(request.url?.path ?? "<missing>")
let url = try #require(request.url)
return Self.makeJSONResponse(url: url, body: "{}")
}, operation: {
let fetchOverride: @Sendable (String, Bool) async throws -> OAuthUsageResponse = { _, _ in
usageResponse
}
let snapshot = try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(
fetchOverride,
operation: {
try await fetcher.loadLatestUsage(model: "sonnet")
})
#expect(snapshot.primary.usedPercent == 7)
#expect(snapshot.secondary?.usedPercent == 21)
#expect(log.contents().isEmpty)
let requests = webRequests.current()
#expect(requests.isEmpty)
})
}
}
}
@Test
func `app runtime auto prefers CLI before web when OAuth unavailable`() async throws {
let cliLogURL = FileManager.default.temporaryDirectory
.appendingPathComponent("claude-auto-web-log-\(UUID().uuidString).txt")
let log = InvocationLog(url: cliLogURL)
let fakeCLI = try Self.makeFakeClaudeCLI(logURL: cliLogURL)
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: ["CLAUDE_CLI_PATH": fakeCLI.path],
runtime: .app,
dataSource: .auto,
manualCookieHeader: "sessionKey=sk-ant-session-token")
try await ClaudeCLISession.withIsolatedSessionForTesting {
try await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting(fakeCLI.path) {
try await self.withNoOAuthCredentials {
try await self.withClaudeWebStub(handler: { request in
let url = try #require(request.url)
switch url.path {
case "/api/organizations":
return Self.makeJSONResponse(
url: url,
body: #"[{"uuid":"org-123","name":"Test Org","capabilities":["chat"]}]"#)
case "/api/organizations/org-123/usage":
let body = """
{
"five_hour": { "utilization": 11, "resets_at": "2025-12-23T16:00:00.000Z" },
"seven_day": { "utilization": 22, "resets_at": "2025-12-29T23:00:00.000Z" },
"seven_day_opus": { "utilization": 33 }
}
"""
return Self.makeJSONResponse(
url: url,
body: body)
case "/api/account":
let body = """
{
"email_address": "web@example.com",
"memberships": [
{
"organization": {
"uuid": "org-123",
"name": "Test Org",
"rate_limit_tier": "claude_max",
"billing_type": "stripe"
}
}
]
}
"""
return Self.makeJSONResponse(
url: url,
body: body)
case "/api/organizations/org-123/overage_spend_limit":
let body = """
{"monthly_credit_limit":5000,"currency":"USD","used_credits":1200,"is_enabled":true}
"""
return Self.makeJSONResponse(
url: url,
body: body)
default:
return Self.makeJSONResponse(url: url, body: "{}", statusCode: 404)
}
}, operation: {
let snapshot = try await fetcher.loadLatestUsage(model: "sonnet")
#expect(snapshot.rawText != nil)
#expect(log.contents().contains("usage"))
})
}
}
}
}
@Test
func `CLI runtime auto prefers web before CLI when OAuth unavailable`() async throws {
let cliLogURL = FileManager.default.temporaryDirectory
.appendingPathComponent("claude-auto-cli-runtime-web-log-\(UUID().uuidString).txt")
let log = InvocationLog(url: cliLogURL)
let fakeCLI = try Self.makeFakeClaudeCLI(logURL: cliLogURL)
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: ["CLAUDE_CLI_PATH": fakeCLI.path],
runtime: .cli,
dataSource: .auto,
manualCookieHeader: "sessionKey=sk-ant-session-token")
try await ClaudeCLISession.withIsolatedSessionForTesting {
try await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting(fakeCLI.path) {
try await self.withNoOAuthCredentials {
try await self.withClaudeWebStub(handler: { request in
let url = try #require(request.url)
switch url.path {
case "/api/organizations":
return Self.makeJSONResponse(
url: url,
body: #"[{"uuid":"org-123","name":"Test Org","capabilities":["chat"]}]"#)
case "/api/organizations/org-123/usage":
let body = """
{
"five_hour": { "utilization": 11, "resets_at": "2025-12-23T16:00:00.000Z" },
"seven_day": { "utilization": 22, "resets_at": "2025-12-29T23:00:00.000Z" },
"seven_day_opus": { "utilization": 33 }
}
"""
return Self.makeJSONResponse(url: url, body: body)
case "/api/account":
let body = """
{
"email_address": "web@example.com",
"memberships": [
{
"organization": {
"uuid": "org-123",
"name": "Test Org",
"rate_limit_tier": "claude_max",
"billing_type": "stripe"
}
}
]
}
"""
return Self.makeJSONResponse(url: url, body: body)
case "/api/organizations/org-123/overage_spend_limit":
let body = """
{"monthly_credit_limit":5000,"currency":"USD","used_credits":1200,"is_enabled":true}
"""
return Self.makeJSONResponse(url: url, body: body)
default:
return Self.makeJSONResponse(url: url, body: "{}", statusCode: 404)
}
}, operation: {
let snapshot = try await fetcher.loadLatestUsage(model: "sonnet")
#expect(snapshot.primary.usedPercent == 11)
#expect(snapshot.secondary?.usedPercent == 22)
#expect(snapshot.opus?.usedPercent == 33)
#expect(snapshot.accountEmail == "web@example.com")
#expect(snapshot.loginMethod == "Claude Max")
#expect(log.contents().isEmpty)
})
}
}
}
}
@Test
func `app runtime auto fails deterministically when planner has no executable steps`() async {
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: ["CLAUDE_CLI_PATH": "/definitely/missing/claude"],
runtime: .app,
dataSource: .auto,
manualCookieHeader: "foo=bar")
await self.withNoOAuthCredentials {
await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {
do {
_ = try await fetcher.loadLatestUsage(model: "sonnet")
Issue.record("Expected app auto no-source fetch to fail.")
} catch let error as ClaudeUsageError {
#expect(error.localizedDescription.contains("Claude planner produced no executable steps."))
} catch {
Issue.record("Unexpected error: \(error)")
}
}
}
}
@Test
func `CLI runtime auto fails deterministically when planner has no executable steps`() async {
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: ["CLAUDE_CLI_PATH": "/definitely/missing/claude"],
runtime: .cli,
dataSource: .auto,
manualCookieHeader: "foo=bar")
await self.withNoOAuthCredentials {
await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting("/definitely/missing/claude") {
do {
_ = try await fetcher.loadLatestUsage(model: "sonnet")
Issue.record("Expected CLI auto no-source fetch to fail.")
} catch let error as ClaudeUsageError {
#expect(error.localizedDescription.contains("Claude planner produced no executable steps."))
} catch {
Issue.record("Unexpected error: \(error)")
}
}
}
}
}
final class ClaudeAutoFetcherStubURLProtocol: URLProtocol {
nonisolated(unsafe) static var handler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
override static func canInit(with request: URLRequest) -> Bool {
request.url?.host == "claude.ai"
}
override static func canonicalRequest(for request: URLRequest) -> URLRequest {
request
}
override func startLoading() {
guard let handler = Self.handler else {
self.client?.urlProtocol(self, didFailWithError: URLError(.badServerResponse))
return
}
do {
let (response, data) = try handler(self.request)
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.client?.urlProtocol(self, didLoad: data)
self.client?.urlProtocolDidFinishLoading(self)
} catch {
self.client?.urlProtocol(self, didFailWithError: error)
}
}
override func stopLoading() {}
}
extension ClaudeAutoFetcherCharacterizationTests {
@Test
func `web fetcher uses configured target organization`() async throws {
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
dataSource: .web,
manualCookieHeader: "sessionKey=sk-ant-session-token",
webOrganizationID: "org-team")
try await self.withClaudeWebStub(handler: { request in
let url = try #require(request.url)
switch url.path {
case "/api/organizations":
let body = """
[
{ "uuid": "org-personal", "name": "Personal", "capabilities": ["chat"] },
{ "uuid": "org-team", "name": "Team Org", "capabilities": ["chat"] }
]
"""
return Self.makeJSONResponse(url: url, body: body)
case "/api/organizations/org-team/usage":
let body = """
{
"five_hour": { "utilization": 14, "resets_at": "2025-12-23T16:00:00.000Z" },
"seven_day": { "utilization": 28, "resets_at": "2025-12-29T23:00:00.000Z" }
}
"""
return Self.makeJSONResponse(url: url, body: body)
case "/api/account":
let body = """
{
"email_address": "linked@example.com",
"memberships": [
{
"organization": {
"uuid": "org-personal",
"name": "Personal",
"rate_limit_tier": "claude_max",
"billing_type": "stripe"
}
},
{
"organization": {
"uuid": "org-team",
"name": "Team Org",
"rate_limit_tier": "enterprise",
"billing_type": "invoice"
}
}
]
}
"""
return Self.makeJSONResponse(url: url, body: body)
case "/api/organizations/org-team/overage_spend_limit":
return Self.makeJSONResponse(url: url, body: "{}", statusCode: 404)
default:
return Self.makeJSONResponse(url: url, body: "{}", statusCode: 404)
}
}, operation: {
let snapshot = try await fetcher.loadLatestUsage(model: "sonnet")
#expect(snapshot.primary.usedPercent == 14)
#expect(snapshot.secondary?.usedPercent == 28)
#expect(snapshot.accountOrganization == "Team Org")
#expect(snapshot.accountEmail == "linked@example.com")
#expect(snapshot.loginMethod == "Claude Enterprise")
})
}
}
extension ClaudeUsageTests {
@Test
func `parses claude web API organizations honors target organization`() throws {
let json = """
[
{ "uuid": "org-personal", "name": "Personal", "capabilities": ["chat"] },
{ "uuid": "org-team", "name": "Team", "capabilities": ["chat"] }
]
"""
let data = Data(json.utf8)
let org = try ClaudeWebAPIFetcher._parseOrganizationsResponseForTesting(
data,
targetOrganizationID: "org-team")
#expect(org.id == "org-team")
#expect(org.name == "Team")
}
@Test
func `oauth delegated retry experimental background respects only on user action suppression`() 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,
allowBackgroundDelegatedRefresh: false)
let fetchOverride: (@Sendable (String, Bool) 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
_ = await loadCounter.increment()
throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI
}
await #expect(throws: ClaudeUsageError.self) {
try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try await ProviderInteractionContext.$current.withValue(.background) {
try await ClaudeUsageFetcher.$hasCachedCredentialsOverride.withValue(true) {
try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) {
try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride.withValue(
delegatedOverride)
{
try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(
loadCredsOverride)
{
try await fetcher.loadLatestUsage(model: "sonnet")
}
}
}
}
}
}
})
}
#expect(await loadCounter.current() == 1)
#expect(await delegatedCounter.current() == 0)
}
@Test
func `oauth load experimental background fallback blocked propagates O auth failure`() async throws {
final class FlagBox: @unchecked Sendable {
var respectPromptCooldownFlags: [Bool] = []
}
let flags = FlagBox()
let fetcher = ClaudeUsageFetcher(
browserDetection: BrowserDetection(cacheTTL: 0),
environment: [:],
dataSource: .oauth,
oauthKeychainPromptCooldownEnabled: true,
allowBackgroundDelegatedRefresh: false)
let loadCredsOverride: (@Sendable (
[String: String],
Bool,
Bool) async throws -> ClaudeOAuthCredentials)? = { _, _, respectKeychainPromptCooldown in
flags.respectPromptCooldownFlags.append(respectKeychainPromptCooldown)
throw ClaudeOAuthCredentialsError.notFound
}
await #expect(throws: ClaudeUsageError.self) {
try await ClaudeOAuthKeychainReadStrategyPreference.withTaskOverrideForTesting(
.securityCLIExperimental,
operation: {
try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(.onlyOnUserAction) {
try await ProviderInteractionContext.$current.withValue(.background) {
try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(loadCredsOverride) {
try await fetcher.loadLatestUsage(model: "sonnet")
}
}
}
})
}
#expect(flags.respectPromptCooldownFlags == [true])
}
}