import CodexBarCore import Foundation import Testing @testable import CodexBar @MainActor struct CodexUserFacingErrorTests { @Test func `missing codex CLI guidance is not collapsed to not running`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-missing-cli") store.errors[.codex] = "Codex not running. Try running a Codex command first. " + "(Codex CLI not found. Install with `npm i -g @openai/codex`.)" #expect(store.userFacingError(for: .codex) == CodexStatusProbeError.codexNotInstalled.localizedDescription) } @Test func `logged out codex CLI guidance is not collapsed to temporary outage`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-cli-login-required") store.errors[.codex] = "Codex connection failed: codex account authentication required to read rate limits" #expect( store.userFacingError(for: .codex) == "Codex CLI is not signed in. Run `codex login --device-auth`, then refresh.") } @Test func `cached logged out codex CLI failure preserves cached suffix`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-cached-cli-login-required") store.lastCreditsError = "Last Codex credits refresh failed: Codex connection failed: " + "codex account authentication required to read rate limits. Cached values from 2m ago." #expect( store.userFacingLastCreditsError == "Codex CLI is not signed in. Run `codex login --device-auth`, then refresh. " + "Cached values from 2m ago.") } @Test func `expired codex auth is sanitized`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-expired-auth") store.errors[.codex] = """ Codex connection failed: failed to fetch codex rate limits: GET https://chatgpt.com/backend-api/wham/usage \ failed: 401 Unauthorized; content-type=text/plain; body={\"error\":{\"message\":\"Provided authentication \ token is expired. Please try signing in again.\",\"code\":\"token_expired\"}} """ #expect(store.userFacingError(for: .codex) == "Codex session expired. Sign in again.") } @Test func `transport codex error is sanitized`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-transport") store.errors[.codex] = "Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500" #expect(store.userFacingError(for: .codex) == "Codex usage is temporarily unavailable. Try refreshing.") } @Test func `decode mismatch codex error is sanitized`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-decode-mismatch") store.errors[.codex] = "Codex connection failed: failed to fetch codex rate limits: " + "Decode error for https://chatgpt.com/backend-api/wham/usage: " + "unknown variant `prolite`, expected one of `guest`, `free`, `go`, `plus`, `pro`" #expect(store.userFacingError(for: .codex) == "Codex usage is temporarily unavailable. Try refreshing.") } @Test func `cached credits failure preserves cached suffix while sanitizing body`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-cached-credits") store.lastCreditsError = "Last Codex credits refresh failed: Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500; body={\"error\":{}} " + "Cached values from 2m ago." #expect( store.userFacingLastCreditsError == "Codex usage is temporarily unavailable. Try refreshing. Cached values from 2m ago.") } @Test func `localized cached credits failure preserves cached suffix while sanitizing body`() { let result = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hant") { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-localized-cached-credits") store.lastCreditsError = "Last Codex credits refresh failed: Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500 Cached values from 2m ago." return store.userFacingLastCreditsError } #expect(result == "Codex 使用量暫時無法取得。請嘗試重新整理。 使用 2m ago 的快取值。") } @Test func `cached missing codex CLI failure preserves cached suffix`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-cached-missing-cli") store.lastCreditsError = "Last Codex credits refresh failed: Codex CLI not found. " + "Install with `npm i -g @openai/codex`. Cached values from 2m ago." #expect( store.userFacingLastCreditsError == CodexStatusProbeError.codexNotInstalled.localizedDescription + " Cached values from 2m ago.") } @Test func `browser mismatch remains unchanged`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-browser-mismatch") store.lastOpenAIDashboardError = "OpenAI cookies are for ratulsarna@gmail.com, not rdsarna@gmail.com. " + "Switch chatgpt.com account, then refresh OpenAI cookies." #expect( store.userFacingLastOpenAIDashboardError == "OpenAI cookies are for ratulsarna@gmail.com, not rdsarna@gmail.com. " + "Switch chatgpt.com account, then refresh OpenAI cookies.") } @Test func `frame load interrupted becomes retry guidance`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-frame-load") store.lastOpenAIDashboardError = "Frame load interrupted" #expect( store.userFacingLastOpenAIDashboardError == "OpenAI web refresh was interrupted. Refresh OpenAI cookies and try again.") } @Test func `open A I web timeout becomes retry guidance`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-openai-web-timeout") store.lastOpenAIDashboardError = "The operation couldn’t be completed. (NSURLErrorDomain error -1001.)" #expect( store.userFacingLastOpenAIDashboardError == "OpenAI web refresh timed out. Refresh OpenAI cookies and try again.") } @Test func `localized cached open A I web timeout preserves cached suffix`() { let result = CodexBarLocalizationOverride.$appLanguage.withValue("zh-Hant") { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-localized-openai-web-timeout") store.lastOpenAIDashboardError = "Last OpenAI dashboard refresh failed: " + "The operation couldn’t be completed. (NSURLErrorDomain error -1001.). " + "Cached values from 2m ago." return store.userFacingLastOpenAIDashboardError } #expect( result == "OpenAI Web 重新整理逾時。請重新整理 OpenAI Cookie 後再試一次。 使用 2m ago 的快取值。") } @Test func `open A I web network error becomes connection guidance`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-openai-web-network") store.lastOpenAIDashboardError = "The operation couldn’t be completed. (NSURLErrorDomain error -1004.)" let expected = [ "OpenAI web refresh hit a network error.", "Check your connection, then refresh OpenAI cookies and try again.", ].joined(separator: " ") #expect(store.userFacingLastOpenAIDashboardError == expected) } @Test func `non codex providers keep raw errors`() { let store = self.makeUsageStore(suite: "CodexUserFacingErrorTests-non-codex") store.errors[.claude] = "Claude probe failed with debug detail" #expect(store.userFacingError(for: .claude) == "Claude probe failed with debug detail") } @Test func `providers pane codex model uses sanitized values`() { let settings = self.makeSettingsStore(suite: "CodexUserFacingErrorTests-pane-model") let store = self.makeUsageStore(settings: settings) store.errors[.codex] = "Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500" store.lastCreditsError = "Last Codex credits refresh failed: Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500 " + "Cached values from 1m ago." store.lastOpenAIDashboardError = "Frame load interrupted" let pane = ProvidersPane(settings: settings, store: store) let model = pane._test_menuCardModel(for: .codex) #expect(model.subtitleText == "Codex usage is temporarily unavailable. Try refreshing.") #expect( model.creditsHintText == "OpenAI web refresh was interrupted. Refresh OpenAI cookies and try again.") #expect( model.creditsHintCopyText == "OpenAI web refresh was interrupted. Refresh OpenAI cookies and try again.") #expect( model.creditsText == "Codex usage is temporarily unavailable. Try refreshing. Cached values from 1m ago.") } @Test func `menu card hides optional codex setup diagnostics kept by providers pane`() throws { let settings = self.makeSettingsStore(suite: "CodexUserFacingErrorTests-menu-diagnostics") let store = self.makeUsageStore(settings: settings) store.lastCreditsError = UsageError.noRateLimitsFound.errorDescription store.lastOpenAIDashboardError = "No matching OpenAI web session found. Sign in to chatgpt.com, then refresh OpenAI cookies." let fetcher = UsageFetcher(environment: [:]) let menuModel = try withStatusItemControllerForTesting( store: store, settings: settings, fetcher: fetcher) { controller in try #require(controller.menuCardModel(for: .codex)) } let pane = ProvidersPane(settings: settings, store: store) let settingsModel = pane._test_menuCardModel(for: .codex) let settingsDiagnostic = pane._test_openAIWebDiagnostic(for: .codex) let settingsInfoRows = ProviderMetricsInlineView.infoRows( for: settingsModel, openAIWebDiagnostic: settingsDiagnostic) #expect(menuModel.creditsText == nil) #expect(menuModel.creditsHintText == nil) #expect(settingsModel.creditsText == UsageError.noRateLimitsFound.errorDescription) #expect(settingsModel.creditsHintText?.contains("No matching OpenAI web session found") == true) #expect(settingsInfoRows.contains { row in row.id == .openAIWeb && row.value.contains("No matching OpenAI web session found") }) } @Test func `providers pane codex error display keeps raw full text for copy`() { let settings = self.makeSettingsStore(suite: "CodexUserFacingErrorTests-pane-error-display") let store = self.makeUsageStore(settings: settings) let raw = "Codex connection failed: failed to fetch codex rate limits: " + "GET https://chatgpt.com/backend-api/wham/usage failed: 500; body={\"error\":{}}" store.errors[.codex] = raw let pane = ProvidersPane(settings: settings, store: store) let display = pane._test_providerErrorDisplay(for: .codex) #expect(display?.preview == "Codex usage is temporarily unavailable. Try refreshing.") #expect(display?.full == raw) } private func makeUsageStore(suite: String) -> UsageStore { let settings = self.makeSettingsStore(suite: suite) return self.makeUsageStore(settings: settings) } private func makeUsageStore(settings: SettingsStore) -> UsageStore { UsageStore( fetcher: UsageFetcher(environment: [:]), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings, startupBehavior: .testing) } private func makeSettingsStore(suite: String) -> SettingsStore { let defaults = UserDefaults(suiteName: suite)! defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) return SettingsStore( userDefaults: defaults, configStore: configStore, zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore(), codexCookieStore: InMemoryCookieHeaderStore(), claudeCookieStore: InMemoryCookieHeaderStore(), cursorCookieStore: InMemoryCookieHeaderStore(), opencodeCookieStore: InMemoryCookieHeaderStore(), factoryCookieStore: InMemoryCookieHeaderStore(), minimaxCookieStore: InMemoryMiniMaxCookieStore(), minimaxAPITokenStore: InMemoryMiniMaxAPITokenStore(), kimiTokenStore: InMemoryKimiTokenStore(), kimiK2TokenStore: InMemoryKimiK2TokenStore(), augmentCookieStore: InMemoryCookieHeaderStore(), ampCookieStore: InMemoryCookieHeaderStore(), copilotTokenStore: InMemoryCopilotTokenStore(), tokenAccountStore: InMemoryTokenAccountStore()) } }