Files
2026-07-13 12:22:33 +08:00

232 lines
9.9 KiB
Swift

import Testing
@testable import CodexBarCLI
@testable import CodexBarCore
struct CLIWebFallbackTests {
private func makeContext(
runtime: ProviderRuntime = .cli,
sourceMode: ProviderSourceMode = .auto,
settings: ProviderSettingsSnapshot? = nil) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
return ProviderFetchContext(
runtime: runtime,
sourceMode: sourceMode,
includeCredits: true,
webTimeout: 60,
webDebugDumpHTML: false,
verbose: false,
env: [:],
settings: settings,
fetcher: UsageFetcher(),
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
browserDetection: browserDetection)
}
private func makeClaudeSettingsSnapshot(cookieHeader: String?) -> ProviderSettingsSnapshot {
ProviderSettingsSnapshot.make(
claude: .init(
usageDataSource: .auto,
webExtrasEnabled: false,
cookieSource: .manual,
manualCookieHeader: cookieHeader))
}
@Test
func `codex falls back when cookies missing`() {
let context = self.makeContext()
let strategy = CodexWebDashboardStrategy()
#expect(strategy.shouldFallback(
on: OpenAIDashboardBrowserCookieImporter.ImportError.noCookiesFound,
context: context))
#expect(strategy.shouldFallback(
on: OpenAIDashboardBrowserCookieImporter.ImportError.noMatchingAccount(found: []),
context: context))
#expect(strategy.shouldFallback(
on: OpenAIDashboardBrowserCookieImporter.ImportError.browserAccessDenied(details: "no access"),
context: context))
#expect(strategy.shouldFallback(
on: OpenAIDashboardBrowserCookieImporter.ImportError.dashboardStillRequiresLogin,
context: context))
#expect(strategy.shouldFallback(
on: OpenAIDashboardFetcher.FetchError.loginRequired,
context: context))
}
@Test
func `codex falls back for dashboard data errors in auto`() {
let context = self.makeContext()
let strategy = CodexWebDashboardStrategy()
#expect(strategy.shouldFallback(
on: OpenAIDashboardFetcher.FetchError.noDashboardData(body: "missing"),
context: context))
}
@Test
func `codex retries fresh browser import for missing usage and no data`() {
#expect(CodexWebDashboardStrategy.shouldRetryWithFreshBrowserImport(
after: OpenAIWebCodexError.missingUsage))
#expect(CodexWebDashboardStrategy.shouldRetryWithFreshBrowserImport(
after: OpenAIDashboardFetcher.FetchError.noDashboardData(body: "missing")))
#expect(!CodexWebDashboardStrategy.shouldRetryWithFreshBrowserImport(
after: OpenAIDashboardFetcher.FetchError.loginRequired))
#expect(!CodexWebDashboardStrategy.shouldRetryWithFreshBrowserImport(
after: OpenAIWebCodexError.timedOut(seconds: 30)))
}
@Test
func `codex shared deadline timeout has useful error`() {
let error = OpenAIWebCodexError.timedOut(seconds: 30)
#expect(error.localizedDescription == "OpenAI web dashboard fetch timed out after 30 seconds.")
}
@Test
func `codex display only falls back in auto`() {
let strategy = CodexWebDashboardStrategy()
let decision = self.makeCodexDisplayOnlyDecision()
#expect(strategy.shouldFallback(
on: CodexDashboardPolicyError.displayOnly(decision),
context: self.makeContext(sourceMode: .auto)))
}
@Test
func `codex display only does not fall back in explicit web`() {
let strategy = CodexWebDashboardStrategy()
let decision = self.makeCodexDisplayOnlyDecision()
#expect(!strategy.shouldFallback(
on: CodexDashboardPolicyError.displayOnly(decision),
context: self.makeContext(sourceMode: .web)))
}
@Test
func `codex web strategy is unavailable when managed account store is unreadable`() async {
let context = self.makeContext(settings: ProviderSettingsSnapshot.make(
codex: .init(
usageDataSource: .auto,
cookieSource: .auto,
manualCookieHeader: nil,
managedAccountStoreUnreadable: true)))
let strategy = CodexWebDashboardStrategy()
let available = await strategy.isAvailable(context)
#expect(!available)
}
@Test
func `codex web strategy is unavailable when selected managed target is unavailable`() async {
let context = self.makeContext(settings: ProviderSettingsSnapshot.make(
codex: .init(
usageDataSource: .auto,
cookieSource: .auto,
manualCookieHeader: nil,
managedAccountTargetUnavailable: true)))
let strategy = CodexWebDashboardStrategy()
let available = await strategy.isAvailable(context)
#expect(!available)
}
@Test
func `codex web strategy fails closed when profile target is unavailable`() async {
let settings = ProviderSettingsSnapshot.make(
codex: .init(
usageDataSource: .auto,
cookieSource: .auto,
manualCookieHeader: nil,
profileAccountTargetUnavailable: true))
let strategy = CodexWebDashboardStrategy()
let autoContext = self.makeContext(sourceMode: .auto, settings: settings)
let autoAvailable = await strategy.isAvailable(autoContext)
#expect(!autoAvailable)
let explicitWebContext = self.makeContext(sourceMode: .web, settings: settings)
let explicitWebAvailable = await strategy.isAvailable(explicitWebContext)
#expect(explicitWebAvailable)
do {
_ = try await strategy.fetch(explicitWebContext)
Issue.record("Expected unavailable profile target to require login")
} catch OpenAIDashboardFetcher.FetchError.loginRequired {
// Expected before browser import can accept an arbitrary account.
} catch {
Issue.record("Unexpected error: \(error)")
}
}
@Test
func `claude falls back when no session key`() {
let context = self.makeContext()
let strategy = ClaudeWebFetchStrategy(browserDetection: BrowserDetection(cacheTTL: 0))
#expect(strategy.shouldFallback(on: ClaudeWebAPIFetcher.FetchError.noSessionKeyFound, context: context))
#expect(strategy.shouldFallback(on: ClaudeWebAPIFetcher.FetchError.unauthorized, context: context))
}
@Test
func `claude CLI fallback is enabled only for app auto`() {
let webAvailableStrategy = ClaudeCLIFetchStrategy(
useWebExtras: false,
manualCookieHeader: nil,
browserDetection: BrowserDetection(cacheTTL: 0),
hasWebFallback: true)
let webUnavailableStrategy = ClaudeCLIFetchStrategy(
useWebExtras: false,
manualCookieHeader: nil,
browserDetection: BrowserDetection(cacheTTL: 0),
hasWebFallback: false)
let error = ClaudeUsageError.parseFailed("cli failed")
let webAvailableSettings = self.makeClaudeSettingsSnapshot(cookieHeader: "sessionKey=sk-ant-test")
let webUnavailableSettings = self.makeClaudeSettingsSnapshot(cookieHeader: "foo=bar")
#expect(webAvailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .app, sourceMode: .auto, settings: webAvailableSettings)))
#expect(!webUnavailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .app, sourceMode: .auto, settings: webUnavailableSettings)))
#expect(!webAvailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .app, sourceMode: .cli)))
#expect(!webAvailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .app, sourceMode: .web)))
#expect(!webAvailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .app, sourceMode: .oauth)))
#expect(!webAvailableStrategy.shouldFallback(
on: error,
context: self.makeContext(runtime: .cli, sourceMode: .auto)))
}
@Test
func `claude web fallback is disabled for app auto`() {
let strategy = ClaudeWebFetchStrategy(browserDetection: BrowserDetection(cacheTTL: 0))
let error = ClaudeWebAPIFetcher.FetchError.unauthorized
#expect(strategy.shouldFallback(on: error, context: self.makeContext(runtime: .cli, sourceMode: .auto)))
#expect(!strategy.shouldFallback(on: error, context: self.makeContext(runtime: .app, sourceMode: .auto)))
}
private func makeCodexDisplayOnlyDecision() -> CodexDashboardAuthorityDecision {
CodexDashboardAuthority.evaluate(
CodexDashboardAuthorityInput(
sourceKind: .liveWeb,
proof: CodexDashboardOwnershipProofContext(
currentIdentity: .emailOnly(normalizedEmail: "shared@example.com"),
expectedScopedEmail: nil,
trustedCurrentUsageEmail: nil,
dashboardSignedInEmail: "shared@example.com",
knownOwners: [
CodexDashboardKnownOwnerCandidate(
identity: .providerAccount(id: "acct-alpha"),
normalizedEmail: "shared@example.com"),
CodexDashboardKnownOwnerCandidate(
identity: .providerAccount(id: "acct-beta"),
normalizedEmail: "shared@example.com"),
]),
routing: CodexDashboardRoutingHints(
targetEmail: "shared@example.com",
lastKnownDashboardRoutingEmail: nil)))
}
}