295 lines
14 KiB
Swift
295 lines
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())
|
||
}
|
||
}
|