import CodexBarCore import Foundation import Testing @testable import CodexBar @Suite(.serialized) struct CodexAccountReconciliationTests { @MainActor private static func makeSettings(suite: String) throws -> SettingsStore { let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set(true, forKey: "providerDetectionCompleted") let settings = SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suite), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) settings.providerDetectionCompleted = true return settings } @Test @MainActor func `settings store exposes codex reconciliation accessors using managed and live overrides`() throws { let suite = "CodexAccountReconciliationTests-settings-store" let settings = try Self.makeSettings(suite: suite) let managed = ManagedCodexAccount( id: UUID(), email: "managed@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "system@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings._test_activeManagedCodexAccount = managed settings._test_liveSystemCodexAccount = live settings.codexActiveSource = .managedAccount(id: managed.id) defer { settings._test_activeManagedCodexAccount = nil settings._test_liveSystemCodexAccount = nil } let snapshot = settings.codexAccountReconciliationSnapshot let projection = settings.codexVisibleAccountProjection #expect(settings.codexActiveSource == .managedAccount(id: managed.id)) #expect(snapshot.storedAccounts.map(\.id) == [managed.id]) #expect(snapshot.storedAccounts.map(\.email) == [managed.email]) #expect(snapshot.activeStoredAccount?.id == managed.id) #expect(snapshot.activeStoredAccount?.email == managed.email) #expect(snapshot.liveSystemAccount?.email == live.email) #expect(snapshot.liveSystemAccount?.codexHomePath == live.codexHomePath) #expect(snapshot.liveSystemAccount?.observedAt == live.observedAt) #expect(snapshot.liveSystemAccount?.identity == .emailOnly(normalizedEmail: "system@example.com")) #expect(snapshot.matchingStoredAccountForLiveSystemAccount == nil) #expect(snapshot.activeSource == .managedAccount(id: managed.id)) #expect(snapshot.hasUnreadableAddedAccountStore == false) #expect(Set(projection.visibleAccounts.map(\.email)) == ["managed@example.com", "system@example.com"]) #expect(settings.codexVisibleAccounts == projection.visibleAccounts) #expect(projection.activeVisibleAccountID == "managed@example.com") #expect(projection.liveVisibleAccountID == "system@example.com") } @Test @MainActor func `settings store managed override does not leak ambient live system account`() throws { let suite = "CodexAccountReconciliationTests-managed-only" let settings = try Self.makeSettings(suite: suite) let managed = ManagedCodexAccount( id: UUID(), email: "managed@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) settings._test_activeManagedCodexAccount = managed settings.codexActiveSource = .managedAccount(id: managed.id) defer { settings._test_activeManagedCodexAccount = nil } let snapshot = settings.codexAccountReconciliationSnapshot let projection = settings.codexVisibleAccountProjection #expect(settings.codexActiveSource == .managedAccount(id: managed.id)) #expect(snapshot.liveSystemAccount == nil) #expect(snapshot.matchingStoredAccountForLiveSystemAccount == nil) #expect(snapshot.activeSource == .managedAccount(id: managed.id)) #expect(projection.visibleAccounts.map(\.email) == ["managed@example.com"]) #expect(projection.activeVisibleAccountID == "managed@example.com") #expect(projection.liveVisibleAccountID == nil) } @Test @MainActor func `settings store reconciliation environment override drives live observation with synthetic store`() throws { let suite = "CodexAccountReconciliationTests-environment-only" let settings = try Self.makeSettings(suite: suite) let ambientHome = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true) try Self.writeCodexAuthFile(homeURL: ambientHome, email: "ambient@example.com", plan: "pro") settings._test_codexReconciliationEnvironment = ["CODEX_HOME": ambientHome.path] defer { settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: ambientHome) } let snapshot = settings.codexAccountReconciliationSnapshot let projection = settings.codexVisibleAccountProjection #expect(settings.codexActiveSource == .liveSystem) #expect(snapshot.storedAccounts.isEmpty) #expect(snapshot.activeStoredAccount == nil) #expect(snapshot.liveSystemAccount?.email == "ambient@example.com") #expect(snapshot.liveSystemAccount?.codexHomePath == ambientHome.path) #expect(snapshot.matchingStoredAccountForLiveSystemAccount == nil) #expect(snapshot.activeSource == .liveSystem) #expect(projection.visibleAccounts.map(\.email) == ["ambient@example.com"]) #expect(projection.activeVisibleAccountID == "ambient@example.com") #expect(projection.liveVisibleAccountID == "ambient@example.com") } @Test @MainActor func `settings store can reuse short lived codex reconciliation snapshot`() throws { let suite = "CodexAccountReconciliationTests-short-lived-cache" let settings = try Self.makeSettings(suite: suite) let ambientHome = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true) try Self.writeCodexAuthFile(homeURL: ambientHome, email: "cached@example.com", plan: "pro") settings._test_codexReconciliationEnvironment = ["CODEX_HOME": ambientHome.path] SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = 60 defer { SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = nil settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: ambientHome) } let first = settings.codexAccountReconciliationSnapshot try FileManager.default.removeItem(at: ambientHome) let cached = settings.codexAccountReconciliationSnapshot settings.invalidateCodexAccountReconciliationSnapshotCache() let refreshed = settings.codexAccountReconciliationSnapshot #expect(first.liveSystemAccount?.email == "cached@example.com") #expect(cached.liveSystemAccount?.email == "cached@example.com") #expect(refreshed.liveSystemAccount == nil) } @Test @MainActor func `codex active source write invalidates short lived reconciliation snapshot`() throws { let suite = "CodexAccountReconciliationTests-active-source-cache-invalidation" let settings = try Self.makeSettings(suite: suite) let ambientHome = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true) try Self.writeCodexAuthFile(homeURL: ambientHome, email: "before@example.com", plan: "pro") settings._test_codexReconciliationEnvironment = ["CODEX_HOME": ambientHome.path] SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = 60 defer { SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = nil settings._test_codexReconciliationEnvironment = nil try? FileManager.default.removeItem(at: ambientHome) } #expect(settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email == "before@example.com") try Self.writeCodexAuthFile(homeURL: ambientHome, email: "after@example.com", plan: "pro") settings.codexActiveSource = .liveSystem #expect(settings.codexAccountReconciliationSnapshot.liveSystemAccount?.email == "after@example.com") } @Test @MainActor func `managed account changes invalidate short lived reconciliation snapshot`() throws { let suite = "CodexAccountReconciliationTests-managed-change-cache-invalidation" let settings = try Self.makeSettings(suite: suite) let storeURL = FileManager.default.temporaryDirectory .appendingPathComponent("codex-managed-store-\(UUID().uuidString).json") try Self.writeManagedCodexStore( ManagedCodexAccountSet(version: FileManagedCodexAccountStore.currentVersion, accounts: []), to: storeURL) let stored = ManagedCodexAccount( id: UUID(), email: "stored@example.com", managedHomePath: "/tmp/stored-managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) settings._test_managedCodexAccountStoreURL = storeURL settings.codexActiveSource = .managedAccount(id: stored.id) SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = 60 defer { SettingsStore.codexAccountReconciliationSnapshotCacheIntervalOverrideForTesting = nil settings._test_managedCodexAccountStoreURL = nil try? FileManager.default.removeItem(at: storeURL) } #expect(settings.codexAccountReconciliationSnapshot.storedAccounts.isEmpty) try Self.writeManagedCodexStore( ManagedCodexAccountSet(version: FileManagedCodexAccountStore.currentVersion, accounts: [stored]), to: storeURL) settings.refreshCodexAccountReconciliationAfterManagedAccountsDidChange() #expect(settings.codexAccountReconciliationSnapshot.storedAccounts.map(\.id) == [stored.id]) } @Test @MainActor func `settings store home path override also keeps reconciliation hermetic`() throws { let suite = "CodexAccountReconciliationTests-home-path-only" let settings = try Self.makeSettings(suite: suite) settings._test_activeManagedCodexRemoteHomePath = "/tmp/managed-route-home" settings._test_liveSystemCodexAccount = nil settings._test_codexReconciliationEnvironment = nil defer { settings._test_activeManagedCodexRemoteHomePath = nil settings._test_liveSystemCodexAccount = nil settings._test_codexReconciliationEnvironment = nil } let snapshot = settings.codexAccountReconciliationSnapshot let projection = settings.codexVisibleAccountProjection #expect(snapshot.storedAccounts.isEmpty) #expect(snapshot.activeStoredAccount == nil) #expect(snapshot.liveSystemAccount == nil) #expect(snapshot.matchingStoredAccountForLiveSystemAccount == nil) #expect(projection.visibleAccounts.isEmpty) #expect(projection.activeVisibleAccountID == nil) #expect(projection.liveVisibleAccountID == nil) } @Test @MainActor func `settings store home path override keeps active source hermetic without persisted source`() throws { let suite = "CodexAccountReconciliationTests-home-path-hermetic-source" let settings = try Self.makeSettings(suite: suite) let ambient = ManagedCodexAccount( id: UUID(), email: "ambient-managed@example.com", managedHomePath: "/tmp/ambient-managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let accounts = ManagedCodexAccountSet( version: FileManagedCodexAccountStore.currentVersion, accounts: [ambient]) let storeURL = FileManager.default.temporaryDirectory .appendingPathComponent("codex-managed-store-\(UUID().uuidString).json") try Self.writeManagedCodexStore(accounts, to: storeURL) settings._test_managedCodexAccountStoreURL = storeURL settings._test_activeManagedCodexRemoteHomePath = "/tmp/managed-route-home" defer { settings._test_managedCodexAccountStoreURL = nil settings._test_activeManagedCodexRemoteHomePath = nil try? FileManager.default.removeItem(at: storeURL) } let snapshot = settings.codexAccountReconciliationSnapshot #expect(settings.codexActiveSource == .liveSystem) #expect(settings.providerConfig(for: .codex)?.codexActiveSource == nil) #expect(snapshot.storedAccounts.map(\.id) == [ambient.id]) #expect(snapshot.storedAccounts.map(\.email) == [ambient.email]) #expect(snapshot.activeStoredAccount == nil) #expect(snapshot.activeSource == .liveSystem) } @Test @MainActor func `settings store normal reconciliation path honors persisted active source`() throws { let suite = "CodexAccountReconciliationTests-normal-path-active-source" let settings = try Self.makeSettings(suite: suite) let persistedSource = CodexActiveSource.managedAccount(id: UUID()) settings.codexActiveSource = persistedSource let snapshot = settings.codexAccountReconciliationSnapshot #expect(snapshot.activeSource == persistedSource) } @Test @MainActor func `settings store debug managed store U R L override loads on disk accounts`() throws { let suite = "CodexAccountReconciliationTests-debug-store-url" let settings = try Self.makeSettings(suite: suite) let stored = ManagedCodexAccount( id: UUID(), email: "stored@example.com", managedHomePath: "/tmp/stored-managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let accounts = ManagedCodexAccountSet( version: FileManagedCodexAccountStore.currentVersion, accounts: [stored]) let storeURL = FileManager.default.temporaryDirectory .appendingPathComponent("codex-managed-store-\(UUID().uuidString).json") try Self.writeManagedCodexStore(accounts, to: storeURL) settings._test_managedCodexAccountStoreURL = storeURL settings.codexActiveSource = .managedAccount(id: stored.id) defer { settings._test_managedCodexAccountStoreURL = nil try? FileManager.default.removeItem(at: storeURL) } let snapshot = settings.codexAccountReconciliationSnapshot #expect(snapshot.storedAccounts.map(\.id) == [stored.id]) #expect(snapshot.storedAccounts.map(\.email) == [stored.email]) #expect(snapshot.activeStoredAccount?.id == stored.id) #expect(snapshot.activeStoredAccount?.email == stored.email) #expect(snapshot.activeSource == .managedAccount(id: stored.id)) } @Test func `live only visible account is active when active source is live system`() { let live = ObservedSystemCodexAccount( email: "live@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let projection = CodexVisibleAccountProjection.make(from: CodexAccountReconciliationSnapshot( storedAccounts: [], activeStoredAccount: nil, liveSystemAccount: live, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .liveSystem, hasUnreadableAddedAccountStore: false)) #expect(projection.visibleAccounts.map(\.email) == ["live@example.com"]) #expect(projection.activeVisibleAccountID == "live@example.com") #expect(projection.liveVisibleAccountID == "live@example.com") } @Test func `workspace hydration changes snapshot equality and visible display state`() { let accountID = UUID() let baseAccount = ManagedCodexAccount( id: accountID, email: "user@example.com", providerAccountID: "account-live", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let hydratedAccount = ManagedCodexAccount( id: accountID, email: "user@example.com", providerAccountID: "account-live", workspaceLabel: "Team Alpha", workspaceAccountID: "account-live", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let baseSnapshot = CodexAccountReconciliationSnapshot( storedAccounts: [baseAccount], activeStoredAccount: baseAccount, liveSystemAccount: nil, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .managedAccount(id: accountID), hasUnreadableAddedAccountStore: false) let hydratedSnapshot = CodexAccountReconciliationSnapshot( storedAccounts: [hydratedAccount], activeStoredAccount: hydratedAccount, liveSystemAccount: nil, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .managedAccount(id: accountID), hasUnreadableAddedAccountStore: false) let baseProjection = CodexVisibleAccountProjection.make(from: baseSnapshot) let hydratedProjection = CodexVisibleAccountProjection.make(from: hydratedSnapshot) #expect(baseSnapshot != hydratedSnapshot) #expect(baseProjection.visibleAccounts.first?.displayName == "user@example.com") #expect(hydratedProjection.visibleAccounts.first?.displayName == "user@example.com — Team Alpha") } @Test func `matching live system account does not duplicate stored identity`() { let stored = ManagedCodexAccount( id: UUID(), email: "user@example.com", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let accounts = ManagedCodexAccountSet(version: 1, accounts: [stored]) let live = ObservedSystemCodexAccount( email: "USER@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let reconciler = DefaultCodexAccountReconciler( storeLoader: { accounts }, systemObserver: StubSystemObserver(account: live), activeSource: .managedAccount(id: stored.id), baseEnvironment: [:]) let projection = reconciler.loadVisibleAccounts() #expect(projection.visibleAccounts.count == 1) #expect(projection.activeVisibleAccountID == "user@example.com") #expect(projection.liveVisibleAccountID == "user@example.com") } @Test func `matching live system account prefers live workspace label and keeps stored fallback`() { let stored = ManagedCodexAccount( id: UUID(), email: "user@example.com", providerAccountID: "account-live", workspaceLabel: "Saved Team", workspaceAccountID: "account-live", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let liveWithLabel = ObservedSystemCodexAccount( email: "USER@example.com", workspaceLabel: "Live Team", workspaceAccountID: "account-live", codexHomePath: "/Users/test/.codex", observedAt: Date(), identity: .providerAccount(id: "account-live")) let labeledSnapshot = CodexAccountReconciliationSnapshot( storedAccounts: [stored], activeStoredAccount: stored, liveSystemAccount: liveWithLabel, matchingStoredAccountForLiveSystemAccount: stored, activeSource: .managedAccount(id: stored.id), hasUnreadableAddedAccountStore: false, storedAccountRuntimeIdentities: [stored.id: .providerAccount(id: "account-live")], storedAccountRuntimeEmails: [stored.id: "user@example.com"]) let liveWithoutLabel = ObservedSystemCodexAccount( email: "USER@example.com", workspaceLabel: nil, workspaceAccountID: "account-live", codexHomePath: "/Users/test/.codex", observedAt: Date(), identity: .providerAccount(id: "account-live")) let fallbackSnapshot = CodexAccountReconciliationSnapshot( storedAccounts: [stored], activeStoredAccount: stored, liveSystemAccount: liveWithoutLabel, matchingStoredAccountForLiveSystemAccount: stored, activeSource: .managedAccount(id: stored.id), hasUnreadableAddedAccountStore: false, storedAccountRuntimeIdentities: [stored.id: .providerAccount(id: "account-live")], storedAccountRuntimeEmails: [stored.id: "user@example.com"]) let liveWithEmptyLabel = ObservedSystemCodexAccount( email: "USER@example.com", workspaceLabel: " \n\t ", workspaceAccountID: "account-live", codexHomePath: "/Users/test/.codex", observedAt: Date(), identity: .providerAccount(id: "account-live")) let emptyLabelSnapshot = CodexAccountReconciliationSnapshot( storedAccounts: [stored], activeStoredAccount: stored, liveSystemAccount: liveWithEmptyLabel, matchingStoredAccountForLiveSystemAccount: stored, activeSource: .managedAccount(id: stored.id), hasUnreadableAddedAccountStore: false, storedAccountRuntimeIdentities: [stored.id: .providerAccount(id: "account-live")], storedAccountRuntimeEmails: [stored.id: "user@example.com"]) let labeledProjection = CodexVisibleAccountProjection.make(from: labeledSnapshot) let fallbackProjection = CodexVisibleAccountProjection.make(from: fallbackSnapshot) let emptyLabelProjection = CodexVisibleAccountProjection.make(from: emptyLabelSnapshot) #expect(labeledProjection.visibleAccounts.count == 1) #expect(labeledProjection.visibleAccounts.first?.workspaceLabel == "Live Team") #expect(labeledProjection.visibleAccounts.first?.displayName == "user@example.com — Live Team") #expect(fallbackProjection.visibleAccounts.count == 1) #expect(fallbackProjection.visibleAccounts.first?.workspaceLabel == "Saved Team") #expect(fallbackProjection.visibleAccounts.first?.displayName == "user@example.com — Saved Team") #expect(emptyLabelProjection.visibleAccounts.count == 1) #expect(emptyLabelProjection.visibleAccounts.first?.workspaceLabel == "Saved Team") #expect(emptyLabelProjection.visibleAccounts.first?.displayName == "user@example.com — Saved Team") } @Test func `matching live system account resolves merged row selection to live system`() { let stored = ManagedCodexAccount( id: UUID(), email: "user@example.com", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "USER@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let snapshot = CodexAccountReconciliationSnapshot( storedAccounts: [stored], activeStoredAccount: stored, liveSystemAccount: live, matchingStoredAccountForLiveSystemAccount: stored, activeSource: .managedAccount(id: stored.id), hasUnreadableAddedAccountStore: false) let resolution = CodexActiveSourceResolver.resolve(from: snapshot) let projection = CodexVisibleAccountProjection.make(from: snapshot) #expect(resolution.persistedSource == .managedAccount(id: stored.id)) #expect(resolution.resolvedSource == .liveSystem) #expect(resolution.requiresPersistenceCorrection) #expect(projection.activeVisibleAccountID == "user@example.com") #expect(projection.source(forVisibleAccountID: "user@example.com") == .liveSystem) } @Test func `provider account does not collapse with email only live account on same email`() throws { let managedHome = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true) defer { try? FileManager.default.removeItem(at: managedHome) } try Self.writeCodexAuthFile( homeURL: managedHome, email: "user@example.com", plan: "pro", accountID: "account-managed") let stored = ManagedCodexAccount( id: UUID(), email: "user@example.com", managedHomePath: managedHome.path, createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let accounts = ManagedCodexAccountSet(version: 1, accounts: [stored]) let live = ObservedSystemCodexAccount( email: "USER@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date(), identity: .emailOnly(normalizedEmail: "user@example.com")) let reconciler = DefaultCodexAccountReconciler( storeLoader: { accounts }, systemObserver: StubSystemObserver(account: live), activeSource: .managedAccount(id: stored.id), baseEnvironment: [:]) let snapshot = reconciler.loadSnapshot() let resolution = CodexActiveSourceResolver.resolve(from: snapshot) let projection = CodexVisibleAccountProjection.make(from: snapshot) #expect(snapshot.matchingStoredAccountForLiveSystemAccount == nil) #expect(resolution.resolvedSource == .managedAccount(id: stored.id)) #expect(projection.visibleAccounts.count == 2) #expect(Set(projection.visibleAccounts.map(\.email)) == Set(["user@example.com"])) #expect(Set(projection.visibleAccounts.map(\.id)).count == 2) #expect(projection.activeVisibleAccountID == projection.visibleAccounts .first { $0.selectionSource == .managedAccount(id: stored.id) }?.id) #expect(projection.liveVisibleAccountID == projection.visibleAccounts .first { $0.selectionSource == .liveSystem }?.id) } @Test func `missing managed source resolves to live system when live account exists`() { let live = ObservedSystemCodexAccount( email: "live@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let missingID = UUID() let snapshot = CodexAccountReconciliationSnapshot( storedAccounts: [], activeStoredAccount: nil, liveSystemAccount: live, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .managedAccount(id: missingID), hasUnreadableAddedAccountStore: false) let resolution = CodexActiveSourceResolver.resolve(from: snapshot) #expect(resolution.persistedSource == .managedAccount(id: missingID)) #expect(resolution.resolvedSource == .liveSystem) #expect(resolution.requiresPersistenceCorrection) } @Test func `unreadable managed source resolves to live system when live account exists`() { let live = ObservedSystemCodexAccount( email: "live@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let unreadableID = UUID() let snapshot = CodexAccountReconciliationSnapshot( storedAccounts: [], activeStoredAccount: nil, liveSystemAccount: live, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .managedAccount(id: unreadableID), hasUnreadableAddedAccountStore: true) let resolution = CodexActiveSourceResolver.resolve(from: snapshot) #expect(resolution.persistedSource == .managedAccount(id: unreadableID)) #expect(resolution.resolvedSource == .liveSystem) #expect(resolution.requiresPersistenceCorrection) } @Test func `managed account remains active when active source stays managed while live account changes`() { let managed = ManagedCodexAccount( id: UUID(), email: "managed@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "system@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let projection = CodexVisibleAccountProjection.make(from: CodexAccountReconciliationSnapshot( storedAccounts: [managed], activeStoredAccount: managed, liveSystemAccount: live, matchingStoredAccountForLiveSystemAccount: nil, activeSource: .managedAccount(id: managed.id), hasUnreadableAddedAccountStore: false)) #expect(Set(projection.visibleAccounts.map(\.email)) == [ "managed@example.com", "system@example.com", ]) #expect(projection.activeVisibleAccountID == "managed@example.com") #expect(projection.liveVisibleAccountID == "system@example.com") } @Test func `live system account that differs from active stored account remains visible`() { let active = ManagedCodexAccount( id: UUID(), email: "managed@example.com", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let accounts = ManagedCodexAccountSet(version: 1, accounts: [active]) let live = ObservedSystemCodexAccount( email: "system@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let reconciler = DefaultCodexAccountReconciler( storeLoader: { accounts }, systemObserver: StubSystemObserver(account: live), activeSource: .managedAccount(id: active.id), baseEnvironment: [:]) let projection = reconciler.loadVisibleAccounts() #expect(Set(projection.visibleAccounts.map(\.email)) == ["managed@example.com", "system@example.com"]) #expect(projection.activeVisibleAccountID == "managed@example.com") #expect(projection.liveVisibleAccountID == "system@example.com") } @Test func `inactive stored account still appears as visible`() { let active = ManagedCodexAccount( id: UUID(), email: "active@example.com", managedHomePath: "/tmp/managed-a", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let inactive = ManagedCodexAccount( id: UUID(), email: "inactive@example.com", managedHomePath: "/tmp/managed-b", createdAt: 4, updatedAt: 5, lastAuthenticatedAt: 6) let accounts = ManagedCodexAccountSet( version: 1, accounts: [active, inactive]) let live = ObservedSystemCodexAccount( email: "system@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let reconciler = DefaultCodexAccountReconciler( storeLoader: { accounts }, systemObserver: StubSystemObserver(account: live), activeSource: .managedAccount(id: active.id), baseEnvironment: [:]) let projection = reconciler.loadVisibleAccounts() #expect(Set(projection.visibleAccounts.map(\.email)) == [ "active@example.com", "inactive@example.com", "system@example.com", ]) #expect(projection.activeVisibleAccountID == "active@example.com") #expect(projection.liveVisibleAccountID == "system@example.com") } @Test func `unreadable account store still exposes live system account and degraded flag`() { let live = ObservedSystemCodexAccount( email: "live@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) let reconciler = DefaultCodexAccountReconciler( storeLoader: { throw FileManagedCodexAccountStoreError.unsupportedVersion(999) }, systemObserver: StubSystemObserver(account: live), baseEnvironment: [:]) let projection = reconciler.loadVisibleAccounts() #expect(projection.visibleAccounts.map(\.email) == ["live@example.com"]) #expect(projection.activeVisibleAccountID == "live@example.com") #expect(projection.liveVisibleAccountID == "live@example.com") #expect(projection.hasUnreadableAddedAccountStore) } @Test func `whitespace only live email is ignored`() { let accounts = ManagedCodexAccountSet(version: 1, accounts: []) let live = ObservedSystemCodexAccount( email: " \n\t ", codexHomePath: "/Users/test/.codex", observedAt: Date()) let reconciler = DefaultCodexAccountReconciler( storeLoader: { accounts }, systemObserver: StubSystemObserver(account: live), baseEnvironment: [:]) let projection = reconciler.loadVisibleAccounts() #expect(projection.visibleAccounts.isEmpty) #expect(projection.activeVisibleAccountID == nil) #expect(projection.liveVisibleAccountID == nil) } @Test @MainActor func `settings store can override active source to live system`() throws { let suite = "CodexAccountReconciliationTests-live-source-override" let settings = try Self.makeSettings(suite: suite) let managed = ManagedCodexAccount( id: UUID(), email: "managed@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "system@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings._test_activeManagedCodexAccount = managed settings._test_liveSystemCodexAccount = live settings.codexActiveSource = .liveSystem defer { settings._test_activeManagedCodexAccount = nil settings._test_liveSystemCodexAccount = nil } let snapshot = settings.codexAccountReconciliationSnapshot let projection = settings.codexVisibleAccountProjection #expect(settings.codexActiveSource == .liveSystem) #expect(snapshot.activeSource == .liveSystem) #expect(projection.activeVisibleAccountID == "system@example.com") #expect(projection.liveVisibleAccountID == "system@example.com") } @Test @MainActor func `selecting merged visible account persists live system source`() throws { let suite = "CodexAccountReconciliationTests-select-merged-visible-account" let settings = try Self.makeSettings(suite: suite) let managed = ManagedCodexAccount( id: UUID(), email: "same@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "SAME@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings._test_activeManagedCodexAccount = managed settings._test_liveSystemCodexAccount = live settings.codexActiveSource = .managedAccount(id: managed.id) defer { settings._test_activeManagedCodexAccount = nil settings._test_liveSystemCodexAccount = nil } let didSelect = settings.selectCodexVisibleAccount(id: "same@example.com") #expect(didSelect) #expect(settings.codexActiveSource == .liveSystem) #expect(settings.codexResolvedActiveSource == .liveSystem) } @Test @MainActor func `selecting authenticated managed account prefers live system when visible row is merged`() throws { let suite = "CodexAccountReconciliationTests-select-authenticated-managed-merged" let settings = try Self.makeSettings(suite: suite) let managed = ManagedCodexAccount( id: UUID(), email: "same@example.com", managedHomePath: "/tmp/managed-home", createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) let live = ObservedSystemCodexAccount( email: "SAME@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date()) settings._test_activeManagedCodexAccount = managed settings._test_liveSystemCodexAccount = live settings.codexActiveSource = .managedAccount(id: UUID()) defer { settings._test_activeManagedCodexAccount = nil settings._test_liveSystemCodexAccount = nil } settings.selectAuthenticatedManagedCodexAccount(managed) #expect(settings.codexActiveSource == .liveSystem) #expect(settings.codexResolvedActiveSource == .liveSystem) } @Test @MainActor func `selecting authenticated managed account keeps managed source for split identity rows`() throws { let suite = "CodexAccountReconciliationTests-select-authenticated-managed-split" let settings = try Self.makeSettings(suite: suite) let managedHome = FileManager.default.temporaryDirectory.appendingPathComponent( UUID().uuidString, isDirectory: true) let storeURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { settings._test_managedCodexAccountStoreURL = nil settings._test_liveSystemCodexAccount = nil try? FileManager.default.removeItem(at: managedHome) try? FileManager.default.removeItem(at: storeURL) } try Self.writeCodexAuthFile( homeURL: managedHome, email: "same@example.com", plan: "pro", accountID: "account-managed") let managed = ManagedCodexAccount( id: UUID(), email: "same@example.com", managedHomePath: managedHome.path, createdAt: 1, updatedAt: 2, lastAuthenticatedAt: 3) try Self.writeManagedCodexStore( ManagedCodexAccountSet(version: FileManagedCodexAccountStore.currentVersion, accounts: [managed]), to: storeURL) settings._test_managedCodexAccountStoreURL = storeURL settings._test_liveSystemCodexAccount = ObservedSystemCodexAccount( email: "SAME@example.com", codexHomePath: "/Users/test/.codex", observedAt: Date(), identity: .emailOnly(normalizedEmail: "same@example.com")) settings.codexActiveSource = .liveSystem let projection = settings.codexVisibleAccountProjection #expect(projection.visibleAccounts.count == 2) settings.selectAuthenticatedManagedCodexAccount(managed) #expect(settings.codexActiveSource == .managedAccount(id: managed.id)) #expect(settings.codexResolvedActiveSource == .managedAccount(id: managed.id)) } } private struct StubSystemObserver: CodexSystemAccountObserving { let account: ObservedSystemCodexAccount? func loadSystemAccount(environment _: [String: String]) throws -> ObservedSystemCodexAccount? { self.account } } extension CodexAccountReconciliationTests { private static func writeManagedCodexStore(_ accounts: ManagedCodexAccountSet, to storeURL: URL) throws { let store = FileManagedCodexAccountStore(fileURL: storeURL) try store.storeAccounts(accounts) } private static func writeCodexAuthFile( homeURL: URL, email: String, plan: String, accountID: String? = nil) throws { try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true) var tokens: [String: Any] = [ "accessToken": "access-token", "refreshToken": "refresh-token", "idToken": Self.fakeJWT(email: email, plan: plan, accountID: accountID), ] if let accountID { tokens["account_id"] = accountID } let auth = ["tokens": tokens] let data = try JSONSerialization.data(withJSONObject: auth) try data.write(to: homeURL.appendingPathComponent("auth.json")) } private static func fakeJWT(email: String, plan: String, accountID: String? = nil) -> String { let header = (try? JSONSerialization.data(withJSONObject: ["alg": "none"])) ?? Data() var payloadObject: [String: Any] = [ "email": email, "chatgpt_plan_type": plan, ] if let accountID { payloadObject["https://api.openai.com/auth"] = [ "chatgpt_account_id": accountID, ] } let payload = (try? JSONSerialization.data(withJSONObject: payloadObject)) ?? Data() func base64URL(_ data: Data) -> String { data.base64EncodedString() .replacingOccurrences(of: "=", with: "") .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") } return "\(base64URL(header)).\(base64URL(payload))." } }