Files
steipete--codexbar/Tests/CodexBarTests/RequiredRefreshCoalescingTests.swift
2026-07-13 12:22:33 +08:00

556 lines
23 KiB
Swift

import Foundation
import Testing
@testable import CodexBar
@testable import CodexBarCore
extension CodexBackgroundRefreshCoalescingTests {
@Test
func `required refresh requests during a pass share one follow-up`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-required-refresh-follow-up")
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
settings.openAIWebAccessEnabled = false
let store = self.makeStore(settings: settings)
let providerGate = BlockingRequiredProviderRefresh()
store._test_providerRefreshOverride = { _ in
await providerGate.run(interaction: ProviderInteractionContext.current)
}
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
}
let firstRefresh = Task { @MainActor in
await store.refreshForSettingsChange()
}
let didStartFirstPass = await providerGate.waitUntilStarted(count: 1)
#expect(didStartFirstPass)
guard didStartFirstPass else {
firstRefresh.cancel()
store.cancelRequiredRefresh()
await providerGate.cancelAll()
await firstRefresh.value
return
}
var laterRefreshes: [Task<Void, Never>] = []
for expectedGeneration in 2...4 {
let task = Task { @MainActor in
await store.refreshForSettingsChange()
}
laterRefreshes.append(task)
for _ in 0..<100 where store.requiredRefreshRequestGeneration < expectedGeneration {
await Task.yield()
}
#expect(store.requiredRefreshRequestGeneration == expectedGeneration)
}
#expect(await providerGate.startedCount() == 1)
await providerGate.resumeNext()
let didStartFollowUp = await providerGate.waitUntilStarted(count: 2)
#expect(didStartFollowUp)
guard didStartFollowUp else {
firstRefresh.cancel()
laterRefreshes.forEach { $0.cancel() }
store.cancelRequiredRefresh()
await providerGate.cancelAll()
await firstRefresh.value
for task in laterRefreshes {
await task.value
}
return
}
#expect(store.requiredRefreshCompletedGeneration == 1)
await providerGate.resumeNext()
await firstRefresh.value
for task in laterRefreshes {
await task.value
}
try await Task.sleep(for: .milliseconds(50))
#expect(await providerGate.startedCount() == 2)
#expect(await providerGate.recordedInteractions() == [.background, .background])
#expect(store.requiredRefreshCompletedGeneration == 4)
#expect(store.requiredRefreshTask == nil)
#expect(store.pendingRequiredRefreshRequest == nil)
}
@Test
func `forced dashboard refresh stops a queued stale scheduler before it starts`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-forced-dashboard-prestart-cancellation")
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
defer { try? FileManager.default.removeItem(atPath: managedAccount.managedHomePath) }
let store = self.makeStore(settings: settings)
var allowNavigationTimeoutRetries: [Bool] = []
store._test_openAIDashboardCookieImportOverride = { targetEmail, _, _, _, _ in
OpenAIDashboardBrowserCookieImporter.ImportResult(
sourceLabel: "Fixture",
cookieCount: 2,
signedInEmail: targetEmail,
matchesCodexEmail: true)
}
store._test_openAIDashboardLoaderOverride = { _, _, allowNavigationTimeoutRetry, _ in
allowNavigationTimeoutRetries.append(allowNavigationTimeoutRetry)
return OpenAIDashboardSnapshot(
signedInEmail: managedAccount.email,
codeReviewRemainingPercent: 95,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
creditsRemaining: 25,
accountPlan: "Pro",
updatedAt: Date())
}
defer {
store._test_openAIDashboardCookieImportOverride = nil
store._test_openAIDashboardLoaderOverride = nil
}
let currentGuard = store.freshCodexOpenAIWebRefreshGuard()
let backgroundGuard = CodexAccountScopedRefreshGuard(
source: currentGuard.source,
identity: currentGuard.identity,
accountKey: currentGuard.accountKey,
authFingerprint: "background-token-material")
let forcedGuard = CodexAccountScopedRefreshGuard(
source: currentGuard.source,
identity: currentGuard.identity,
accountKey: currentGuard.accountKey,
authFingerprint: "forced-token-material")
store.openAIWebAccountDidChange = true
// Keep both calls on this MainActor turn so the forced request cancels the scheduler
// before its task body starts.
store.scheduleOpenAIDashboardRefreshIfNeeded(expectedGuard: backgroundGuard)
let backgroundTask = try #require(store.openAIDashboardBackgroundRefreshTask)
await store.refreshOpenAIDashboardIfNeeded(
force: true,
expectedGuard: forcedGuard,
bypassCoalescing: true,
allowCodexUsageBackfill: false)
await backgroundTask.value
#expect(backgroundTask.isCancelled)
#expect(allowNavigationTimeoutRetries == [true])
#expect(store.openAIDashboardBackgroundRefreshTask == nil)
#expect(store.openAIDashboardRefreshTask == nil)
}
@Test
func `forced dashboard enrichment supersedes weaker background request`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-forced-dashboard-supersedes-background")
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
defer { try? FileManager.default.removeItem(atPath: managedAccount.managedHomePath) }
let store = self.makeStore(settings: settings)
let dashboardLoader = BlockingManagedOpenAIDashboardLoader()
var allowNavigationTimeoutRetries: [Bool] = []
var dashboardInteractions: [ProviderInteraction] = []
store._test_providerRefreshOverride = { _ in }
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
store._test_openAIDashboardLoaderOverride = { _, _, allowNavigationTimeoutRetry, _ in
allowNavigationTimeoutRetries.append(allowNavigationTimeoutRetry)
dashboardInteractions.append(ProviderInteractionContext.current)
return try await dashboardLoader.awaitResult()
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_openAIDashboardLoaderOverride = nil
}
store.scheduleOpenAIDashboardRefreshIfNeeded(
expectedGuard: store.freshCodexOpenAIWebRefreshGuard())
let didStartBackground = await dashboardLoader.waitUntilStartedWithin(count: 1)
#expect(didStartBackground)
guard didStartBackground else {
store.invalidateOpenAIDashboardRefreshTask()
await dashboardLoader.cancelAll()
return
}
let staleDashboardTask = store.openAIDashboardRefreshTask
let backgroundTask = store.openAIDashboardBackgroundRefreshTask
let forcedRefresh = Task { @MainActor in
await BrowserCookieAccessGate.withExplicitRetry {
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await store.refresh(enrichmentMode: .forcedBackground)
await store.awaitForcedRefreshEnrichment()
}
}
}
let didStartForced = await dashboardLoader.waitUntilStartedWithin(count: 2)
#expect(didStartForced)
guard didStartForced else {
forcedRefresh.cancel()
store.cancelForcedRefreshEnrichment()
store.invalidateOpenAIDashboardRefreshTask()
await dashboardLoader.cancelAll()
await staleDashboardTask?.value
await backgroundTask?.value
await forcedRefresh.value
return
}
#expect(staleDashboardTask?.isCancelled == true)
#expect(backgroundTask?.isCancelled == true)
await dashboardLoader.resumeNext(with: .failure(URLError(.timedOut)))
await staleDashboardTask?.value
await backgroundTask?.value
#expect(store.lastOpenAIDashboardError == nil)
await dashboardLoader.resumeNext(with: .success(OpenAIDashboardSnapshot(
signedInEmail: managedAccount.email,
codeReviewRemainingPercent: 95,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
creditsRemaining: 25,
accountPlan: "Pro",
updatedAt: Date())))
await forcedRefresh.value
#expect(allowNavigationTimeoutRetries == [false, true])
#expect(dashboardInteractions == [.background, .userInitiated])
#expect(store.openAIDashboard?.creditsRemaining == 25)
#expect(store.lastOpenAIDashboardError == nil)
#expect(!store.hasForcedRefreshEnrichmentInFlight)
}
@Test
func `account scoped refresh supersedes weaker background dashboard request`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-account-dashboard-supersedes-background")
settings.statusChecksEnabled = false
settings.costUsageEnabled = false
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
defer { try? FileManager.default.removeItem(atPath: managedAccount.managedHomePath) }
let store = self.makeStore(settings: settings)
let dashboardLoader = BlockingManagedOpenAIDashboardLoader()
var allowNavigationTimeoutRetries: [Bool] = []
store._test_providerRefreshOverride = { _ in }
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
store._test_openAIDashboardLoaderOverride = { _, _, allowNavigationTimeoutRetry, _ in
allowNavigationTimeoutRetries.append(allowNavigationTimeoutRetry)
return try await dashboardLoader.awaitResult()
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_openAIDashboardLoaderOverride = nil
}
store.scheduleOpenAIDashboardRefreshIfNeeded(
expectedGuard: store.freshCodexOpenAIWebRefreshGuard())
let didStartBackground = await dashboardLoader.waitUntilStartedWithin(count: 1)
#expect(didStartBackground)
guard didStartBackground else {
store.invalidateOpenAIDashboardRefreshTask()
await dashboardLoader.cancelAll()
return
}
let staleDashboardTask = store.openAIDashboardRefreshTask
let backgroundTask = store.openAIDashboardBackgroundRefreshTask
let accountRefresh = Task { @MainActor in
await BrowserCookieAccessGate.withExplicitRetry {
await ProviderInteractionContext.$current.withValue(.userInitiated) {
await store.refreshCodexAccountScopedState()
}
}
}
let didStartForced = await dashboardLoader.waitUntilStartedWithin(count: 2)
#expect(didStartForced)
guard didStartForced else {
accountRefresh.cancel()
store.invalidateOpenAIDashboardRefreshTask()
await dashboardLoader.cancelAll()
await staleDashboardTask?.value
await backgroundTask?.value
await accountRefresh.value
return
}
#expect(staleDashboardTask?.isCancelled == true)
#expect(backgroundTask?.isCancelled == true)
await dashboardLoader.resumeNext(with: .failure(URLError(.timedOut)))
await staleDashboardTask?.value
await backgroundTask?.value
#expect(store.lastOpenAIDashboardError == nil)
await dashboardLoader.resumeNext(with: .success(OpenAIDashboardSnapshot(
signedInEmail: managedAccount.email,
codeReviewRemainingPercent: 95,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
creditsRemaining: 25,
accountPlan: "Pro",
updatedAt: Date())))
await accountRefresh.value
#expect(allowNavigationTimeoutRetries == [false, true])
#expect(store.openAIDashboard?.creditsRemaining == 25)
#expect(store.lastOpenAIDashboardError == nil)
}
@Test
func `forced background refresh detaches stale dashboard before its tail`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-forced-dashboard-detaches-account")
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
let alphaAccount = try Self.installManagedAccount(
email: "alpha@example.com",
settings: settings)
defer { try? FileManager.default.removeItem(atPath: alphaAccount.managedHomePath) }
let store = self.makeStore(settings: settings)
store.syncOpenAIWebState()
let alphaDashboard = OpenAIDashboardSnapshot(
signedInEmail: alphaAccount.email,
codeReviewRemainingPercent: 95,
creditEvents: [],
dailyBreakdown: [],
usageBreakdown: [],
creditsPurchaseURL: nil,
creditsRemaining: 25,
accountPlan: "Pro",
updatedAt: Date())
store.openAIDashboard = alphaDashboard
store.openAIDashboardAttachmentAuthorized = true
store.lastOpenAIDashboardSnapshot = alphaDashboard
store.lastOpenAIDashboardAttachmentAuthorized = true
let betaAccount = ManagedCodexAccount(
id: UUID(),
email: "beta@example.com",
managedHomePath: "/tmp/codexbar-managed-beta",
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
let tokenGate = BlockingForcedTokenRefresh()
store._test_providerRefreshOverride = { provider in
guard provider == .codex else { return }
settings._test_activeManagedCodexAccount = betaAccount
settings.codexActiveSource = .managedAccount(id: betaAccount.id)
}
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
store._test_tokenUsageRefreshOverride = { provider, force in
guard force else { return }
await tokenGate.run(
provider: provider,
force: force,
interaction: ProviderInteractionContext.current,
refreshPhase: ProviderRefreshContext.current,
browserRetryAllowed: false)
}
defer {
settings._test_activeManagedCodexAccount = nil
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_tokenUsageRefreshOverride = nil
}
await store.refresh(enrichmentMode: .forcedBackground)
#expect(await tokenGate.waitUntilStarted(count: 1))
#expect(store.openAIDashboard == nil)
#expect(!store.openAIDashboardAttachmentAuthorized)
#expect(store.lastOpenAIDashboardSnapshot == nil)
#expect(!store.lastOpenAIDashboardAttachmentAuthorized)
#expect(store.openAIDashboardRequiresLogin)
#expect(store.hasForcedRefreshEnrichmentInFlight)
let enrichmentTask = store.forcedRefreshEnrichmentTask
store.cancelForcedRefreshEnrichment()
await tokenGate.resumeNext()
await enrichmentTask?.value
}
@Test
func `forced token tail excludes periodic token sequence`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-forced-token-excludes-timer")
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.openAIWebAccessEnabled = false
let store = self.makeStore(settings: settings)
let tokenGate = BlockingForcedTokenRefresh()
store._test_providerRefreshOverride = { _ in }
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
store._test_tokenUsageRefreshOverride = { provider, force in
await tokenGate.run(
provider: provider,
force: force,
interaction: ProviderInteractionContext.current,
refreshPhase: ProviderRefreshContext.current,
browserRetryAllowed: false)
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_tokenUsageRefreshOverride = nil
}
await store.refresh(enrichmentMode: .forcedBackground)
#expect(await tokenGate.waitUntilStarted(count: 1))
store.scheduleTokenRefreshForTesting()
try await Task.sleep(for: .milliseconds(100))
#expect(await tokenGate.recordedCalls().count == 1)
await tokenGate.resumeNext()
await store.awaitForcedRefreshEnrichment()
let calls = await tokenGate.recordedCalls()
#expect(calls.count == 1)
#expect(calls.first?.force == true)
#expect(!store.hasForcedRefreshEnrichmentInFlight)
}
@Test
func `forced enrichment excludes timer after token child completes`() async throws {
let settings = try self.makeSettingsStore(
suite: "CodexBackgroundRefreshCoalescingTests-forced-tail-excludes-token-timer")
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.openAIWebAccessEnabled = false
let managedAccount = try Self.installManagedAccount(
email: "managed@example.com",
settings: settings)
defer { try? FileManager.default.removeItem(atPath: managedAccount.managedHomePath) }
let store = self.makeStore(settings: settings)
let tokenGate = BlockingForcedTokenRefresh()
let creditsGate = BlockingCreditsLoader()
store._test_providerRefreshOverride = { _ in }
store._test_codexCreditsLoaderOverride = {
try await creditsGate.awaitResult()
}
store._test_tokenUsageRefreshOverride = { provider, force in
await tokenGate.run(
provider: provider,
force: force,
interaction: ProviderInteractionContext.current,
refreshPhase: ProviderRefreshContext.current,
browserRetryAllowed: false)
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_tokenUsageRefreshOverride = nil
}
await store.refresh(enrichmentMode: .forcedBackground)
#expect(await tokenGate.waitUntilStarted(count: 1))
#expect(await creditsGate.waitUntilStartedWithin(count: 1))
await tokenGate.resumeNext()
for _ in 0..<100 where store.tokenRefreshSequenceTask != nil {
await Task.yield()
}
#expect(store.tokenRefreshSequenceTask == nil)
#expect(store.hasForcedRefreshEnrichmentInFlight)
store.scheduleTokenRefreshForTesting()
try await Task.sleep(for: .milliseconds(100))
#expect(store.tokenRefreshSequenceTask == nil)
#expect(await tokenGate.recordedCalls().count == 1)
await creditsGate.resumeNext(with: .success(CreditsSnapshot(
remaining: 25,
events: [],
updatedAt: Date())))
await store.awaitForcedRefreshEnrichment()
#expect(!store.hasForcedRefreshEnrichmentInFlight)
}
}
private actor BlockingRequiredProviderRefresh {
private var interactions: [ProviderInteraction] = []
private var continuations: [(id: UUID, continuation: CheckedContinuation<Void, Never>)] = []
func run(interaction: ProviderInteraction) async {
let id = UUID()
self.interactions.append(interaction)
await withTaskCancellationHandler {
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
if Task.isCancelled {
continuation.resume()
} else {
self.continuations.append((id: id, continuation: continuation))
}
}
} onCancel: {
Task { await self.cancel(id: id) }
}
}
func waitUntilStarted(count: Int, timeout: Duration = .seconds(5)) async -> Bool {
let deadline = ContinuousClock.now + timeout
while self.interactions.count < count {
if ContinuousClock.now >= deadline {
return false
}
try? await Task.sleep(for: .milliseconds(10))
}
return true
}
func startedCount() -> Int {
self.interactions.count
}
func recordedInteractions() -> [ProviderInteraction] {
self.interactions
}
func resumeNext() {
guard !self.continuations.isEmpty else { return }
self.continuations.removeFirst().continuation.resume()
}
func cancelAll() {
let continuations = self.continuations
self.continuations.removeAll()
continuations.forEach { $0.continuation.resume() }
}
private func cancel(id: UUID) {
guard let index = self.continuations.firstIndex(where: { $0.id == id }) else { return }
self.continuations.remove(at: index).continuation.resume()
}
}