From b1e02d7ab2ef6861e074eb80e5454301e97cb5c6 Mon Sep 17 00:00:00 2001 From: Chris Busillo Date: Tue, 2 Jun 2026 14:06:26 -0400 Subject: [PATCH] Fix account-scoped provider report matching --- .../AccountConfigurationStore.swift | 32 ++++ .../ContextPanelPreviewApp.swift | 19 +-- .../AccountConfigurationStoreTests.swift | 145 ++++++++++++++++++ 3 files changed, 179 insertions(+), 17 deletions(-) diff --git a/Sources/ContextPanelCore/AccountConfigurationStore.swift b/Sources/ContextPanelCore/AccountConfigurationStore.swift index 730c35a..2adb9d4 100644 --- a/Sources/ContextPanelCore/AccountConfigurationStore.swift +++ b/Sources/ContextPanelCore/AccountConfigurationStore.swift @@ -63,6 +63,38 @@ public struct LocalProviderAccountConfiguration: Codable, Equatable, Identifiabl } } +public extension LocalProviderAccountConfiguration { + var providerReportAccountIDs: [String] { + switch connectorKind { + case .codexRateLimits, .geminiCodeAssist: + guard let authPath else { return [] } + return Self.localAccountIDs(provider: provider, path: authPath) + case .claudeLocalStatus: + guard let authPath = effectiveAuthPath else { return [] } + return Self.localAccountIDs(provider: provider, path: authPath) + case .claudeOAuthUsage: + return [ConnectorRedactor.localAccountID(provider: provider, stableID: id)] + } + } + + func matchesProviderReport(_ report: StoredProviderReport) -> Bool { + guard report.provider == provider else { return false } + if let configuredAccountID = report.configuredAccountID { + return configuredAccountID == id + } + return report.accountID == id || providerReportAccountIDs.contains(report.accountID) + } + + private static func localAccountIDs(provider: Provider, path: String) -> [String] { + var ids = [ConnectorRedactor.localAccountID(provider: provider, path: path)] + let expandedPath = NSString(string: path).expandingTildeInPath + if expandedPath != path { + ids.append(ConnectorRedactor.localAccountID(provider: provider, path: expandedPath)) + } + return ids + } +} + public struct AccountConfigurationDocument: Codable, Equatable, Sendable { public let schemaVersion: Int public var updatedAt: Date diff --git a/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift b/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift index 96b7948..b20b6e6 100644 --- a/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift +++ b/Sources/ContextPanelPreview/ContextPanelPreviewApp.swift @@ -663,16 +663,7 @@ final class SettingsPaneModel: ObservableObject { guard let storedSnapshot else { return SettingsAccountRefreshSummary(text: "No refresh yet", status: .unknown) } - let reports = storedSnapshot.reports.filter { report in - switch account.connectorKind { - case .codexRateLimits: - report.provider == .openAI - case .geminiCodeAssist: - report.provider == .google - case .claudeLocalStatus, .claudeOAuthUsage: - report.provider == .anthropic - } - } + let reports = storedSnapshot.reports.filter { account.matchesProviderReport($0) } guard !reports.isEmpty else { return SettingsAccountRefreshSummary(text: "No refresh report yet", status: .unknown) } @@ -2328,13 +2319,7 @@ final class ContextPanelAppModel: ObservableObject { } func reportNeedsAttention(_ account: LocalProviderAccountConfiguration) -> Bool { - providerReportsNeedingAttention.contains { report in - guard report.provider == account.provider else { return false } - if let configuredAccountID = report.configuredAccountID { - return configuredAccountID == account.id - } - return true - } + providerReportsNeedingAttention.contains { account.matchesProviderReport($0) } } var lastSuccessfulProviderRefreshText: String? { diff --git a/Tests/ContextPanelCoreTests/AccountConfigurationStoreTests.swift b/Tests/ContextPanelCoreTests/AccountConfigurationStoreTests.swift index 2dfd8a8..58d1871 100644 --- a/Tests/ContextPanelCoreTests/AccountConfigurationStoreTests.swift +++ b/Tests/ContextPanelCoreTests/AccountConfigurationStoreTests.swift @@ -16,6 +16,151 @@ import Testing #expect(result.document.accounts.contains { $0.connectorKind == .claudeOAuthUsage && $0.effectiveAuthPath == nil }) } +@Test func localProviderAccountConfigurationMatchesReportsByConfiguredAccountID() throws { + let account = LocalProviderAccountConfiguration( + id: "openai-code-default", + provider: .openAI, + connectorKind: .codexRateLimits, + displayName: "Every Code", + authPath: "/tmp/code-auth.json" + ) + let report = StoredProviderReport( + provider: .openAI, + accountID: ConnectorRedactor.localAccountID(provider: .openAI, stableID: "chatgpt:user-a"), + configuredAccountID: "openai-code-default", + accountName: "Every Code", + generatedAt: Date(timeIntervalSince1970: 0), + status: .failure, + errorMessage: nil + ) + + #expect(account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationMatchesLocalConnectorReportsByResolvedPath() throws { + let authPath = "/tmp/gemini-oauth.json" + let account = LocalProviderAccountConfiguration( + id: "gemini-default", + provider: .google, + connectorKind: .geminiCodeAssist, + displayName: "Gemini", + authPath: authPath + ) + let report = StoredProviderReport( + provider: .google, + accountID: ConnectorRedactor.localAccountID(provider: .google, path: authPath), + accountName: "Gemini", + generatedAt: Date(timeIntervalSince1970: 0), + status: .stale, + errorMessage: nil + ) + + #expect(account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationMatchesExpandedLocalConnectorPaths() throws { + let expandedPath = "\(ContextPanelLocations.realUserHomeDirectory().path)/.gemini/oauth_creds.json" + let account = LocalProviderAccountConfiguration( + id: "gemini-default", + provider: .google, + connectorKind: .geminiCodeAssist, + displayName: "Gemini", + authPath: "~/.gemini/oauth_creds.json" + ) + let report = StoredProviderReport( + provider: .google, + accountID: ConnectorRedactor.localAccountID(provider: .google, path: expandedPath), + accountName: "Gemini", + generatedAt: Date(timeIntervalSince1970: 0), + status: .failure, + errorMessage: nil + ) + + #expect(account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationDoesNotMatchProviderWideReportFromAnotherAccount() throws { + let account = LocalProviderAccountConfiguration( + id: "gemini-default", + provider: .google, + connectorKind: .geminiCodeAssist, + displayName: "Gemini", + authPath: "/tmp/gemini-a.json" + ) + let report = StoredProviderReport( + provider: .google, + accountID: ConnectorRedactor.localAccountID(provider: .google, path: "/tmp/gemini-b.json"), + accountName: "Other Gemini", + generatedAt: Date(timeIntervalSince1970: 0), + status: .failure, + errorMessage: nil + ) + + #expect(!account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationUsesConfiguredAccountIDAsAuthoritative() throws { + let account = LocalProviderAccountConfiguration( + id: "openai-code-default", + provider: .openAI, + connectorKind: .codexRateLimits, + displayName: "Every Code", + authPath: "/tmp/code-auth.json" + ) + let report = StoredProviderReport( + provider: .openAI, + accountID: "openai-code-default", + configuredAccountID: "openai-codex-default", + accountName: "Codex", + generatedAt: Date(timeIntervalSince1970: 0), + status: .failure, + errorMessage: nil + ) + + #expect(!account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationMatchesClaudeLocalStatusReportsByEffectivePath() throws { + let account = LocalProviderAccountConfiguration( + id: "claude-local-default", + provider: .anthropic, + connectorKind: .claudeLocalStatus, + displayName: "Claude" + ) + let report = StoredProviderReport( + provider: .anthropic, + accountID: ConnectorRedactor.localAccountID( + provider: .anthropic, + path: ContextPanelLocations.claudeStatuslineCacheURL().path + ), + accountName: "Claude", + generatedAt: Date(timeIntervalSince1970: 0), + status: .stale, + errorMessage: nil + ) + + #expect(account.matchesProviderReport(report)) +} + +@Test func localProviderAccountConfigurationMatchesClaudeOAuthReportsByRedactedStableID() throws { + let account = LocalProviderAccountConfiguration( + id: "claude-oauth-default", + provider: .anthropic, + connectorKind: .claudeOAuthUsage, + displayName: "Claude" + ) + let report = StoredProviderReport( + provider: .anthropic, + accountID: ConnectorRedactor.localAccountID(provider: .anthropic, stableID: "claude-oauth-default"), + accountName: "Claude", + generatedAt: Date(timeIntervalSince1970: 0), + status: .failure, + errorMessage: nil + ) + + #expect(account.matchesProviderReport(report)) +} + @Test func accountConfigurationStorePreservesCustomAccountsWithoutAddingDefaults() throws { let url = try temporaryDirectory().appending(path: "accounts.json") let store = AccountConfigurationStore(configurationURL: url)