From 08ba0ad64a05b166a74f62e565feea01eb4b15ca Mon Sep 17 00:00:00 2001 From: mkkim417 Date: Fri, 20 Feb 2026 11:12:44 +0900 Subject: [PATCH 1/3] Fix Claude Enterprise web usage and member overage handling --- .../Claude/ClaudeProviderDescriptor.swift | 35 ++++++- .../ClaudeWeb/ClaudeWebAPIFetcher.swift | 63 +++++++++--- Tests/CodexBarTests/ClaudeUsageTests.swift | 98 +++++++++++++++++++ 3 files changed, 177 insertions(+), 19 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index 002a600ae..e4a75a2a8 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -128,6 +128,14 @@ public struct ClaudeUsageStrategy: Equatable, Sendable { public let useWebExtras: Bool } +#if DEBUG +extension ClaudeProviderDescriptor { + public static func _snapshotFromClaudeUsageForTesting(_ usage: ClaudeUsageSnapshot) -> UsageSnapshot { + ClaudeOAuthFetchStrategy._snapshotForTesting(from: usage) + } +} +#endif + struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { let id: String = "claude.oauth" let kind: ProviderFetchKind = .oauth @@ -248,19 +256,40 @@ struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { } fileprivate static func snapshot(from usage: ClaudeUsageSnapshot) -> UsageSnapshot { + let hideUsageMetrics = Self.shouldHideUsageMetrics(for: usage) + let primary: RateWindow? = hideUsageMetrics ? nil : usage.primary + let secondary: RateWindow? = hideUsageMetrics ? nil : usage.secondary + let tertiary: RateWindow? = hideUsageMetrics ? nil : usage.opus let identity = ProviderIdentitySnapshot( providerID: .claude, accountEmail: usage.accountEmail, accountOrganization: usage.accountOrganization, loginMethod: usage.loginMethod) return UsageSnapshot( - primary: usage.primary, - secondary: usage.secondary, - tertiary: usage.opus, + primary: primary, + secondary: secondary, + tertiary: tertiary, providerCost: usage.providerCost, updatedAt: usage.updatedAt, identity: identity) } + + private static func shouldHideUsageMetrics(for usage: ClaudeUsageSnapshot) -> Bool { + // Claude Enterprise web accounts can return HTTP 200 with all usage windows set to null. + // In this case, showing "Session 100% left" is misleading; keep only account/cost info. + guard usage.secondary == nil, usage.opus == nil else { return false } + guard usage.primary.usedPercent == 0 else { return false } + guard usage.primary.resetsAt == nil, usage.primary.resetDescription == nil else { return false } + // Web path provides account identity; OAuth generally does not. + guard usage.accountEmail != nil else { return false } + return true + } + + #if DEBUG + static func _snapshotForTesting(from usage: ClaudeUsageSnapshot) -> UsageSnapshot { + self.snapshot(from: usage) + } + #endif } struct ClaudeWebFetchStrategy: ProviderFetchStrategy { diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift index e004b47c1..8030dce87 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift @@ -185,9 +185,14 @@ public enum ClaudeWebAPIFetcher { let organization = try await fetchOrganizationInfo(sessionKey: sessionKey, logger: log) log("Organization resolved") + let account = await fetchAccountInfo(sessionKey: sessionKey, orgId: organization.id, logger: log) var usage = try await fetchUsageData(orgId: organization.id, sessionKey: sessionKey, logger: log) if usage.extraUsageCost == nil, - let extra = await fetchExtraUsageCost(orgId: organization.id, sessionKey: sessionKey, logger: log) + let extra = await fetchExtraUsageCost( + orgId: organization.id, + accountUUID: account?.accountUUID, + sessionKey: sessionKey, + logger: log) { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, @@ -200,7 +205,7 @@ public enum ClaudeWebAPIFetcher { accountEmail: usage.accountEmail, loginMethod: usage.loginMethod) } - if let account = await fetchAccountInfo(sessionKey: sessionKey, orgId: organization.id, logger: log) { + if let account { usage = WebUsageData( sessionPercentUsed: usage.sessionPercentUsed, sessionResetsAt: usage.sessionResetsAt, @@ -453,27 +458,23 @@ public enum ClaudeWebAPIFetcher { } // Parse five_hour (session) usage - var sessionPercent: Double? + var sessionPercent: Double = 0 var sessionResets: Date? if let fiveHour = json["five_hour"] as? [String: Any] { - if let utilization = fiveHour["utilization"] as? Int { - sessionPercent = Double(utilization) + if let utilization = fiveHour["utilization"] as? NSNumber { + sessionPercent = utilization.doubleValue } if let resetsAt = fiveHour["resets_at"] as? String { sessionResets = self.parseISO8601Date(resetsAt) } } - guard let sessionPercent else { - // If we can't parse session utilization, treat this as a failure so callers can fall back to the CLI. - throw FetchError.invalidResponse - } // Parse seven_day (weekly) usage var weeklyPercent: Double? var weeklyResets: Date? if let sevenDay = json["seven_day"] as? [String: Any] { - if let utilization = sevenDay["utilization"] as? Int { - weeklyPercent = Double(utilization) + if let utilization = sevenDay["utilization"] as? NSNumber { + weeklyPercent = utilization.doubleValue } if let resetsAt = sevenDay["resets_at"] as? String { weeklyResets = self.parseISO8601Date(resetsAt) @@ -483,8 +484,8 @@ public enum ClaudeWebAPIFetcher { // Parse seven_day_opus (Opus-specific weekly) usage var opusPercent: Double? if let sevenDayOpus = json["seven_day_opus"] as? [String: Any] { - if let utilization = sevenDayOpus["utilization"] as? Int { - opusPercent = Double(utilization) + if let utilization = sevenDayOpus["utilization"] as? NSNumber { + opusPercent = utilization.doubleValue } } @@ -519,10 +520,24 @@ public enum ClaudeWebAPIFetcher { /// Best-effort fetch of Claude Extra spend/limit (does not fail the main usage fetch). private static func fetchExtraUsageCost( orgId: String, + accountUUID: String?, + sessionKey: String, + logger: ((String) -> Void)? = nil) async -> ProviderCostSnapshot? + { + let preferredURL = self.overageSpendLimitURL(orgId: orgId, accountUUID: accountUUID) + if let snapshot = await self.fetchExtraUsageCost(url: preferredURL, sessionKey: sessionKey, logger: logger) { + return snapshot + } + guard accountUUID != nil else { return nil } + let fallbackURL = self.overageSpendLimitURL(orgId: orgId, accountUUID: nil) + return await self.fetchExtraUsageCost(url: fallbackURL, sessionKey: sessionKey, logger: logger) + } + + private static func fetchExtraUsageCost( + url: URL, sessionKey: String, logger: ((String) -> Void)? = nil) async -> ProviderCostSnapshot? { - let url = URL(string: "\(baseURL)/organizations/\(orgId)/overage_spend_limit")! var request = URLRequest(url: url) request.setValue("sessionKey=\(sessionKey)", forHTTPHeaderField: "Cookie") request.setValue("application/json", forHTTPHeaderField: "Accept") @@ -540,6 +555,14 @@ public enum ClaudeWebAPIFetcher { } } + private static func overageSpendLimitURL(orgId: String, accountUUID: String?) -> URL { + var components = URLComponents(string: "\(baseURL)/organizations/\(orgId)/overage_spend_limit")! + if let accountUUID { + components.queryItems = [URLQueryItem(name: "account_uuid", value: accountUUID)] + } + return components.url! + } + private static func parseOverageSpendLimit(_ data: Data) -> ProviderCostSnapshot? { guard let decoded = try? JSONDecoder().decode(OverageSpendLimitResponse.self, from: data) else { return nil } guard decoded.isEnabled == true else { return nil } @@ -628,19 +651,25 @@ public enum ClaudeWebAPIFetcher { public struct WebAccountInfo: Sendable { public let email: String? + public let accountUUID: String? public let loginMethod: String? - public init(email: String?, loginMethod: String?) { + public init(email: String?, accountUUID: String?, loginMethod: String?) { self.email = email + self.accountUUID = accountUUID self.loginMethod = loginMethod } } private struct AccountResponse: Decodable { + let uuid: String? + let accountUUID: String? let emailAddress: String? let memberships: [Membership]? enum CodingKeys: String, CodingKey { + case uuid + case accountUUID = "account_uuid" case emailAddress = "email_address" case memberships } @@ -690,11 +719,13 @@ public enum ClaudeWebAPIFetcher { private static func parseAccountInfo(_ data: Data, orgId: String?) -> WebAccountInfo? { guard let response = try? JSONDecoder().decode(AccountResponse.self, from: data) else { return nil } let email = response.emailAddress?.trimmingCharacters(in: .whitespacesAndNewlines) + let accountUUID = (response.accountUUID ?? response.uuid)? + .trimmingCharacters(in: .whitespacesAndNewlines) let membership = Self.selectMembership(response.memberships, orgId: orgId) let plan = Self.inferPlan( rateLimitTier: membership?.organization.rateLimitTier, billingType: membership?.organization.billingType) - return WebAccountInfo(email: email, loginMethod: plan) + return WebAccountInfo(email: email, accountUUID: accountUUID, loginMethod: plan) } private static func selectMembership( diff --git a/Tests/CodexBarTests/ClaudeUsageTests.swift b/Tests/CodexBarTests/ClaudeUsageTests.swift index 7f007b811..1d803ca9b 100644 --- a/Tests/CodexBarTests/ClaudeUsageTests.swift +++ b/Tests/CodexBarTests/ClaudeUsageTests.swift @@ -680,6 +680,21 @@ struct ClaudeUsageTests { } @Test + func parsesClaudeWebAPIUsageResponseWhenAllUsageWindowsAreNull() throws { + let json = """ + { + "five_hour": null, + "seven_day": null, + "seven_day_opus": null + } + """ + let data = Data(json.utf8) + let parsed = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data) + #expect(parsed.sessionPercentUsed == 0) + #expect(parsed.weeklyPercentUsed == nil) + #expect(parsed.opusPercentUsed == nil) + } + func parsesClaudeWebAPIOverageSpendLimit() { let json = """ { @@ -715,6 +730,27 @@ struct ClaudeUsageTests { #expect(cost?.used == 67.89) } + @Test + func parsesClaudeWebAPIOverageSpendLimitEnterpriseMemberPayload() { + let json = """ + { + "organization_uuid": "00000000-0000-0000-0000-000000000001", + "limit_type": "member", + "seat_tier": null, + "account_uuid": "00000000-0000-0000-0000-000000000002", + "monthly_credit_limit": 30000, + "currency": "USD", + "used_credits": 14025, + "is_enabled": true + } + """ + let data = Data(json.utf8) + let cost = ClaudeWebAPIFetcher._parseOverageSpendLimitForTesting(data) + #expect(cost?.currencyCode == "USD") + #expect(cost?.limit == 300) + #expect(cost?.used == 140.25) + } + @Test func parsesClaudeWebAPIOrganizationsResponse() throws { let json = """ @@ -776,6 +812,7 @@ struct ClaudeUsageTests { let data = Data(json.utf8) let info = ClaudeWebAPIFetcher._parseAccountInfoForTesting(data, orgId: "org-123") #expect(info?.email == "steipete@gmail.com") + #expect(info?.accountUUID == nil) #expect(info?.loginMethod == "Claude Max") } @@ -839,6 +876,20 @@ struct ClaudeUsageTests { #expect(info?.loginMethod == "Claude Enterprise") } + @Test + func parsesClaudeWebAPIAccountInfoWithAccountUUID() { + let json = """ + { + "uuid": "00000000-0000-0000-0000-000000000003", + "email_address": "account@example.com", + "memberships": [] + } + """ + let data = Data(json.utf8) + let info = ClaudeWebAPIFetcher._parseAccountInfoForTesting(data, orgId: nil) + #expect(info?.accountUUID == "00000000-0000-0000-0000-000000000003") + } + @Test func claudeUsageFetcherInitWithDataSources() { // Verify we can create fetchers with both configurations @@ -854,6 +905,53 @@ struct ClaudeUsageTests { #expect(webVersion?.isEmpty != true) #expect(cliVersion?.isEmpty != true) } + + @Test + func hidesClaudeUsageMetricsWhenEnterpriseWebUsageWindowsAreAllNull() { + let usage = ClaudeUsageSnapshot( + primary: RateWindow(usedPercent: 0, windowMinutes: 5 * 60, resetsAt: nil, resetDescription: nil), + secondary: nil, + opus: nil, + providerCost: ProviderCostSnapshot( + used: 140.25, + limit: 300, + currencyCode: "USD", + period: "Monthly", + resetsAt: nil, + updatedAt: Date()), + updatedAt: Date(), + accountEmail: "member@example.com", + accountOrganization: "Example Org", + loginMethod: "Claude Enterprise", + rawText: nil) + + let snapshot = ClaudeProviderDescriptor._snapshotFromClaudeUsageForTesting(usage) + #expect(snapshot.primary == nil) + #expect(snapshot.secondary == nil) + #expect(snapshot.tertiary == nil) + #expect(snapshot.providerCost != nil) + } + + @Test + func keepsClaudeUsageMetricsWhenSessionResetExists() { + let usage = ClaudeUsageSnapshot( + primary: RateWindow( + usedPercent: 0, + windowMinutes: 5 * 60, + resetsAt: Date(timeIntervalSince1970: 1_700_000_000), + resetDescription: "Dec 1 at 1:00PM"), + secondary: nil, + opus: nil, + providerCost: nil, + updatedAt: Date(), + accountEmail: "member@example.com", + accountOrganization: "Example Org", + loginMethod: "Claude Enterprise", + rawText: nil) + + let snapshot = ClaudeProviderDescriptor._snapshotFromClaudeUsageForTesting(usage) + #expect(snapshot.primary != nil) + } } extension ClaudeUsageTests { From e9932f67a5763c243213a20b1444dc6761a70bcd Mon Sep 17 00:00:00 2001 From: mkkim417 Date: Fri, 20 Feb 2026 15:51:51 +0900 Subject: [PATCH 2/3] Fix Claude Enterprise web usage and member overage handling --- .../Claude/ClaudeProviderDescriptor.swift | 10 +---- .../Providers/Claude/ClaudeUsageFetcher.swift | 4 ++ .../ClaudeWeb/ClaudeWebAPIFetcher.swift | 31 +++++++++++++++- Tests/CodexBarTests/ClaudeUsageTests.swift | 37 ++++++++++++++++++- 4 files changed, 71 insertions(+), 11 deletions(-) diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift index e4a75a2a8..8466f6e61 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeProviderDescriptor.swift @@ -275,14 +275,8 @@ struct ClaudeOAuthFetchStrategy: ProviderFetchStrategy { } private static func shouldHideUsageMetrics(for usage: ClaudeUsageSnapshot) -> Bool { - // Claude Enterprise web accounts can return HTTP 200 with all usage windows set to null. - // In this case, showing "Session 100% left" is misleading; keep only account/cost info. - guard usage.secondary == nil, usage.opus == nil else { return false } - guard usage.primary.usedPercent == 0 else { return false } - guard usage.primary.resetsAt == nil, usage.primary.resetDescription == nil else { return false } - // Web path provides account identity; OAuth generally does not. - guard usage.accountEmail != nil else { return false } - return true + // Derived directly from the web usage payload shape to avoid depending on best-effort account fetches. + usage.usageMetricsUnavailable } #if DEBUG diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift index ed57ac71b..41423c768 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeUsageFetcher.swift @@ -10,6 +10,7 @@ public struct ClaudeUsageSnapshot: Sendable { public let primary: RateWindow public let secondary: RateWindow? public let opus: RateWindow? + public let usageMetricsUnavailable: Bool public let providerCost: ProviderCostSnapshot? public let updatedAt: Date public let accountEmail: String? @@ -21,6 +22,7 @@ public struct ClaudeUsageSnapshot: Sendable { primary: RateWindow, secondary: RateWindow?, opus: RateWindow?, + usageMetricsUnavailable: Bool = false, providerCost: ProviderCostSnapshot? = nil, updatedAt: Date, accountEmail: String?, @@ -31,6 +33,7 @@ public struct ClaudeUsageSnapshot: Sendable { self.primary = primary self.secondary = secondary self.opus = opus + self.usageMetricsUnavailable = usageMetricsUnavailable self.providerCost = providerCost self.updatedAt = updatedAt self.accountEmail = accountEmail @@ -801,6 +804,7 @@ public struct ClaudeUsageFetcher: ClaudeUsageFetching, Sendable { primary: primary, secondary: secondary, opus: opus, + usageMetricsUnavailable: webData.usageMetricsUnavailable, providerCost: providerCost, updatedAt: Date(), accountEmail: webData.accountEmail, diff --git a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift index 8030dce87..e0fb8234e 100644 --- a/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift +++ b/Sources/CodexBarCore/Providers/Claude/ClaudeWeb/ClaudeWebAPIFetcher.swift @@ -84,6 +84,7 @@ public enum ClaudeWebAPIFetcher { public let weeklyPercentUsed: Double? public let weeklyResetsAt: Date? public let opusPercentUsed: Double? + public let usageMetricsUnavailable: Bool public let extraUsageCost: ProviderCostSnapshot? public let accountOrganization: String? public let accountEmail: String? @@ -95,6 +96,7 @@ public enum ClaudeWebAPIFetcher { weeklyPercentUsed: Double?, weeklyResetsAt: Date?, opusPercentUsed: Double?, + usageMetricsUnavailable: Bool, extraUsageCost: ProviderCostSnapshot?, accountOrganization: String?, accountEmail: String?, @@ -105,6 +107,7 @@ public enum ClaudeWebAPIFetcher { self.weeklyPercentUsed = weeklyPercentUsed self.weeklyResetsAt = weeklyResetsAt self.opusPercentUsed = opusPercentUsed + self.usageMetricsUnavailable = usageMetricsUnavailable self.extraUsageCost = extraUsageCost self.accountOrganization = accountOrganization self.accountEmail = accountEmail @@ -200,6 +203,7 @@ public enum ClaudeWebAPIFetcher { weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, + usageMetricsUnavailable: usage.usageMetricsUnavailable, extraUsageCost: extra, accountOrganization: usage.accountOrganization, accountEmail: usage.accountEmail, @@ -212,6 +216,7 @@ public enum ClaudeWebAPIFetcher { weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, + usageMetricsUnavailable: usage.usageMetricsUnavailable, extraUsageCost: usage.extraUsageCost, accountOrganization: usage.accountOrganization, accountEmail: account.email, @@ -224,6 +229,7 @@ public enum ClaudeWebAPIFetcher { weeklyPercentUsed: usage.weeklyPercentUsed, weeklyResetsAt: usage.weeklyResetsAt, opusPercentUsed: usage.opusPercentUsed, + usageMetricsUnavailable: usage.usageMetricsUnavailable, extraUsageCost: usage.extraUsageCost, accountOrganization: name, accountEmail: usage.accountEmail, @@ -458,7 +464,8 @@ public enum ClaudeWebAPIFetcher { } // Parse five_hour (session) usage - var sessionPercent: Double = 0 + let usageMetricsUnavailable = self.usageWindowsUnavailable(in: json) + var sessionPercent: Double? var sessionResets: Date? if let fiveHour = json["five_hour"] as? [String: Any] { if let utilization = fiveHour["utilization"] as? NSNumber { @@ -468,6 +475,10 @@ public enum ClaudeWebAPIFetcher { sessionResets = self.parseISO8601Date(resetsAt) } } + guard sessionPercent != nil || usageMetricsUnavailable else { + // Keep malformed payloads on the fallback path. Only all-null enterprise payloads are accepted. + throw FetchError.invalidResponse + } // Parse seven_day (weekly) usage var weeklyPercent: Double? @@ -490,17 +501,33 @@ public enum ClaudeWebAPIFetcher { } return WebUsageData( - sessionPercentUsed: sessionPercent, + sessionPercentUsed: sessionPercent ?? 0, sessionResetsAt: sessionResets, weeklyPercentUsed: weeklyPercent, weeklyResetsAt: weeklyResets, opusPercentUsed: opusPercent, + usageMetricsUnavailable: usageMetricsUnavailable, extraUsageCost: nil, accountOrganization: nil, accountEmail: nil, loginMethod: nil) } + private static func usageWindowsUnavailable(in json: [String: Any]) -> Bool { + let keys = [ + "five_hour", + "seven_day", + "seven_day_oauth_apps", + "seven_day_opus", + "seven_day_sonnet", + "seven_day_cowork", + "iguana_necktie", + ] + let values = keys.compactMap { json[$0] } + guard !values.isEmpty else { return false } + return values.allSatisfy { $0 is NSNull } + } + // MARK: - Extra usage cost (Claude "Extra") private struct OverageSpendLimitResponse: Decodable { diff --git a/Tests/CodexBarTests/ClaudeUsageTests.swift b/Tests/CodexBarTests/ClaudeUsageTests.swift index 1d803ca9b..a94578e14 100644 --- a/Tests/CodexBarTests/ClaudeUsageTests.swift +++ b/Tests/CodexBarTests/ClaudeUsageTests.swift @@ -684,8 +684,12 @@ struct ClaudeUsageTests { let json = """ { "five_hour": null, + "seven_day_oauth_apps": null, "seven_day": null, - "seven_day_opus": null + "seven_day_opus": null, + "seven_day_sonnet": null, + "seven_day_cowork": null, + "iguana_necktie": null } """ let data = Data(json.utf8) @@ -693,6 +697,35 @@ struct ClaudeUsageTests { #expect(parsed.sessionPercentUsed == 0) #expect(parsed.weeklyPercentUsed == nil) #expect(parsed.opusPercentUsed == nil) + #expect(parsed.usageMetricsUnavailable == true) + } + + @Test + func parsesClaudeWebAPIUsageResponseWhenPresentUsageWindowsAreAllNull() throws { + let json = """ + { + "five_hour": null, + "seven_day": null + } + """ + let data = Data(json.utf8) + let parsed = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data) + #expect(parsed.sessionPercentUsed == 0) + #expect(parsed.usageMetricsUnavailable == true) + } + + @Test + func rejectsClaudeWebAPIUsageResponseWhenSessionWindowMissingAndPayloadNotAllNull() { + let json = """ + { + "five_hour": null, + "seven_day": { "utilization": 12, "resets_at": "2025-12-29T23:00:00.000Z" } + } + """ + let data = Data(json.utf8) + #expect(throws: ClaudeWebAPIFetcher.FetchError.self) { + _ = try ClaudeWebAPIFetcher._parseUsageResponseForTesting(data) + } } func parsesClaudeWebAPIOverageSpendLimit() { @@ -912,6 +945,7 @@ struct ClaudeUsageTests { primary: RateWindow(usedPercent: 0, windowMinutes: 5 * 60, resetsAt: nil, resetDescription: nil), secondary: nil, opus: nil, + usageMetricsUnavailable: true, providerCost: ProviderCostSnapshot( used: 140.25, limit: 300, @@ -942,6 +976,7 @@ struct ClaudeUsageTests { resetDescription: "Dec 1 at 1:00PM"), secondary: nil, opus: nil, + usageMetricsUnavailable: false, providerCost: nil, updatedAt: Date(), accountEmail: "member@example.com", From 999d977d76fffb656551f614a522fbef51bc6394 Mon Sep 17 00:00:00 2001 From: mkkim417 Date: Fri, 20 Feb 2026 16:14:18 +0900 Subject: [PATCH 3/3] Restore missing @Test annotation for Claude overage parser test --- Tests/CodexBarTests/ClaudeUsageTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CodexBarTests/ClaudeUsageTests.swift b/Tests/CodexBarTests/ClaudeUsageTests.swift index a94578e14..486af21ee 100644 --- a/Tests/CodexBarTests/ClaudeUsageTests.swift +++ b/Tests/CodexBarTests/ClaudeUsageTests.swift @@ -728,6 +728,7 @@ struct ClaudeUsageTests { } } + @Test func parsesClaudeWebAPIOverageSpendLimit() { let json = """ {