import AppKit import CodexBarCore import Foundation import Testing @testable import CodexBar @Suite(.serialized) @MainActor struct StatusMenuCodexSwitcherPresentationTests { private func disableMenuCardsForTesting() { StatusItemController.menuCardRenderingEnabled = false StatusItemController.setMenuRefreshEnabledForTesting(false) } private func makeSettings() -> SettingsStore { let suite = "StatusMenuCodexSwitcherPresentationTests-\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defaults.removePersistentDomain(forName: suite) return SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suite), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) } private func makeManagedAccountStoreURL(accounts: [ManagedCodexAccount]) throws -> URL { let storeURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let store = FileManagedCodexAccountStore(fileURL: storeURL) try store.storeAccounts(ManagedCodexAccountSet( version: FileManagedCodexAccountStore.currentVersion, accounts: accounts)) return storeURL } private func enableOnlyCodex(_ settings: SettingsStore) { let registry = ProviderRegistry.shared for provider in UsageProvider.allCases { guard let metadata = registry.metadata[provider] else { continue } settings.setProviderEnabled(provider: provider, metadata: metadata, enabled: provider == .codex) } } private func representedIDs(in menu: NSMenu) -> [String] { menu.items.compactMap { $0.representedObject as? String } } private func snapshot(email: String, percent: Double = 12) -> UsageSnapshot { UsageSnapshot( primary: RateWindow( usedPercent: percent, windowMinutes: 300, resetsAt: Date().addingTimeInterval(300), resetDescription: nil), secondary: RateWindow( usedPercent: percent, windowMinutes: 10080, resetsAt: Date().addingTimeInterval(86400), resetDescription: nil), updatedAt: Date(), identity: ProviderIdentitySnapshot( providerID: .codex, accountEmail: email, accountOrganization: nil, loginMethod: "Plus")) } private func removeAccountIdentity(fromSnapshotStoreAt fileURL: URL) throws { var payload = try #require(JSONSerialization.jsonObject(with: Data(contentsOf: fileURL)) as? [String: Any]) var records = try #require(payload["records"] as? [[String: Any]]) records[0].removeValue(forKey: "accountIdentity") payload["records"] = records try JSONSerialization.data(withJSONObject: payload).write(to: fileURL) } @Test func `codex account ordering keeps workspace groups contiguous`() { let teamActive = CodexVisibleAccount( id: "team-a-active", email: "active@example.com", workspaceLabel: "Team A", workspaceAccountID: "team-a", storedAccountID: UUID(), selectionSource: .managedAccount(id: UUID()), isActive: true, isLive: false, canReauthenticate: true, canRemove: true) let teamHighQuota = CodexVisibleAccount( id: "team-b-high-quota", email: "high@example.com", workspaceLabel: "Team B", workspaceAccountID: "team-b", storedAccountID: UUID(), selectionSource: .managedAccount(id: UUID()), isActive: false, isLive: false, canReauthenticate: true, canRemove: true) let teamSibling = CodexVisibleAccount( id: "team-a-sibling", email: "sibling@example.com", workspaceLabel: "Team A", workspaceAccountID: "team-a", storedAccountID: UUID(), selectionSource: .managedAccount(id: UUID()), isActive: false, isLive: false, canReauthenticate: true, canRemove: true) let accounts = [teamActive, teamHighQuota, teamSibling] let snapshots = [ CodexAccountUsageSnapshot( account: teamActive, snapshot: self.snapshot(email: teamActive.email, percent: 95), error: nil, sourceLabel: "test"), CodexAccountUsageSnapshot( account: teamHighQuota, snapshot: self.snapshot(email: teamHighQuota.email, percent: 10), error: nil, sourceLabel: "test"), CodexAccountUsageSnapshot( account: teamSibling, snapshot: self.snapshot(email: teamSibling.email, percent: 20), error: nil, sourceLabel: "test"), ] let ordered = CodexAccountPresentationOrdering.orderedAccounts( accounts, snapshots: snapshots, activeVisibleAccountID: teamActive.id) #expect(ordered.map(\.id) == ["team-a-active", "team-a-sibling", "team-b-high-quota"]) #expect(ordered.codexWorkspaceSections().map(\.title) == ["Team A", "Team B"]) #expect(ordered.codexWorkspaceSections().first?.accounts.map(\.id) == ["team-a-active", "team-a-sibling"]) } @Test func `codex stacked menu orders by quota and groups workspaces`() throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false settings.multiAccountMenuLayout = .stacked self.enableOnlyCodex(settings) let lowID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-111111111111")) let highID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-222222222222")) let low = ManagedCodexAccount( id: lowID, email: "low@example.com", workspaceLabel: "Team Low", workspaceAccountID: "team-low", managedHomePath: "/tmp/low-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 2) let high = ManagedCodexAccount( id: highID, email: "high@example.com", workspaceLabel: "Team High", workspaceAccountID: "team-high", managedHomePath: "/tmp/high-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 2) let storeURL = try self.makeManagedAccountStoreURL(accounts: [low, high]) defer { settings._test_managedCodexAccountStoreURL = nil settings._test_liveSystemCodexAccount = nil try? FileManager.default.removeItem(at: storeURL) } settings._test_managedCodexAccountStoreURL = storeURL settings._test_liveSystemCodexAccount = ObservedSystemCodexAccount( email: "active@example.com", workspaceLabel: "Personal", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings.codexActiveSource = .liveSystem let fetcher = UsageFetcher() let store = UsageStore(fetcher: fetcher, browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) store.codexAccountSnapshots = settings.codexVisibleAccountProjection.visibleAccounts.map { account in let usedPercent = switch account.email { case "high@example.com": 10.0 case "low@example.com": 80.0 default: 95.0 } return CodexAccountUsageSnapshot( account: account, snapshot: self.snapshot(email: account.email, percent: usedPercent), error: nil, sourceLabel: "test") } let controller = StatusItemController( store: store, settings: settings, account: fetcher.loadAccountInfo(), updater: DisabledUpdaterController(), preferencesSelection: PreferencesSelection(), statusBar: .system) defer { controller.releaseStatusItemsForTesting() } let display = try #require(controller.codexAccountMenuDisplay(for: .codex)) #expect(display.accounts.map(\.email) == ["active@example.com", "high@example.com", "low@example.com"]) #expect(display.workspaceSections.map(\.title) == ["Personal", "Team High", "Team Low"]) let menu = controller.makeMenu(for: .codex) controller.menuWillOpen(menu) #expect(self.representedIDs(in: menu).count(where: { $0.hasPrefix("codexWorkspace-") }) == 3) } @Test func `codex stacked menu surfaces account health labels`() throws { self.disableMenuCardsForTesting() let settings = self.makeSettings() settings.statusChecksEnabled = false settings.refreshFrequency = .manual settings.mergeIcons = false settings.multiAccountMenuLayout = .stacked self.enableOnlyCodex(settings) 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 = ObservedSystemCodexAccount( email: "live@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings.codexActiveSource = .liveSystem let visibleAccount = try #require(settings.codexVisibleAccountProjection.visibleAccounts .first { $0.email == "managed@example.com" }) #expect(CodexAccountHealth.status(for: visibleAccount, error: "401 Unauthorized") .label == "Needs re-auth") } @Test func `codex account snapshot store hydrates current visible accounts`() { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let account = CodexVisibleAccount( id: "active@example.com", email: "active@example.com", workspaceAccountID: "acct-active", storedAccountID: nil, selectionSource: .liveSystem, isActive: true, isLive: true, canReauthenticate: true, canRemove: false) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) store.store([ CodexAccountUsageSnapshot( account: account, snapshot: self.snapshot(email: account.email, percent: 17), error: nil, sourceLabel: "test"), ]) let hydrated = store.load(for: [account]) #expect(hydrated.map(\.id) == [account.id]) #expect(hydrated.first?.snapshot?.primary?.usedPercent == 17) #expect(hydrated.first?.account.email == account.email) } @Test func `codex account snapshot store rejects mismatched workspace records`() { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let oldAccountID = UUID() let newAccountID = UUID() let oldAccount = CodexVisibleAccount( id: "workspace@example.com", email: "workspace@example.com", workspaceLabel: "Old Team", workspaceAccountID: "acct-old", storedAccountID: oldAccountID, selectionSource: .managedAccount(id: oldAccountID), isActive: false, isLive: false, canReauthenticate: true, canRemove: true) let newAccount = CodexVisibleAccount( id: "workspace@example.com", email: "workspace@example.com", workspaceLabel: "New Team", workspaceAccountID: "acct-new", storedAccountID: newAccountID, selectionSource: .managedAccount(id: newAccountID), isActive: true, isLive: false, canReauthenticate: true, canRemove: true) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) store.store([ CodexAccountUsageSnapshot( account: oldAccount, snapshot: self.snapshot(email: oldAccount.email, percent: 71), error: nil, sourceLabel: "test"), ]) let hydrated = store.load(for: [newAccount]) #expect(hydrated.isEmpty) } @Test func `codex account snapshot store keeps same composite owner after auth fingerprint changes`() { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let accountID = UUID() let oldAccount = CodexVisibleAccount( id: "reauth@example.com", email: "reauth@example.com", workspaceAccountID: "acct-reauth", authFingerprint: "old-auth-fingerprint", storedAccountID: accountID, selectionSource: .managedAccount(id: accountID), isActive: false, isLive: false, canReauthenticate: true, canRemove: true) let newAccount = CodexVisibleAccount( id: "reauth@example.com", email: "reauth@example.com", workspaceAccountID: "acct-reauth", authFingerprint: "new-auth-fingerprint", storedAccountID: accountID, selectionSource: .managedAccount(id: accountID), isActive: true, isLive: false, canReauthenticate: true, canRemove: true) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) store.store([ CodexAccountUsageSnapshot( account: oldAccount, snapshot: self.snapshot(email: oldAccount.email, percent: 71), error: nil, sourceLabel: "test"), ]) let hydrated = store.load(for: [newAccount]) #expect(hydrated.map(\.id) == [newAccount.id]) #expect(hydrated.first?.snapshot?.primary?.usedPercent == 71) } @Test func `codex account snapshot store rejects legacy workspace records without identity`() throws { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let payload = """ { "records" : [ { "error" : "cached", "id" : "legacy@example.com", "snapshot" : null, "sourceLabel" : "legacy" } ], "version" : 1 } """ try Data(payload.utf8).write(to: fileURL) let accountID = UUID() let workspaceAccount = CodexVisibleAccount( id: "legacy@example.com", email: "legacy@example.com", workspaceLabel: "New Team", workspaceAccountID: "acct-new", storedAccountID: accountID, selectionSource: .managedAccount(id: accountID), isActive: true, isLive: false, canReauthenticate: true, canRemove: true) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) let hydrated = store.load(for: [workspaceAccount]) #expect(hydrated.isEmpty) } @Test func `codex account snapshot store rejects normalized legacy email ids without composite owner`() throws { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let payload = """ { "records" : [ { "error" : "cached", "id" : "Legacy@Example.com", "snapshot" : null, "sourceLabel" : "legacy" } ], "version" : 1 } """ try Data(payload.utf8).write(to: fileURL) let account = CodexVisibleAccount( id: "Legacy@Example.com", email: "legacy@example.com", storedAccountID: nil, selectionSource: .liveSystem, isActive: true, isLive: true, canReauthenticate: true, canRemove: false) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) #expect(store.load(for: [account]).isEmpty) } @Test func `codex account snapshot store rejects legacy stable ids crossing workspace members`() throws { let fileURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: fileURL) } let managedAccountID = UUID() let visibleID = "managed:\(managedAccountID.uuidString.lowercased())" let priorAccount = CodexVisibleAccount( id: visibleID, email: "first-member@example.com", workspaceAccountID: "shared-workspace", storedAccountID: managedAccountID, selectionSource: .managedAccount(id: managedAccountID), isActive: false, isLive: false, canReauthenticate: true, canRemove: true) let otherMember = CodexVisibleAccount( id: priorAccount.id, email: "second-member@example.com", workspaceAccountID: priorAccount.workspaceAccountID, storedAccountID: managedAccountID, selectionSource: .managedAccount(id: managedAccountID), isActive: true, isLive: false, canReauthenticate: true, canRemove: true) let store = FileCodexAccountUsageSnapshotStore(fileURL: fileURL) store.store([ CodexAccountUsageSnapshot( account: priorAccount, snapshot: self.snapshot(email: priorAccount.email, percent: 71), error: nil, sourceLabel: "legacy"), ]) try self.removeAccountIdentity(fromSnapshotStoreAt: fileURL) #expect(store.load(for: [otherMember]).isEmpty) } }