232 lines
9.9 KiB
Swift
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)))
|
|
}
|
|
}
|