168 lines
6.0 KiB
Swift
168 lines
6.0 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
@Suite(.serialized)
|
|
struct ClaudeOAuthDelegatedRefreshLinuxTests {
|
|
private actor Counter {
|
|
private var value = 0
|
|
|
|
func increment() {
|
|
self.value += 1
|
|
}
|
|
|
|
func current() -> Int {
|
|
self.value
|
|
}
|
|
}
|
|
|
|
private actor VersionDetectionCapture {
|
|
private var value: Bool?
|
|
|
|
func record(_ value: Bool) {
|
|
self.value = value
|
|
}
|
|
|
|
func current() -> Bool? {
|
|
self.value
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func cliOAuthSkipsVersionDetectionWhileAppPreservesIt() async throws {
|
|
#expect(try await self.detectsClaudeVersion(runtime: .cli) == false)
|
|
#expect(try await self.detectsClaudeVersion(runtime: .app) == true)
|
|
}
|
|
|
|
@Test
|
|
func cliOAuthDoesNotDelegateRefreshEvenForUserAction() async {
|
|
let result = await self.runDelegatedRefresh(
|
|
runtime: .cli,
|
|
interaction: .userInitiated,
|
|
promptMode: .always)
|
|
|
|
#expect(result.attempts == 0)
|
|
#expect(result.message.contains("CodexBar CLI does not launch Claude"))
|
|
}
|
|
|
|
@Test
|
|
func appOAuthPreservesUserInitiatedDelegatedRefresh() async {
|
|
let result = await self.runDelegatedRefresh(
|
|
runtime: .app,
|
|
interaction: .userInitiated,
|
|
promptMode: .onlyOnUserAction)
|
|
|
|
#expect(result.attempts == 1)
|
|
#expect(result.message.contains("still unavailable after delegated Claude CLI refresh"))
|
|
}
|
|
|
|
@Test
|
|
func appOAuthBackgroundRespectsPlatformKeychainPromptPolicy() async {
|
|
let result = await self.runDelegatedRefresh(
|
|
runtime: .app,
|
|
interaction: .background,
|
|
promptMode: .onlyOnUserAction)
|
|
|
|
#expect(result.attempts == 0)
|
|
#expect(result.message.contains("background repair is suppressed"))
|
|
}
|
|
|
|
private func runDelegatedRefresh(
|
|
runtime: ProviderRuntime,
|
|
interaction: ProviderInteraction,
|
|
promptMode: ClaudeOAuthKeychainPromptMode) async -> (attempts: Int, message: String)
|
|
{
|
|
let counter = Counter()
|
|
let fetcher = ClaudeUsageFetcher(
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
environment: [:],
|
|
runtime: runtime,
|
|
dataSource: .oauth)
|
|
let credentialsOverride: @Sendable (
|
|
[String: String],
|
|
Bool,
|
|
Bool) async throws -> ClaudeOAuthCredentials = { _, _, _ in
|
|
throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI
|
|
}
|
|
let delegatedOverride: @Sendable (
|
|
Date,
|
|
TimeInterval,
|
|
[String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome = { _, _, _ in
|
|
await counter.increment()
|
|
return .attemptedSucceeded
|
|
}
|
|
|
|
do {
|
|
_ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(promptMode) {
|
|
try await ProviderInteractionContext.$current.withValue(interaction) {
|
|
try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride
|
|
.withValue(credentialsOverride) {
|
|
try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride
|
|
.withValue(delegatedOverride) {
|
|
try await fetcher.loadLatestUsage(model: "sonnet")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Issue.record("Expected delegated-refresh path to fail with mocked stale credentials")
|
|
return (await counter.current(), "")
|
|
} catch let error as ClaudeUsageError {
|
|
guard case let .oauthFailed(message) = error else {
|
|
Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)")
|
|
return (await counter.current(), "")
|
|
}
|
|
return (await counter.current(), message)
|
|
} catch {
|
|
Issue.record("Expected ClaudeUsageError, got \(error)")
|
|
return (await counter.current(), "")
|
|
}
|
|
}
|
|
|
|
private func detectsClaudeVersion(runtime: ProviderRuntime) async throws -> Bool {
|
|
let capture = VersionDetectionCapture()
|
|
let fetcher = ClaudeUsageFetcher(
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
environment: [:],
|
|
runtime: runtime,
|
|
dataSource: .oauth)
|
|
let credentialsOverride: @Sendable (
|
|
[String: String],
|
|
Bool,
|
|
Bool) async throws -> ClaudeOAuthCredentials = { _, _, _ in
|
|
ClaudeOAuthCredentials(
|
|
accessToken: "access-token",
|
|
refreshToken: nil,
|
|
expiresAt: Date(timeIntervalSinceNow: 3600),
|
|
scopes: ["user:profile"],
|
|
rateLimitTier: nil)
|
|
}
|
|
let fetchOverride: @Sendable (String, Bool) async throws -> OAuthUsageResponse = {
|
|
_, detectClaudeVersion in
|
|
await capture.record(detectClaudeVersion)
|
|
return try Self.makeOAuthUsageResponse()
|
|
}
|
|
|
|
_ = try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(credentialsOverride) {
|
|
try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) {
|
|
try await fetcher.loadLatestUsage(model: "sonnet")
|
|
}
|
|
}
|
|
|
|
guard let value = await capture.current() else {
|
|
Issue.record("Expected OAuth fetch to report its version-detection policy")
|
|
return false
|
|
}
|
|
return value
|
|
}
|
|
|
|
private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse {
|
|
let json = """
|
|
{
|
|
"five_hour": { "utilization": 7, "resets_at": "2025-12-23T16:00:00.000Z" },
|
|
"seven_day": { "utilization": 21, "resets_at": "2025-12-29T23:00:00.000Z" }
|
|
}
|
|
"""
|
|
return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
|
|
}
|
|
}
|