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

178 lines
6.4 KiB
Swift

import AppKit
import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@MainActor
@Suite(.serialized)
struct DeferredMenuInteractionRefreshTailTests {
@Test
func `repeated scheduling during forced enrichment produces one deferred refresh`() async {
let settings = testSettingsStore(
suiteName: "DeferredMenuInteractionRefreshTailTests-forced-tail")
settings.providerDetectionCompleted = true
settings.refreshFrequency = .manual
settings.statusChecksEnabled = false
settings.costUsageEnabled = true
settings.openAIWebAccessEnabled = false
settings.codexCookieSource = .off
for provider in UsageProvider.allCases {
guard let metadata = ProviderRegistry.shared.metadata[provider] else { continue }
settings.setProviderEnabled(
provider: provider,
metadata: metadata,
enabled: provider == .codex)
}
let isolatedRoot = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-deferred-refresh-tests", isDirectory: true)
.appendingPathComponent(UUID().uuidString, isDirectory: true)
let environment = [
"HOME": isolatedRoot.path,
"CODEX_HOME": isolatedRoot.appendingPathComponent(".codex", isDirectory: true).path,
]
let fetcher = UsageFetcher(environment: environment)
let store = UsageStore(
fetcher: fetcher,
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
startupBehavior: .testing,
environmentBase: environment)
let tokenTail = DeferredMenuRefreshTokenTailBlocker()
var providerRefreshCount = 0
store._test_providerRefreshOverride = { provider in
guard provider == .codex else { return }
providerRefreshCount += 1
}
store._test_codexCreditsLoaderOverride = {
CreditsSnapshot(remaining: 25, events: [], updatedAt: Date())
}
store._test_tokenUsageRefreshOverride = { provider, force in
guard provider == .codex, force else { return }
await tokenTail.run()
}
defer {
store._test_providerRefreshOverride = nil
store._test_codexCreditsLoaderOverride = nil
store._test_tokenUsageRefreshOverride = nil
}
let controller = StatusItemController(
store: store,
settings: settings,
account: AccountInfo(email: nil, plan: nil),
updater: DisabledUpdaterController(),
preferencesSelection: PreferencesSelection(),
statusBar: testStatusBar(),
menuCardRenderingEnabled: false,
menuRefreshEnabled: false)
defer {
controller.cancelDeferredMenuInteractionRefreshTask()
controller.releaseStatusItemsForTesting()
}
var deferredRefreshCount = 0
controller.onDeferredMenuInteractionRefreshForTesting = {
deferredRefreshCount += 1
}
await store.refresh(enrichmentMode: .forcedBackground)
let enrichmentTask = store.forcedRefreshEnrichmentTask
let didStartTail = await tokenTail.waitUntilStarted()
#expect(didStartTail)
guard didStartTail else {
store.cancelForcedRefreshEnrichment()
await enrichmentTask?.value
return
}
#expect(providerRefreshCount == 1)
#expect(store.hasForcedRefreshEnrichmentInFlight)
// The follow-up automatic refresh should not start another token-cost tail.
settings.costUsageEnabled = false
controller.deferMenuInteractionRefreshIfNeeded(providers: [.codex])
for _ in 0..<3 {
controller.scheduleDeferredMenuInteractionRefreshIfNeeded(delay: .zero)
try? await Task.sleep(for: .milliseconds(30))
}
#expect(deferredRefreshCount == 0)
#expect(providerRefreshCount == 1)
#expect(controller.deferredMenuInteractionRefreshProviders == [.codex])
#expect(controller.deferredMenuInteractionRefreshTask != nil)
await tokenTail.release()
await enrichmentTask?.value
controller.scheduleDeferredMenuInteractionRefreshIfNeeded(delay: .zero)
let completedExactlyOnce = await Self.waitUntil {
deferredRefreshCount == 1 &&
providerRefreshCount == 2 &&
!controller.deferredMenuInteractionRefreshPending
}
#expect(completedExactlyOnce)
#expect(deferredRefreshCount == 1)
#expect(providerRefreshCount == 2)
#expect(controller.deferredMenuInteractionRefreshProviders.isEmpty)
}
private static func waitUntil(
timeout: Duration = .seconds(2),
condition: @escaping @MainActor () -> Bool) async -> Bool
{
let deadline = ContinuousClock.now + timeout
while !condition() {
if ContinuousClock.now >= deadline {
return false
}
try? await Task.sleep(for: .milliseconds(10))
}
return true
}
}
private actor DeferredMenuRefreshTokenTailBlocker {
private var started = 0
private var released = false
private var waiter: (id: UUID, continuation: CheckedContinuation<Void, Never>)?
func run() async {
let id = UUID()
self.started += 1
await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
if self.released || Task.isCancelled {
continuation.resume()
} else {
self.waiter = (id: id, continuation: continuation)
}
}
} onCancel: {
Task { await self.cancel(id: id) }
}
}
func waitUntilStarted(timeout: Duration = .seconds(2)) async -> Bool {
let deadline = ContinuousClock.now + timeout
while self.started == 0 {
if ContinuousClock.now >= deadline {
return false
}
try? await Task.sleep(for: .milliseconds(10))
}
return true
}
func release() {
self.released = true
self.waiter?.continuation.resume()
self.waiter = nil
}
private func cancel(id: UUID) {
guard self.waiter?.id == id else { return }
self.waiter?.continuation.resume()
self.waiter = nil
}
}