import CodexBarCore import Foundation import Testing @testable import CodexBar @MainActor @Suite("Codex session restore notifications") struct CodexSessionQuotaFalseRestoreTests { private let start = Date(timeIntervalSince1970: 1_700_000_000) @Test func `same future boundary suppresses restore and duplicate depletion`() throws { let owner = try self.owner("same-boundary") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 0, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) } @Test func `advanced boundary before trusted expiry stays suppressed`() throws { let owner = try self.owner("early-advanced") let boundary = self.start.addingTimeInterval(5 * 3600) let advanced = boundary.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 0, boundary: advanced, at: self.start.addingTimeInterval(120), owner: owner) self.observe(store, used: 10, boundary: advanced, at: self.start.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) } @Test func `depleted boundary stays frozen until its reset can be proven`() throws { let owner = try self.owner("depleted-advanced") let boundary = self.start.addingTimeInterval(5 * 3600) let advanced = boundary.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 100, boundary: advanced, at: self.start.addingTimeInterval(120), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) self.observe(store, used: 100, boundary: advanced, at: boundary.addingTimeInterval(60), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) self.observe(store, used: 0, boundary: advanced, at: boundary.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == advanced) } @Test func `depleted observation recovers a missing trusted boundary`() throws { let owner = try self.owner("depleted-recovered-boundary") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: nil, at: self.start, owner: owner) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) self.observe(store, used: 20, boundary: boundary, at: self.start.addingTimeInterval(180), owner: owner) self.observe(store, used: 10, boundary: boundary, at: self.start.addingTimeInterval(240), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt == nil) } @Test func `expired baseline boundary cannot produce a single sample restore`() throws { let owner = try self.owner("expired-baseline") let expired = self.start.addingTimeInterval(-60) let future = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 100, boundary: expired, at: self.start, owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == nil) let firstPositive = self.start.addingTimeInterval(60) self.observe(store, used: 20, boundary: future, at: firstPositive, owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt == firstPositive) self.observe(store, used: 10, boundary: future, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == future) } @Test func `boundary expired before evaluation is not trusted`() throws { let owner = try self.owner("expired-before-evaluation") let observedAt = self.start.addingTimeInterval(60) let boundary = self.start.addingTimeInterval(120) let evaluatedAt = self.start.addingTimeInterval(180) let store = Self.makeStore(notifier: SessionQuotaNotifierSpy()) self.observe( store, used: 100, boundary: boundary, at: observedAt, evaluatedAt: evaluatedAt, owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == nil) } @Test func `depletion cannot advance a still future trusted boundary`() throws { let owner = try self.owner("depletion-advanced") let boundary = self.start.addingTimeInterval(5 * 3600) let advanced = boundary.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: advanced, at: self.start.addingTimeInterval(60), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) self.observe(store, used: 20, boundary: advanced, at: boundary.addingTimeInterval(60), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == advanced) } @Test func `pre boundary observation cannot advance metadata when processed later`() throws { let owner = try self.owner("delayed-observation") let boundary = self.start.addingTimeInterval(5 * 3600) let advanced = boundary.addingTimeInterval(5 * 3600) let store = Self.makeStore(notifier: SessionQuotaNotifierSpy()) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe( store, used: 30, boundary: advanced, at: self.start.addingTimeInterval(60), evaluatedAt: boundary.addingTimeInterval(60), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) } @Test(arguments: [false, true]) func `ambiguous post expiry restore requires two fresh observations`(boundaryPresent: Bool) throws { let owner = try self.owner(boundaryPresent ? "expired-equivalent" : "expired-missing") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) let first = boundary.addingTimeInterval(60) self.observe(store, used: 20, boundary: boundaryPresent ? boundary : nil, at: first, owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt == first) self.observe( store, used: 10, boundary: boundaryPresent ? boundary : nil, at: boundary.addingTimeInterval(120), owner: owner) self.observe( store, used: 5, boundary: boundaryPresent ? boundary : nil, at: boundary.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) } @Test func `boundaryless restore requires two fresh observations`() throws { let owner = try self.owner("boundaryless") let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: nil, at: self.start, owner: owner) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 20, boundary: nil, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt != nil) self.observe(store, used: 10, boundary: nil, at: self.start.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) } @Test func `advanced post expiry boundary restores exactly once`() throws { let owner = try self.owner("post-expiry-advanced") let boundary = self.start.addingTimeInterval(5 * 3600) let advanced = boundary.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 20, boundary: advanced, at: boundary.addingTimeInterval(60), owner: owner) self.observe(store, used: 10, boundary: advanced, at: boundary.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == advanced) } @Test func `advanced boundary expired at observation time requires confirmation`() throws { let owner = try self.owner("advanced-expired-at-observation") let boundary = self.start.addingTimeInterval(100) let candidate = self.start.addingTimeInterval(250) let previous = SessionQuotaTransitionState( remaining: 0, source: .primary, observedAt: self.start, codexOwnerKey: owner, trustedResetBoundary: boundary, pendingCodexRestoreObservationAt: nil) let evaluation = SessionQuotaTransitionReducer.evaluate( previous: previous, observation: SessionQuotaTransitionObservation( provider: .codex, remaining: 80, source: .primary, resetBoundary: candidate, observedAt: self.start.addingTimeInterval(300), evaluationTime: self.start.addingTimeInterval(200), codexOwnerKey: owner), notificationsEnabled: true) #expect(evaluation.outcome == .awaitingCodexRestoreConfirmation) #expect(evaluation.state.trustedResetBoundary == boundary) } @Test func `regressed post expiry boundary requires two fresh observations`() throws { let owner = try self.owner("regressed") let boundary = self.start.addingTimeInterval(5 * 3600) let regressed = self.start.addingTimeInterval(10 * 60) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 20, boundary: regressed, at: boundary.addingTimeInterval(60), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt != nil) self.observe(store, used: 10, boundary: regressed, at: boundary.addingTimeInterval(120), owner: owner) self.observe(store, used: 5, boundary: regressed, at: boundary.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted, .restored]) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt == nil) } @Test func `older and equal observations cannot change the depleted baseline`() throws { let owner = try self.owner("observation-order") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) let depletedAt = self.start.addingTimeInterval(120) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: depletedAt, owner: owner) self.observe(store, used: 0, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 0, boundary: boundary, at: depletedAt, owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(store.sessionQuotaTransitionStates[.codex]?.observedAt == depletedAt) } @Test func `owner change establishes a new baseline without restoring`() throws { let ownerA = try self.owner("owner-a") let ownerB = try self.owner("owner-b") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: ownerA) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: ownerA) self.observe(store, used: 0, boundary: boundary, at: self.start.addingTimeInterval(120), owner: ownerB) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.codexOwnerKey == ownerB) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 100) } @Test func `source change establishes a new reducer baseline`() throws { let owner = try self.owner("source-change") let boundary = self.start.addingTimeInterval(5 * 3600) let previous = SessionQuotaTransitionState( remaining: 0, source: .primary, observedAt: self.start, codexOwnerKey: owner, trustedResetBoundary: boundary, pendingCodexRestoreObservationAt: nil) let evaluation = SessionQuotaTransitionReducer.evaluate( previous: previous, observation: SessionQuotaTransitionObservation( provider: .codex, remaining: 100, source: .copilotSecondaryFallback, resetBoundary: boundary, observedAt: self.start.addingTimeInterval(60), evaluationTime: self.start.addingTimeInterval(60), codexOwnerKey: owner), notificationsEnabled: true) #expect(evaluation.outcome == .baselineChanged) #expect(evaluation.state.remaining == 100) #expect(evaluation.state.source == .copilotSecondaryFallback) } @Test func `missing owner fails closed and clears prior state`() throws { let owner = try self.owner("missing-owner") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) store.handleSessionQuotaTransition( provider: .codex, snapshot: self.snapshot(used: 100, resetBoundary: boundary, updatedAt: self.start.addingTimeInterval(60)), codexOwnerKey: nil, now: self.start.addingTimeInterval(60)) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) #expect(notifier.transitions.isEmpty) self.observe( store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) } } extension CodexSessionQuotaFalseRestoreTests { @Test func `missing owner keeps stale observations behind the fresh baseline barrier`() throws { let owner = try self.owner("missing-owner-watermark") let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) let invalidatedAt = self.start.addingTimeInterval(120) self.observe(store, used: 20, boundary: nil, at: self.start, owner: owner) store.handleSessionQuotaTransition( provider: .codex, snapshot: self.snapshot(used: 100, resetBoundary: nil, updatedAt: invalidatedAt), codexOwnerKey: nil, now: invalidatedAt) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 20, boundary: nil, at: self.start.addingTimeInterval(90), owner: owner) self.observe(store, used: 10, boundary: nil, at: self.start.addingTimeInterval(100), owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequirement?.observedAtWatermark == invalidatedAt) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(121), owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) } @Test func `windowless Codex result advances a matching depleted baseline watermark`() throws { let owner = try self.owner("windowless-partial") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) store.handleSessionQuotaTransition( provider: .codex, snapshot: UsageSnapshot( primary: nil, secondary: nil, updatedAt: self.start.addingTimeInterval(120), identity: ProviderIdentitySnapshot( providerID: .codex, accountEmail: "session-fixture@example.test", accountOrganization: nil, loginMethod: "test")), codexOwnerKey: owner, now: self.start.addingTimeInterval(120)) self.observe(store, used: 20, boundary: boundary, at: self.start.addingTimeInterval(90), owner: owner) self.observe(store, used: 10, boundary: boundary, at: self.start.addingTimeInterval(100), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(store.sessionQuotaTransitionStates[.codex]?.observedAt == self.start.addingTimeInterval(120)) #expect(store.sessionQuotaTransitionStates[.codex]?.trustedResetBoundary == boundary) #expect(notifier.transitions == [.depleted]) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted]) } @Test func `windowless Codex result blocks stale boundaryless restore confirmation`() throws { let owner = try self.owner("windowless-boundaryless") let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: nil, at: self.start, owner: owner) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: owner) store.handleSessionQuotaTransition( provider: .codex, snapshot: UsageSnapshot( primary: nil, secondary: nil, updatedAt: self.start.addingTimeInterval(120), identity: ProviderIdentitySnapshot( providerID: .codex, accountEmail: "session-fixture@example.test", accountOrganization: nil, loginMethod: "test")), codexOwnerKey: owner, now: self.start.addingTimeInterval(120)) self.observe(store, used: 20, boundary: nil, at: self.start.addingTimeInterval(90), owner: owner) self.observe(store, used: 10, boundary: nil, at: self.start.addingTimeInterval(100), owner: owner) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(store.sessionQuotaTransitionStates[.codex]?.observedAt == self.start.addingTimeInterval(120)) #expect(store.sessionQuotaTransitionStates[.codex]?.pendingCodexRestoreObservationAt == nil) #expect(notifier.transitions == [.depleted]) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(180), owner: owner) #expect(notifier.transitions == [.depleted]) } @Test func `disabled provider cleanup does not refire Codex depletion`() throws { let owner = try self.owner("disabled-provider-cleanup") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) store.clearDisabledProviderState(enabledProviders: []) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) } @Test func `unavailable provider cleanup does not refire Codex depletion`() throws { let owner = try self.owner("unavailable-provider-cleanup") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: owner) store.clearUnavailableProviderState( displayEnabledProviders: [.codex], availableProviders: []) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) } @Test func `cleanup after a positive Codex baseline still reports depletion on recovery`() throws { let owner = try self.owner("positive-provider-cleanup") let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) store.clearDisabledProviderState(enabledProviders: []) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(!store.codexSessionQuotaBaselineRequired) self.observe(store, used: 100, boundary: boundary, at: self.start.addingTimeInterval(120), owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) } @Test func `cleanup without a prior Codex baseline keeps startup depletion semantics`() throws { let owner = try self.owner("startup-cleanup") let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) store.clearDisabledProviderState(enabledProviders: []) #expect(!store.codexSessionQuotaBaselineRequired) self.observe(store, used: 100, boundary: nil, at: self.start, owner: owner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) } @Test func `notifications disabled keep stale observations behind the fresh baseline barrier`() throws { let owner = try self.owner("disabled-watermark") let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) let invalidatedAt = self.start.addingTimeInterval(120) self.observe(store, used: 20, boundary: nil, at: self.start, owner: owner) store.settings.sessionQuotaNotificationsEnabled = false self.observe(store, used: 100, boundary: nil, at: invalidatedAt, owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) #expect(store.codexSessionQuotaBaselineRequirement?.observedAtWatermark == invalidatedAt) store.settings.sessionQuotaNotificationsEnabled = true self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: owner) self.observe(store, used: 20, boundary: nil, at: self.start.addingTimeInterval(90), owner: owner) self.observe(store, used: 10, boundary: nil, at: self.start.addingTimeInterval(100), owner: owner) self.observe(store, used: 20, boundary: nil, at: invalidatedAt, owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequirement?.observedAtWatermark == invalidatedAt) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(121), owner: owner) #expect(notifier.transitions.isEmpty) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) self.observe(store, used: 20, boundary: nil, at: self.start.addingTimeInterval(122), owner: owner) self.observe(store, used: 10, boundary: nil, at: self.start.addingTimeInterval(123), owner: owner) #expect(notifier.transitions == [.restored]) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(124), owner: owner) #expect(notifier.transitions == [.restored, .depleted]) } @Test func `non Codex providers preserve immediate restore semantics`() { let boundary = self.start.addingTimeInterval(5 * 3600) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, provider: .claude, used: 20, boundary: boundary, at: self.start, owner: nil) self.observe( store, provider: .claude, used: 100, boundary: boundary, at: self.start.addingTimeInterval(60), owner: nil) self.observe( store, provider: .claude, used: 0, boundary: boundary, at: self.start.addingTimeInterval(120), owner: nil) #expect(notifier.transitions == [.depleted, .restored]) } @Test func `non Codex providers preserve disabled baseline tracking`() { let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) store.settings.sessionQuotaNotificationsEnabled = false self.observe(store, provider: .claude, used: 20, boundary: nil, at: self.start, owner: nil) self.observe( store, provider: .claude, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: nil) #expect(notifier.transitions.isEmpty) store.settings.sessionQuotaNotificationsEnabled = true self.observe( store, provider: .claude, used: 20, boundary: nil, at: self.start.addingTimeInterval(120), owner: nil) #expect(notifier.transitions == [.restored]) } @Test func `selected Codex account caller forwards its stable owner`() async throws { let expectedOwner = try self.owner("selected-caller") let limitResetOwner = try #require(CodexLimitResetOwnerKey( identity: .providerAccount(id: "workspace-fixture-selected-caller"), accountEmail: "session-fixture@example.test")) let now = self.start let snapshot = self.snapshot( used: 20, resetBoundary: now.addingTimeInterval(5 * 3600), updatedAt: now) let account = CodexVisibleAccount( id: "live:selected-caller", email: "session-fixture@example.test", workspaceAccountID: "workspace-fixture-selected-caller", storedAccountID: nil, selectionSource: .liveSystem, isActive: true, isLive: true, canReauthenticate: false, canRemove: false) let result = ProviderFetchResult( usage: snapshot, credits: nil, dashboard: nil, sourceLabel: "fixture", strategyID: "fixture.oauth", strategyKind: .oauth) let store = Self.makeStore(notifier: SessionQuotaNotifierSpy()) await store.applySelectedCodexVisibleAccountOutcome( ProviderFetchOutcome(result: .success(result), attempts: []), account: account, snapshot: snapshot, sourceLabel: "fixture", limitResetOwnerKey: limitResetOwner) #expect(store.sessionQuotaTransitionStates[.codex]?.codexOwnerKey == expectedOwner) } @Test func `selected email only Codex account keeps session notifications`() async { let email = "email-only-session@example.test" let account = CodexVisibleAccount( id: "live:email-only-session", email: email, workspaceAccountID: nil, authFingerprint: "fixture-auth-fingerprint", storedAccountID: nil, selectionSource: .liveSystem, isActive: true, isLive: true, canReauthenticate: false, canRemove: false) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) let boundary = self.start.addingTimeInterval(5 * 3600) let advancedBoundary = boundary.addingTimeInterval(5 * 3600) for (used, observedAt, resetBoundary) in [ (20.0, self.start, boundary), (100.0, self.start.addingTimeInterval(60), boundary), (20.0, boundary.addingTimeInterval(60), advancedBoundary), (10.0, boundary.addingTimeInterval(120), advancedBoundary), ] { let snapshot = self.snapshot( used: used, resetBoundary: resetBoundary, updatedAt: observedAt, email: email) let result = ProviderFetchResult( usage: snapshot, credits: nil, dashboard: nil, sourceLabel: "fixture", strategyID: "fixture.oauth", strategyKind: .oauth) await store.applySelectedCodexVisibleAccountOutcome( ProviderFetchOutcome(result: .success(result), attempts: []), account: account, snapshot: snapshot, sourceLabel: "fixture", limitResetOwnerKey: nil) } #expect(store.sessionQuotaTransitionStates[.codex]?.codexOwnerKey != nil) #expect(notifier.transitions == [.depleted, .restored]) } @Test func `email only notification owners isolate source and credential rotation`() throws { let email = "email-only-owner@example.test" let identity = CodexIdentity.emailOnly(normalizedEmail: email) let liveA = try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: "fixture-a"))) let liveB = try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: "fixture-b"))) let profile = try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .profileHome(path: "/tmp/codex-email-only-owner"), identity: identity, accountKey: email, authFingerprint: "fixture-a"))) let providerIdentity = CodexIdentity.providerAccount(id: "workspace-email-only-owner") let providerA = try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .liveSystem, identity: providerIdentity, accountKey: email, authFingerprint: "fixture-a"))) let providerB = try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .profileHome(path: "/tmp/codex-email-only-owner"), identity: providerIdentity, accountKey: email, authFingerprint: "fixture-b"))) #expect(liveA != liveB) #expect(liveA != profile) #expect(providerA == providerB) #expect(CodexLimitResetOwnerKey(identity: identity, accountEmail: email) == nil) #expect(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: nil)) == nil) } @Test func `email only credential rotation establishes a new baseline`() throws { let email = "rotating-email-only-owner@example.test" let identity = CodexIdentity.emailOnly(normalizedEmail: email) let oldGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: "fixture-old") let newGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: "fixture-new") let oldOwner = try #require(UsageStore.codexSessionQuotaOwnerKey(for: oldGuard)) let newOwner = try #require(UsageStore.codexSessionQuotaOwnerKey(for: newGuard)) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) self.observe(store, used: 20, boundary: nil, at: self.start, owner: oldOwner) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: oldOwner) store.snapshots[.codex] = self.snapshot( used: 100, resetBoundary: nil, updatedAt: self.start.addingTimeInterval(60), email: email) store.lastCodexUsagePublicationGuard = oldGuard store.lastCodexAccountScopedRefreshGuard = oldGuard store.reconcileCodexAccountStateForUsageOwner(newGuard) #expect(store.snapshots[.codex] == nil) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) self.observe( store, used: 100, boundary: nil, at: self.start.addingTimeInterval(120), owner: newOwner) #expect(notifier.transitions == [.depleted]) #expect(store.sessionQuotaTransitionStates[.codex]?.codexOwnerKey == newOwner) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) #expect(!store.codexSessionQuotaBaselineRequired) self.observe( store, used: 20, boundary: nil, at: self.start.addingTimeInterval(180), owner: newOwner) self.observe( store, used: 10, boundary: nil, at: self.start.addingTimeInterval(240), owner: newOwner) #expect(notifier.transitions == [.depleted, .restored]) } @Test func `provider owner survives source and credential changes`() throws { let email = "provider-source-owner@example.test" let identity = CodexIdentity.providerAccount(id: "workspace-provider-source-owner") let oldGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email, authFingerprint: "fixture-old") let newGuard = CodexAccountScopedRefreshGuard( source: .profileHome(path: "/tmp/codex-provider-source-owner"), identity: identity, accountKey: email, authFingerprint: "fixture-new") let oldOwner = try #require(UsageStore.codexSessionQuotaOwnerKey(for: oldGuard)) let newOwner = try #require(UsageStore.codexSessionQuotaOwnerKey(for: newGuard)) let notifier = SessionQuotaNotifierSpy() let store = Self.makeStore(notifier: notifier) #expect(oldOwner == newOwner) self.observe(store, used: 20, boundary: nil, at: self.start, owner: oldOwner) self.observe(store, used: 100, boundary: nil, at: self.start.addingTimeInterval(60), owner: oldOwner) store.snapshots[.codex] = self.snapshot( used: 100, resetBoundary: nil, updatedAt: self.start.addingTimeInterval(60), email: email) store.lastCodexUsagePublicationGuard = oldGuard store.lastCodexAccountScopedRefreshGuard = oldGuard store.reconcileCodexAccountStateForUsageOwner(newGuard) #expect(store.snapshots[.codex] == nil) #expect(store.sessionQuotaTransitionStates[.codex]?.remaining == 0) self.observe( store, used: 20, boundary: nil, at: self.start.addingTimeInterval(120), owner: newOwner) self.observe( store, used: 10, boundary: nil, at: self.start.addingTimeInterval(180), owner: newOwner) #expect(notifier.transitions == [.depleted, .restored]) _ = store.prepareCodexAccountScopedRefreshIfNeeded( forceInvalidation: true, currentGuardOverride: newGuard) #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) } @Test func `regular refresh owner builder supports email only identity`() throws { let email = "regular-email-only-owner@example.test" let refreshGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: .emailOnly(normalizedEmail: email), accountKey: email, authFingerprint: "fixture-regular") let owner = try #require(UsageStore.codexSessionQuotaOwnerKey(for: refreshGuard)) #expect(!owner.rawValue.isEmpty) } @Test func `clearing published Codex usage clears typed transition state`() throws { let owner = try self.owner("cleanup") let boundary = self.start.addingTimeInterval(5 * 3600) let store = Self.makeStore(notifier: SessionQuotaNotifierSpy()) self.observe(store, used: 20, boundary: boundary, at: self.start, owner: owner) #expect(store.sessionQuotaTransitionStates[.codex] != nil) store.clearCodexPublishedUsageState() #expect(store.sessionQuotaTransitionStates[.codex] == nil) #expect(store.codexSessionQuotaBaselineRequired) } private func observe( _ store: UsageStore, provider: UsageProvider = .codex, used: Double, boundary: Date?, at: Date, evaluatedAt: Date? = nil, owner: CodexSessionQuotaOwnerKey?) { store.handleSessionQuotaTransition( provider: provider, snapshot: self.snapshot( provider: provider, used: used, resetBoundary: boundary, updatedAt: at), codexOwnerKey: owner, now: evaluatedAt ?? at) } private func snapshot( provider: UsageProvider = .codex, used: Double, resetBoundary: Date?, updatedAt: Date, email: String = "session-fixture@example.test") -> UsageSnapshot { UsageSnapshot( primary: RateWindow( usedPercent: used, windowMinutes: 300, resetsAt: resetBoundary, resetDescription: nil), secondary: nil, updatedAt: updatedAt, identity: ProviderIdentitySnapshot( providerID: provider, accountEmail: email, accountOrganization: nil, loginMethod: "test")) } private func owner(_ suffix: String) throws -> CodexSessionQuotaOwnerKey { try #require(CodexSessionQuotaOwnerKey(refreshGuard: CodexAccountScopedRefreshGuard( source: .liveSystem, identity: .providerAccount(id: "workspace-fixture-\(suffix)"), accountKey: "session-fixture@example.test"))) } private static func makeStore(notifier: SessionQuotaNotifierSpy) -> UsageStore { let suiteName = "CodexSessionQuotaFalseRestoreTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) let settings = SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suiteName), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.refreshFrequency = .manual settings.statusChecksEnabled = false settings.sessionQuotaNotificationsEnabled = true return UsageStore( fetcher: UsageFetcher(), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings, sessionQuotaNotifier: notifier) } } @MainActor private final class SessionQuotaNotifierSpy: SessionQuotaNotifying { private(set) var transitions: [SessionQuotaTransition] = [] func post(transition: SessionQuotaTransition, provider _: UsageProvider, badge _: NSNumber?) { self.transitions.append(transition) } func postQuotaWarning( event _: QuotaWarningEvent, provider _: UsageProvider, soundEnabled _: Bool, onScreenAlertEnabled _: Bool) {} }