Skip to content

Commit 52d09da

Browse files
committed
fix: guard Codex reset cache by auth fingerprint
1 parent 870e639 commit 52d09da

4 files changed

Lines changed: 149 additions & 4 deletions

File tree

Sources/CodexBar/CodexAccountUsageSnapshotStore.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ struct FileCodexAccountUsageSnapshotStore: CodexAccountUsageSnapshotStoring, @un
4747
return self.workspaceAccountID == currentWorkspaceAccountID
4848
}
4949

50-
if self.storedAccountID != nil || account.storedAccountID != nil {
51-
return self.storedAccountID == account.storedAccountID
52-
}
53-
5450
let currentAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
5551
if self.authFingerprint != nil || currentAuthFingerprint != nil {
5652
return self.authFingerprint == currentAuthFingerprint
5753
}
5854

55+
if self.storedAccountID != nil || account.storedAccountID != nil {
56+
return self.storedAccountID == account.storedAccountID
57+
}
58+
5959
guard let selectionSource else { return true }
6060
return selectionSource == account.selectionSource
6161
}

Sources/CodexBar/UsageStore+TokenAccounts.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,12 @@ extension UsageStore {
555555
return priorWorkspaceID == accountWorkspaceID
556556
}
557557

558+
let priorAuthFingerprint = CodexAuthFingerprint.normalize(prior.authFingerprint)
559+
let accountAuthFingerprint = CodexAuthFingerprint.normalize(account.authFingerprint)
560+
if priorAuthFingerprint != nil || accountAuthFingerprint != nil {
561+
guard priorAuthFingerprint == accountAuthFingerprint else { return false }
562+
}
563+
558564
if prior.selectionSource == account.selectionSource {
559565
switch account.selectionSource {
560566
case .managedAccount:

Tests/CodexBarTests/CodexAccountVisibleHistoryBackfillTests.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,4 +832,103 @@ extension CodexAccountScopedRefreshTests {
832832
#expect(liveSnapshot.primary?.windowMinutes == 300)
833833
#expect(liveSnapshot.primary?.resetsAt == priorReset)
834834
}
835+
836+
@Test
837+
func `ignores live codex prior snapshot after auth fingerprint changes`() async throws {
838+
let settings = self.makeSettingsStore(
839+
suite: "CodexAccountVisibleHistoryBackfillTests-live-prior-auth-change")
840+
settings.refreshFrequency = .manual
841+
settings.multiAccountMenuLayout = .stacked
842+
843+
let managedID = try #require(UUID(uuidString: "DDDDDDDD-EEEE-FFFF-AAAA-222222222222"))
844+
let liveHome = FileManager.default.temporaryDirectory
845+
.appendingPathComponent("codex-visible-live-prior-auth-\(UUID().uuidString)", isDirectory: true)
846+
let managedHome = FileManager.default.temporaryDirectory
847+
.appendingPathComponent("codex-visible-live-prior-auth-managed-\(UUID().uuidString)", isDirectory: true)
848+
try FileManager.default.createDirectory(at: liveHome, withIntermediateDirectories: true)
849+
try FileManager.default.createDirectory(at: managedHome, withIntermediateDirectories: true)
850+
settings._test_liveSystemCodexAccount = ObservedSystemCodexAccount(
851+
email: "live-prior-auth@example.com",
852+
authFingerprint: "current-live-auth-fingerprint",
853+
codexHomePath: liveHome.path,
854+
observedAt: Date(),
855+
identity: .emailOnly(normalizedEmail: "live-prior-auth@example.com"))
856+
let managedAccount = ManagedCodexAccount(
857+
id: managedID,
858+
email: "managed-prior-auth@example.com",
859+
providerAccountID: "acct-managed-prior-auth",
860+
workspaceLabel: "Managed Team",
861+
workspaceAccountID: "acct-managed-prior-auth",
862+
managedHomePath: managedHome.path,
863+
createdAt: 1,
864+
updatedAt: 2,
865+
lastAuthenticatedAt: 2)
866+
let storeURL = try self.makeManagedAccountStoreURL(accounts: [managedAccount])
867+
defer {
868+
settings._test_managedCodexAccountStoreURL = nil
869+
settings._test_liveSystemCodexAccount = nil
870+
try? FileManager.default.removeItem(at: storeURL)
871+
try? FileManager.default.removeItem(at: liveHome)
872+
try? FileManager.default.removeItem(at: managedHome)
873+
}
874+
settings._test_managedCodexAccountStoreURL = storeURL
875+
settings.codexActiveSource = .managedAccount(id: managedID)
876+
877+
let now = Date()
878+
let priorReset = now.addingTimeInterval(2 * 60 * 60)
879+
let liveAccount = try #require(settings.codexVisibleAccountProjection.visibleAccounts.first {
880+
$0.selectionSource == .liveSystem
881+
})
882+
let priorLiveAccount = CodexVisibleAccount(
883+
id: liveAccount.id,
884+
email: liveAccount.email,
885+
workspaceLabel: liveAccount.workspaceLabel,
886+
workspaceAccountID: liveAccount.workspaceAccountID,
887+
authFingerprint: "stale-live-auth-fingerprint",
888+
storedAccountID: liveAccount.storedAccountID,
889+
selectionSource: liveAccount.selectionSource,
890+
isActive: liveAccount.isActive,
891+
isLive: liveAccount.isLive,
892+
canReauthenticate: liveAccount.canReauthenticate,
893+
canRemove: liveAccount.canRemove)
894+
let snapshotStore = RecordingCodexAccountUsageSnapshotStore(initialSnapshots: [
895+
CodexAccountUsageSnapshot(
896+
account: priorLiveAccount,
897+
snapshot: UsageSnapshot(
898+
primary: RateWindow(
899+
usedPercent: 18,
900+
windowMinutes: 300,
901+
resetsAt: priorReset,
902+
resetDescription: nil),
903+
secondary: nil,
904+
updatedAt: now.addingTimeInterval(-60)),
905+
error: nil,
906+
sourceLabel: "cached"),
907+
])
908+
let store = UsageStore(
909+
fetcher: UsageFetcher(environment: [:]),
910+
browserDetection: BrowserDetection(cacheTTL: 0),
911+
settings: settings,
912+
codexAccountUsageSnapshotStore: snapshotStore,
913+
startupBehavior: .testing)
914+
self.installContextualCodexProvider(on: store) { _ in
915+
UsageSnapshot(
916+
primary: RateWindow(
917+
usedPercent: 9,
918+
windowMinutes: 0,
919+
resetsAt: nil,
920+
resetDescription: nil),
921+
secondary: nil,
922+
updatedAt: now)
923+
}
924+
925+
await store.refreshCodexVisibleAccountsForMenu()
926+
927+
let liveSnapshot = try #require(store.codexAccountSnapshots.first {
928+
$0.account.selectionSource == .liveSystem
929+
}?.snapshot)
930+
#expect(liveSnapshot.primary?.usedPercent == 9)
931+
#expect(liveSnapshot.primary?.windowMinutes == 0)
932+
#expect(liveSnapshot.primary?.resetsAt == nil)
933+
}
835934
}

