import CodexBarCore import Foundation import Testing @testable import CodexBar @Suite(.serialized) @MainActor struct CodexAccountScopedRefreshTests { @Test func `account transition invalidates codex scoped state and preserves token usage`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-invalidate") settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) let staleSnapshot = self.codexSnapshot(email: "alpha@example.com", usedPercent: 10) let staleCredits = self.credits(remaining: 42) let staleDashboard = self.dashboard(email: "alpha@example.com", creditsRemaining: 42, usedPercent: 20) let tokenSnapshot = CostUsageTokenSnapshot( sessionTokens: 120, sessionCostUSD: 1.2, last30DaysTokens: 900, last30DaysCostUSD: 9.0, daily: [], updatedAt: Date()) var widgetSnapshots: [WidgetSnapshot] = [] store._setSnapshotForTesting(staleSnapshot, provider: .codex) store.credits = staleCredits store.lastCreditsSnapshot = staleCredits store.lastCreditsSnapshotAccountKey = "alpha@example.com" store.lastCreditsSource = .api store.openAIDashboard = staleDashboard store.lastOpenAIDashboardSnapshot = staleDashboard store.lastOpenAIDashboardTargetEmail = "alpha@example.com" store._setTokenSnapshotForTesting(tokenSnapshot, provider: .codex) store.lastCodexAccountScopedRefreshGuard = store .currentCodexAccountScopedRefreshGuard(preferCurrentSnapshot: false) store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) } defer { store._test_widgetSnapshotSaveOverride = nil } settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") let didInvalidate = store.prepareCodexAccountScopedRefreshIfNeeded() await store.widgetSnapshotPersistTask?.value #expect(didInvalidate) #expect(store.snapshots[.codex] == nil) #expect(store.credits == nil) #expect(store.lastCreditsSnapshot == nil) #expect(store.lastCreditsSnapshotAccountKey == nil) #expect(store.lastCreditsSource == .none) #expect(store.openAIDashboard == nil) #expect(store.lastOpenAIDashboardSnapshot == nil) #expect(store.tokenSnapshots[.codex] == tokenSnapshot) #expect(widgetSnapshots.count == 1) #expect(widgetSnapshots[0].entries.contains(where: { $0.provider == .codex }) == false) } @Test func `first switch invalidates after codex refresh seeds the previous account guard`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-first-switch") settings.refreshFrequency = .manual settings.codexCookieSource = .off settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) self.installImmediateCodexProvider( on: store, snapshot: self.codexSnapshot(email: "alpha@example.com", usedPercent: 10)) await store.refreshProvider(.codex, allowDisabled: true) #expect(store.lastCodexAccountScopedRefreshGuard?.accountKey == "alpha@example.com") settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") let didInvalidate = store.prepareCodexAccountScopedRefreshIfNeeded() #expect(didInvalidate) #expect(store.snapshots[.codex] == nil) } @Test func `stale codex usage success is discarded after account switch`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-stale-success") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) let refreshTask = Task { await store.refreshProvider(.codex, allowDisabled: true) } await blocker.waitUntilStarted() settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") await blocker.resume(with: .success(self.codexSnapshot(email: "alpha@example.com", usedPercent: 25))) await refreshTask.value #expect(store.snapshots[.codex] == nil) #expect(store.errors[.codex] == nil) } @Test func `same email provider account switch discards stale codex usage success`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-stale-same-email-provider-account") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount( email: "alpha@example.com", identity: .providerAccount(id: "acct-alpha")) let store = self.makeUsageStore(settings: settings) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) let refreshTask = Task { await store.refreshProvider(.codex, allowDisabled: true) } await blocker.waitUntilStarted() settings._test_liveSystemCodexAccount = self.liveAccount( email: "alpha@example.com", identity: .providerAccount(id: "acct-beta")) await blocker.resume(with: .success(self.codexSnapshot(email: "alpha@example.com", usedPercent: 25))) await refreshTask.value #expect(store.snapshots[.codex] == nil) #expect(store.errors[.codex] == nil) } @Test func `stale codex usage failure does not clear newer account snapshot`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-stale-failure") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) let refreshTask = Task { await store.refreshProvider(.codex, allowDisabled: true) } await blocker.waitUntilStarted() settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") let freshSnapshot = self.codexSnapshot(email: "beta@example.com", usedPercent: 5) store._setSnapshotForTesting(freshSnapshot, provider: .codex) let betaGuard = store.freshCodexAccountScopedRefreshGuard() store.lastCodexUsagePublicationGuard = betaGuard store.lastCodexAccountScopedRefreshGuard = betaGuard await blocker.resume(with: .failure(TestRefreshError(message: "stale failure"))) await refreshTask.value #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "beta@example.com") #expect(store.errors[.codex] == nil) } @Test func `codex visible account refresh preserves prior snapshots when network fails`() async throws { try await self.withCodexVisibleAccountFailureStore( suite: "CodexAccountScopedRefreshTests-preserve-codex-snapshots", errorMessage: "Network error: offline") { store, snapshotStore, priorSnapshots in await store.refreshCodexVisibleAccountsForMenu() #expect(store.codexAccountSnapshots.count == priorSnapshots.count) #expect(store.codexAccountSnapshots.allSatisfy { $0.snapshot?.primary?.usedPercent == 17 }) #expect(store.codexAccountSnapshots.allSatisfy { $0.error == "Network error: offline" }) #expect(store.codexAccountSnapshots.allSatisfy { $0.sourceLabel == "cached" }) let persisted = snapshotStore.storedSnapshots #expect(persisted.count == priorSnapshots.count) #expect(persisted.allSatisfy { $0.snapshot?.primary?.usedPercent == 17 }) #expect(persisted.allSatisfy { $0.error == "Network error: offline" }) } } @Test func `codex visible account refresh drops prior snapshots when auth fails`() async throws { try await self.withCodexVisibleAccountFailureStore( suite: "CodexAccountScopedRefreshTests-drop-auth-failed-snapshots", errorMessage: "401 Unauthorized") { store, snapshotStore, priorSnapshots in await store.refreshCodexVisibleAccountsForMenu() #expect(store.codexAccountSnapshots.count == priorSnapshots.count) #expect(store.codexAccountSnapshots.allSatisfy { $0.snapshot == nil }) #expect(store.codexAccountSnapshots.allSatisfy { $0.error == "401 Unauthorized" }) let persisted = snapshotStore.storedSnapshots #expect(persisted.count == priorSnapshots.count) #expect(persisted.allSatisfy { $0.snapshot == nil }) #expect(persisted.allSatisfy { $0.error == "401 Unauthorized" }) } } @Test func `credits fallback only reuses cache for the same codex account`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-credits") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) let cachedCredits = self.credits(remaining: 12) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 10), provider: .codex) store.lastCreditsSnapshot = cachedCredits store.lastCreditsSnapshotAccountKey = "alpha@example.com" store._test_codexCreditsLoaderOverride = { throw TestRefreshError(message: "Codex credits data not available yet") } defer { store._test_codexCreditsLoaderOverride = nil } await store.refreshCreditsIfNeeded() #expect(store.credits == cachedCredits) #expect(store.lastCreditsError == nil) settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") store._setSnapshotForTesting(self.codexSnapshot(email: "beta@example.com", usedPercent: 10), provider: .codex) await store.refreshCreditsIfNeeded() #expect(store.credits == nil) #expect(store.lastCreditsSource == .none) #expect(store.lastCreditsError == "Codex credits are still loading; will retry shortly.") } @Test func `managed refresh invalidation keeps state when provider account is unchanged`() throws { let settings = self.makeSettingsStore( suite: "CodexAccountScopedRefreshTests-managed-renamed-email") let managedHome = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) defer { try? FileManager.default.removeItem(at: managedHome) } try Self.writeCodexAuthFile( homeURL: managedHome, email: "renamed@example.com", plan: "pro", accountId: "acct-managed") let managedAccountID = UUID() let managedAccount = ManagedCodexAccount( id: managedAccountID, email: "legacy@example.com", managedHomePath: managedHome.path, createdAt: 1, updatedAt: 1, lastAuthenticatedAt: 1) settings._test_activeManagedCodexAccount = managedAccount settings.codexActiveSource = .managedAccount(id: managedAccountID) defer { settings._test_activeManagedCodexAccount = nil } let store = self.makeUsageStore(settings: settings) let currentSnapshot = self.codexSnapshot(email: "renamed@example.com", usedPercent: 10) let currentCredits = self.credits(remaining: 42) let currentDashboard = self.dashboard(email: "renamed@example.com", creditsRemaining: 42, usedPercent: 20) store._setSnapshotForTesting(currentSnapshot, provider: .codex) store.credits = currentCredits store.lastCreditsSnapshot = currentCredits store.lastCreditsSnapshotAccountKey = "renamed@example.com" store.openAIDashboard = currentDashboard store.lastOpenAIDashboardSnapshot = currentDashboard store.lastOpenAIDashboardTargetEmail = "renamed@example.com" store.seedCodexAccountScopedRefreshGuard( source: .managedAccount(id: managedAccountID), accountEmail: "renamed@example.com") let currentGuard = store.currentCodexAccountScopedRefreshGuard( preferCurrentSnapshot: false, allowLastKnownLiveFallback: false) let didInvalidate = store.prepareCodexAccountScopedRefreshIfNeeded() #expect(currentGuard.identity == .providerAccount(id: "acct-managed")) #expect(currentGuard.accountKey == "renamed@example.com") #expect(didInvalidate == false) #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "renamed@example.com") #expect(store.credits?.remaining == 42) #expect(store.openAIDashboard?.signedInEmail == "renamed@example.com") } @Test func `credits refresh returns quickly when no live codex account is available`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-credits-no-live-account") let isolatedHome = FileManager.default.temporaryDirectory .appendingPathComponent("codex-credits-no-live-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: isolatedHome, withIntermediateDirectories: true) settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = nil settings._test_codexReconciliationEnvironment = ["CODEX_HOME": isolatedHome.path] defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: isolatedHome) } let store = self.makeUsageStore(settings: settings) var loaderCalled = false store._test_codexCreditsLoaderOverride = { loaderCalled = true return self.credits(remaining: 1) } defer { store._test_codexCreditsLoaderOverride = nil } let startedAt = ContinuousClock.now await store.refreshCreditsIfNeeded(minimumSnapshotUpdatedAt: Date()) let elapsed = startedAt.duration(to: .now) #expect(loaderCalled == false) #expect(elapsed < .seconds(3)) } @Test func `stale dashboard apply is discarded after account switch`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) let expectedGuard = store.currentCodexAccountScopedRefreshGuard() settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") await store.applyOpenAIDashboard( self.dashboard(email: "alpha@example.com", creditsRemaining: 11, usedPercent: 35), targetEmail: "alpha@example.com", expectedGuard: expectedGuard, allowCodexUsageBackfill: true) #expect(store.openAIDashboard == nil) #expect(store.snapshots[.codex] == nil) #expect(store.credits == nil) } @Test func `dashboard refresh fail closes when live identity is unresolved without trusted continuity`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-unresolved-fail-closed") let isolatedHome = FileManager.default.temporaryDirectory .appendingPathComponent("codex-openai-web-unresolved-fail-closed-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: isolatedHome, withIntermediateDirectories: true) settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = nil settings._test_codexReconciliationEnvironment = ["CODEX_HOME": isolatedHome.path] defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: isolatedHome) } let store = self.makeUsageStore(settings: settings) store.lastKnownLiveSystemCodexEmail = nil store._test_openAIDashboardLoaderOverride = { _, _, _, _ in self.dashboard(email: "seeded@example.com", creditsRemaining: 33, usedPercent: 12) } defer { store._test_openAIDashboardLoaderOverride = nil } let expectedGuard = store.currentCodexAccountScopedRefreshGuard() #expect(expectedGuard.source == .liveSystem) #expect(expectedGuard.identity == .unresolved) #expect(expectedGuard.accountKey == nil) await store.refreshOpenAIDashboardIfNeeded(force: true, expectedGuard: expectedGuard) #expect(store.openAIDashboard == nil) #expect(store.lastOpenAIDashboardSnapshot == nil) #expect(store.snapshots[.codex] == nil) #expect(store.credits == nil) #expect(store.openAIDashboardRequiresLogin == true) #expect(store.lastOpenAIDashboardError?.contains("could not be verified") == true) } @Test func `dashboard refresh attaches for unresolved live identity with trusted non dashboard continuity`() async { let settings = self.makeSettingsStore( suite: "CodexAccountScopedRefreshTests-dashboard-unresolved-trusted-continuity") let isolatedHome = FileManager.default.temporaryDirectory .appendingPathComponent("codex-openai-web-unresolved-trusted-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: isolatedHome, withIntermediateDirectories: true) settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = nil settings._test_codexReconciliationEnvironment = ["CODEX_HOME": isolatedHome.path] defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: isolatedHome) } let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting( self.codexSnapshot(email: "trusted@example.com", usedPercent: 12), provider: .codex) store.lastSourceLabels[.codex] = "codex-cli" let trustedGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: .emailOnly(normalizedEmail: "trusted@example.com"), accountKey: "trusted@example.com") store.lastCodexUsagePublicationGuard = trustedGuard store.lastCodexAccountScopedRefreshGuard = trustedGuard store._test_openAIDashboardLoaderOverride = { _, _, _, _ in self.dashboard(email: "trusted@example.com", creditsRemaining: 33, usedPercent: 12) } defer { store._test_openAIDashboardLoaderOverride = nil } let expectedGuard = store.currentCodexOpenAIWebRefreshGuard() #expect(expectedGuard.identity == .unresolved) await store.refreshOpenAIDashboardIfNeeded(force: true, expectedGuard: expectedGuard) #expect(store.openAIDashboard?.signedInEmail == "trusted@example.com") #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "trusted@example.com") #expect(store.lastSourceLabels[.codex] == "codex-cli") #expect(store.credits?.remaining == 33) #expect(store.lastCreditsSource == .dashboardWeb) #expect(store.lastCreditsSnapshotAccountKey == "trusted@example.com") #expect( store.lastCodexAccountScopedRefreshGuard?.identity == .emailOnly(normalizedEmail: "trusted@example.com")) #expect(store.lastCodexAccountScopedRefreshGuard?.accountKey == "trusted@example.com") } @Test func `no usable codex usage does not block weekly only dashboard backfill`() async { let settings = self.makeSettingsStore( suite: "CodexAccountScopedRefreshTests-no-usable-usage-weekly-dashboard-backfill") settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = self.liveAccount( email: "weekly@example.com", identity: .providerAccount(id: "acct-weekly")) let store = self.makeUsageStore(settings: settings) self.installFailingCodexProvider(on: store, error: UsageError.noRateLimitsFound) await store.refreshProvider(.codex, allowDisabled: true) #expect(store.snapshots[.codex] == nil) await store.applyOpenAIDashboard( OpenAIDashboardSnapshot( signedInEmail: "weekly@example.com", codeReviewRemainingPercent: 88, creditEvents: [], dailyBreakdown: [], usageBreakdown: [], creditsPurchaseURL: nil, primaryLimit: nil, secondaryLimit: RateWindow( usedPercent: 27, windowMinutes: 10080, resetsAt: Date(timeIntervalSince1970: 1_775_000_000), resetDescription: "next week"), creditsRemaining: 14, accountPlan: "Pro", updatedAt: Date(timeIntervalSince1970: 1_774_900_000)), targetEmail: "weekly@example.com", allowCodexUsageBackfill: true) #expect(store.openAIDashboard?.signedInEmail == "weekly@example.com") #expect(store.snapshots[.codex]?.primary == nil) #expect(store.snapshots[.codex]?.secondary?.usedPercent == 27) #expect(store.snapshots[.codex]?.secondary?.windowMinutes == 10080) #expect(store.lastSourceLabels[.codex] == "openai-web") } @Test func `dashboard display only keeps dashboard visible and clears dashboard derived data`() async throws { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-display-only-cleanup") let managedHome = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) defer { try? FileManager.default.removeItem(at: managedHome) } try Self.writeCodexAuthFile( homeURL: managedHome, email: "shared@example.com", plan: "pro", accountId: "acct-managed") let managedAccount = ManagedCodexAccount( id: UUID(), email: "shared@example.com", managedHomePath: managedHome.path, createdAt: 1, updatedAt: 1, lastAuthenticatedAt: 1) let managedStoreURL = try self.makeManagedAccountStoreURL(accounts: [managedAccount]) defer { settings._test_managedCodexAccountStoreURL = nil try? FileManager.default.removeItem(at: managedStoreURL) OpenAIDashboardCacheStore.clear() } settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_managedCodexAccountStoreURL = managedStoreURL settings._test_liveSystemCodexAccount = self.liveAccount( email: "shared@example.com", identity: .emailOnly(normalizedEmail: "shared@example.com")) settings.codexActiveSource = .liveSystem let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "shared@example.com", usedPercent: 20), provider: .codex) store.lastSourceLabels[.codex] = "openai-web" let staleCredits = self.credits(remaining: 20) store.credits = staleCredits store.lastCreditsSnapshot = staleCredits store.lastCreditsSnapshotAccountKey = "shared@example.com" store.lastCreditsSource = .dashboardWeb OpenAIDashboardCacheStore.save(OpenAIDashboardCache( accountEmail: "shared@example.com", snapshot: self.dashboard(email: "shared@example.com", creditsRemaining: 20, usedPercent: 20))) await store.applyOpenAIDashboard( self.dashboard(email: "shared@example.com", creditsRemaining: 9, usedPercent: 35), targetEmail: "shared@example.com") #expect(store.openAIDashboard?.signedInEmail == "shared@example.com") #expect(store.lastOpenAIDashboardSnapshot?.signedInEmail == "shared@example.com") #expect(store.snapshots[.codex] == nil) #expect(store.lastSourceLabels[.codex] == nil) #expect(store.credits == nil) #expect(store.lastCreditsSource == .none) #expect(OpenAIDashboardCacheStore.load() == nil) } @Test func `dashboard downgrade from real attach to display only retires owned state immediately`() async throws { OpenAIDashboardCacheStore.clear() defer { OpenAIDashboardCacheStore.clear() } let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-downgrade") let managedHome = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) defer { try? FileManager.default.removeItem(at: managedHome) } try Self.writeCodexAuthFile( homeURL: managedHome, email: "shared@example.com", plan: "pro", accountId: "acct-managed") let managedAccount = ManagedCodexAccount( id: UUID(), email: "shared@example.com", managedHomePath: managedHome.path, createdAt: 1, updatedAt: 1, lastAuthenticatedAt: 1) let managedStoreURL = try self.makeManagedAccountStoreURL(accounts: [managedAccount]) defer { settings._test_managedCodexAccountStoreURL = nil try? FileManager.default.removeItem(at: managedStoreURL) } settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = self.liveAccount( email: "shared@example.com", identity: .emailOnly(normalizedEmail: "shared@example.com")) settings.codexActiveSource = .liveSystem let store = self.makeUsageStore(settings: settings) await store.applyOpenAIDashboard( self.dashboard(email: "shared@example.com", creditsRemaining: 20, usedPercent: 20), targetEmail: "shared@example.com") #expect(store.openAIDashboard?.signedInEmail == "shared@example.com") #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "shared@example.com") #expect(store.lastSourceLabels[.codex] == "openai-web") #expect(store.credits?.remaining == 20) #expect(store.lastCreditsSource == .dashboardWeb) #expect(OpenAIDashboardCacheStore.load()?.accountEmail == "shared@example.com") settings._test_managedCodexAccountStoreURL = managedStoreURL await store.applyOpenAIDashboard( self.dashboard(email: "shared@example.com", creditsRemaining: 9, usedPercent: 35), targetEmail: "shared@example.com") #expect(store.openAIDashboard?.signedInEmail == "shared@example.com") #expect(store.lastOpenAIDashboardSnapshot?.signedInEmail == "shared@example.com") #expect(store.snapshots[.codex] == nil) #expect(store.lastSourceLabels[.codex] == nil) #expect(store.credits == nil) #expect(store.lastCreditsSource == .none) #expect(OpenAIDashboardCacheStore.load() == nil) } @Test func `dashboard refresh rejects stale completion during live account reconciliation lag`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-reject-stale-live-lag") let isolatedHome = FileManager.default.temporaryDirectory .appendingPathComponent("codex-openai-web-stale-live-lag-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: isolatedHome, withIntermediateDirectories: true) settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_codexReconciliationEnvironment = ["CODEX_HOME": isolatedHome.path] settings.codexActiveSource = .liveSystem defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: isolatedHome) } let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 12), provider: .codex) let expectedGuard = store.currentCodexOpenAIWebRefreshGuard() #expect(expectedGuard.accountKey == nil) store._setSnapshotForTesting(self.codexSnapshot(email: "beta@example.com", usedPercent: 18), provider: .codex) await store.applyOpenAIDashboard( self.dashboard(email: "alpha@example.com", creditsRemaining: 40, usedPercent: 20), targetEmail: nil, expectedGuard: expectedGuard, allowCodexUsageBackfill: true) #expect(store.openAIDashboard == nil) #expect(store.credits == nil) #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "beta@example.com") } @Test func `default dashboard refresh path discards stale completion after account switch`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-guard") settings.refreshFrequency = .manual settings.openAIWebAccessEnabled = true settings.codexCookieSource = .auto settings.statusChecksEnabled = false settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) self.installImmediateCodexProvider( on: store, snapshot: self.codexSnapshot(email: "alpha@example.com", usedPercent: 18)) await store.refresh() let dashboardBlocker = BlockingOpenAIDashboardLoader() store._test_openAIDashboardLoaderOverride = { _, _, _, _ in try await dashboardBlocker.awaitResult() } defer { store._test_openAIDashboardLoaderOverride = nil } let refreshTask = Task { await store.refresh() } await dashboardBlocker.waitUntilStarted() settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") store._setSnapshotForTesting(self.codexSnapshot(email: "beta@example.com", usedPercent: 7), provider: .codex) store.openAIDashboard = nil store.credits = nil await dashboardBlocker.resume(with: .success( self.dashboard(email: "alpha@example.com", creditsRemaining: 44, usedPercent: 21))) await refreshTask.value #expect(store.openAIDashboard == nil) #expect(store.credits == nil) #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "beta@example.com") } @Test func `same email provider account switch discards stale dashboard completion`() async { let settings = self .makeSettingsStore(suite: "CodexAccountScopedRefreshTests-dashboard-same-email-provider-account") settings.refreshFrequency = .manual settings._test_liveSystemCodexAccount = self.liveAccount( email: "alpha@example.com", identity: .providerAccount(id: "acct-alpha")) let store = self.makeUsageStore(settings: settings) let expectedGuard = store.currentCodexOpenAIWebRefreshGuard() #expect(expectedGuard.identity == .providerAccount(id: "acct-alpha")) settings._test_liveSystemCodexAccount = self.liveAccount( email: "alpha@example.com", identity: .providerAccount(id: "acct-beta")) await store.applyOpenAIDashboard( self.dashboard(email: "alpha@example.com", creditsRemaining: 11, usedPercent: 35), targetEmail: "alpha@example.com", expectedGuard: expectedGuard, allowCodexUsageBackfill: true) #expect(store.openAIDashboard == nil) #expect(store.snapshots[.codex] == nil) #expect(store.credits == nil) } @Test func `live switch invalidates stale codex state even when only last known live email remains`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-invalidate-with-stale-last-known") let isolatedHome = FileManager.default.temporaryDirectory .appendingPathComponent("codex-invalidate-stale-last-known-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: isolatedHome, withIntermediateDirectories: true) settings.refreshFrequency = .manual settings.codexCookieSource = .auto settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") settings._test_codexReconciliationEnvironment = ["CODEX_HOME": isolatedHome.path] defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: isolatedHome) } let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 10), provider: .codex) store.credits = self.credits(remaining: 12) store.lastCreditsSnapshot = self.credits(remaining: 12) store.lastCreditsSnapshotAccountKey = "alpha@example.com" store.openAIDashboard = self.dashboard(email: "alpha@example.com", creditsRemaining: 12, usedPercent: 20) store.lastOpenAIDashboardSnapshot = store.openAIDashboard store.lastKnownLiveSystemCodexEmail = "alpha@example.com" store.lastCodexAccountScopedRefreshGuard = store.currentCodexAccountScopedRefreshGuard( preferCurrentSnapshot: false, allowLastKnownLiveFallback: true) settings._test_liveSystemCodexAccount = nil store.snapshots.removeValue(forKey: .codex) let didInvalidate = store.prepareCodexAccountScopedRefreshIfNeeded() await store.widgetSnapshotPersistTask?.value #expect(didInvalidate) #expect(store.snapshots[.codex] == nil) #expect(store.credits == nil) #expect(store.openAIDashboard == nil) #expect(store.lastCodexAccountScopedRefreshGuard?.accountKey == nil) } @Test func `codex account refresh persists widget snapshots on invalidation and completion`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-widgets") settings.refreshFrequency = .manual settings.codexCookieSource = .off settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 18), provider: .codex) store.lastCodexAccountScopedRefreshGuard = store .currentCodexAccountScopedRefreshGuard(preferCurrentSnapshot: false) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) store._test_codexCreditsLoaderOverride = { self.credits(remaining: 77) } defer { store._test_codexCreditsLoaderOverride = nil } var widgetSnapshots: [WidgetSnapshot] = [] store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) } defer { store._test_widgetSnapshotSaveOverride = nil } settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") let refreshTask = Task { await store.refreshCodexAccountScopedState(allowDisabled: true) } await blocker.waitUntilStarted() await blocker.resume(with: .success(self.codexSnapshot(email: "beta@example.com", usedPercent: 8))) await refreshTask.value await store.widgetSnapshotPersistTask?.value #expect(widgetSnapshots.count == 2) #expect(widgetSnapshots[0].entries.contains(where: { $0.provider == .codex }) == false) #expect(widgetSnapshots[1].entries.first { $0.provider == .codex }?.creditsRemaining == 77) } @Test func `widget snapshot saves stay ordered across codex account invalidation and completion`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-widget-order") settings.refreshFrequency = .manual let store = self.makeUsageStore(settings: settings) let saver = BlockingWidgetSnapshotSaver() store._test_widgetSnapshotSaveOverride = { snapshot in await saver.save(snapshot) } defer { store._test_widgetSnapshotSaveOverride = nil } store.persistWidgetSnapshot(reason: "codex-account-invalidate") await saver.waitUntilStarted(count: 1) #expect(await saver.startedCount() == 1) store._setSnapshotForTesting(self.codexSnapshot(email: "beta@example.com", usedPercent: 8), provider: .codex) store.credits = self.credits(remaining: 77) store.persistWidgetSnapshot(reason: "codex-account-refresh") try? await Task.sleep(nanoseconds: 50_000_000) #expect(await saver.startedCount() == 1) await saver.resumeNext() await saver.waitUntilStarted(count: 2) await saver.resumeNext() await store.widgetSnapshotPersistTask?.value let snapshots = await saver.savedSnapshots() #expect(snapshots.count == 2) #expect(snapshots[0].entries.contains(where: { $0.provider == .codex }) == false) #expect(snapshots[1].entries.first { $0.provider == .codex }?.creditsRemaining == 77) } @Test func `widget snapshot excludes display only dashboard code review`() async throws { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-widget-display-only-dashboard") settings.refreshFrequency = .manual let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 18), provider: .codex) store.credits = CreditsSnapshot(remaining: 12, events: [], updatedAt: Date()) store.openAIDashboard = self.dashboard( email: "alpha@example.com", creditsRemaining: 12, usedPercent: 20) store.openAIDashboardAttachmentAuthorized = false var widgetSnapshots: [WidgetSnapshot] = [] store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) } defer { store._test_widgetSnapshotSaveOverride = nil } store.persistWidgetSnapshot(reason: "display-only-dashboard") await store.widgetSnapshotPersistTask?.value let codexEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .codex }) #expect(codexEntry.creditsRemaining == nil) #expect(codexEntry.codeReviewRemainingPercent == nil) } @Test func `widget snapshot includes attached dashboard code review`() async throws { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-widget-attached-dashboard") settings.refreshFrequency = .manual let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 18), provider: .codex) store.openAIDashboard = self.dashboard( email: "alpha@example.com", creditsRemaining: 12, usedPercent: 20) store.openAIDashboardAttachmentAuthorized = true var widgetSnapshots: [WidgetSnapshot] = [] store._test_widgetSnapshotSaveOverride = { widgetSnapshots.append($0) } defer { store._test_widgetSnapshotSaveOverride = nil } store.persistWidgetSnapshot(reason: "attached-dashboard") await store.widgetSnapshotPersistTask?.value let codexEntry = try #require(widgetSnapshots.last?.entries.first { $0.provider == .codex }) #expect(codexEntry.codeReviewRemainingPercent == 88) } @Test func `codex account refresh reports usage and credits phases before completion`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-phases") settings.refreshFrequency = .manual settings.codexCookieSource = .off settings._test_liveSystemCodexAccount = self.liveAccount(email: "alpha@example.com") let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "alpha@example.com", usedPercent: 18), provider: .codex) store.lastCodexAccountScopedRefreshGuard = store .currentCodexAccountScopedRefreshGuard(preferCurrentSnapshot: false) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) store._test_codexCreditsLoaderOverride = { self.credits(remaining: 77) } defer { store._test_codexCreditsLoaderOverride = nil } var phases: [CodexAccountScopedRefreshPhase] = [] settings._test_liveSystemCodexAccount = self.liveAccount(email: "beta@example.com") let refreshTask = Task { await store.refreshCodexAccountScopedState( allowDisabled: true, phaseDidChange: { phases.append($0) }) } await blocker.waitUntilStarted() #expect(phases == [.invalidated]) await blocker.resume(with: .success(self.codexSnapshot(email: "beta@example.com", usedPercent: 8))) await refreshTask.value #expect(phases == [.invalidated, .usage, .credits, .completed]) } @Test func `refresh loads credits when codex email is discovered by usage in the same cycle`() async { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-refresh-credits") settings.refreshFrequency = .manual settings.codexCookieSource = .off let store = self.makeUsageStore(settings: settings) let blocker = BlockingCodexFetchStrategy() self.installBlockingCodexProvider(on: store, blocker: blocker) store._test_codexCreditsLoaderOverride = { self.credits(remaining: 55) } defer { store._test_codexCreditsLoaderOverride = nil } let refreshTask = Task { await store.refresh() } await blocker.waitUntilStarted() await blocker.resume(with: .success(self.codexSnapshot(email: "alpha@example.com", usedPercent: 12))) await refreshTask.value #expect(store.lastCodexAccountScopedRefreshGuard?.accountKey == "alpha@example.com") await store.creditsRefreshTask?.value #expect(store.credits?.remaining == 55) #expect(store.lastCreditsSource == .api) } @Test func `settings codex account selection refreshes credits on the first switch`() async throws { let settings = self.makeSettingsStore(suite: "CodexAccountScopedRefreshTests-settings-selection") settings.refreshFrequency = .manual settings.codexCookieSource = .off let managedAccountID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-111111111111")) let managedAccount = ManagedCodexAccount( id: managedAccountID, email: "managed@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 2) let storeURL = try self.makeManagedAccountStoreURL(accounts: [managedAccount]) defer { settings._test_managedCodexAccountStoreURL = nil settings._test_liveSystemCodexAccount = nil try? FileManager.default.removeItem(at: storeURL) } settings._test_managedCodexAccountStoreURL = storeURL settings._test_liveSystemCodexAccount = self.liveAccount(email: "live@example.com") let store = self.makeUsageStore(settings: settings) store._setSnapshotForTesting(self.codexSnapshot(email: "live@example.com", usedPercent: 30), provider: .codex) store.lastCodexAccountScopedRefreshGuard = store .currentCodexAccountScopedRefreshGuard(preferCurrentSnapshot: false) self.installImmediateCodexProvider( on: store, snapshot: self.codexSnapshot(email: "managed@example.com", usedPercent: 9)) store._test_codexCreditsLoaderOverride = { self.credits(remaining: 55) } defer { store._test_codexCreditsLoaderOverride = nil } let pane = ProvidersPane(settings: settings, store: store) await pane._test_selectCodexVisibleAccount(id: "managed@example.com") #expect(settings.codexActiveSource == .managedAccount(id: managedAccountID)) #expect(store.snapshots[.codex]?.accountEmail(for: .codex) == "managed@example.com") #expect(store.credits?.remaining == 55) } }