项目文件夹

文件
2026-07-13 12:22:33 +08:00

295 行
14 KiB
Swift

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())
}
}