Tests/CodexBarTests/StatusMenuCodexSwitcherPresentationTests.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,46 @@ struct StatusMenuCodexSwitcherPresentationTests {
320320
#expect(hydrated.isEmpty)
321321
}
322322

323+
@Test
324+
func `codex account snapshot store rejects same stored account after auth fingerprint changes`() {
325+
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
326+
defer { try? FileManager.default.removeItem(at: fileURL) }
327+
328+
let accountID = UUID()
329+
let oldAccount = CodexVisibleAccount(
330+
id: "reauth@example.com",
331+
email: "reauth@example.com",
332+
authFingerprint: "old-auth-fingerprint",
333+
storedAccountID: accountID,
334+
selectionSource: .managedAccount(id: accountID),
335+
isActive: false,
336+
isLive: false,
337+
canReauthenticate: true,
338+
canRemove: true)
339+
let newAccount = CodexVisibleAccount(
340+
id: "reauth@example.com",
341+
email: "reauth@example.com",
342+
authFingerprint: "new-auth-fingerprint",
343+
storedAccountID: accountID,
344+
selectionSource: .managedAccount(id: accountID),
345+
isActive: true,
346+
isLive: false,
347+
canReauthenticate: true,
348+
canRemove: true)
349+
let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL)
350+
store.store([
351+
CodexAccountUsageSnapshot(
352+
account: oldAccount,
353+
snapshot: self.snapshot(email: oldAccount.email, percent: 71),
354+
error: nil,
355+
sourceLabel: "test"),
356+
])
357+
358+
let hydrated = store.load(for: [newAccount])
359+
360+
#expect(hydrated.isEmpty)
361+
}
362+
323363
@Test
324364
func `codex account snapshot store rejects legacy workspace records without identity`() throws {
325365
let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)

0 commit comments

Comments
 (0)