487 lines
19 KiB
Swift
487 lines
19 KiB
Swift
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)
|
|
}
|
|
}
|