import CodexBarCore import Foundation @testable import CodexBar @MainActor final class CodexAccountPromotionTestContainer { let suiteName: String let rootURL: URL let liveHomeURL: URL let managedHomesURL: URL let managedStoreURL: URL let settings: SettingsStore let usageStore: UsageStore let fileStore: FileManagedCodexAccountStore let homeFactory: ManagedCodexHomeFactory let identityReader: DefaultManagedCodexIdentityReader let workspaceResolver: any ManagedCodexWorkspaceResolving let baseEnvironment: [String: String] init( suiteName: String, workspaceIdentities: [String: CodexOpenAIWorkspaceIdentity] = [:]) throws { self.suiteName = suiteName self.rootURL = FileManager.default.temporaryDirectory .appendingPathComponent("codex-account-promotion-tests", isDirectory: true) .appendingPathComponent(UUID().uuidString, isDirectory: true) self.liveHomeURL = self.rootURL.appendingPathComponent("liveHome", isDirectory: true) self.managedHomesURL = self.rootURL.appendingPathComponent("managed-codex-homes", isDirectory: true) self.managedStoreURL = self.rootURL.appendingPathComponent("managed-codex-accounts.json", isDirectory: false) self.baseEnvironment = ["CODEX_HOME": self.liveHomeURL.path] self.fileStore = FileManagedCodexAccountStore(fileURL: self.managedStoreURL, fileManager: .default) self.homeFactory = ManagedCodexHomeFactory(root: self.managedHomesURL, fileManager: .default) self.identityReader = DefaultManagedCodexIdentityReader() self.workspaceResolver = StubManagedCodexWorkspaceResolver(identities: workspaceIdentities) try FileManager.default.createDirectory(at: self.liveHomeURL, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: self.managedHomesURL, withIntermediateDirectories: true) _ = try self.fileStore.ensureFileExists() let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) defaults.set(true, forKey: "providerDetectionCompleted") self.settings = SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suiteName), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) self.settings._test_activeManagedCodexAccount = nil self.settings._test_activeManagedCodexRemoteHomePath = nil self.settings._test_unreadableManagedCodexAccountStore = false self.settings._test_managedCodexAccountStoreURL = self.managedStoreURL self.settings._test_liveSystemCodexAccount = nil self.settings._test_codexReconciliationEnvironment = self.baseEnvironment self.settings.providerDetectionCompleted = true self.settings.refreshFrequency = .manual self.settings.codexCookieSource = .off self.usageStore = UsageStore( fetcher: UsageFetcher(environment: [:]), browserDetection: BrowserDetection(cacheTTL: 0), settings: self.settings, startupBehavior: .testing) self.installDynamicCodexUsageLoader() self.usageStore._test_codexCreditsLoaderOverride = { CreditsSnapshot(remaining: 17, events: [], updatedAt: Date()) } } func tearDown() { self.usageStore._test_codexCreditsLoaderOverride = nil self.settings._test_activeManagedCodexAccount = nil self.settings._test_activeManagedCodexRemoteHomePath = nil self.settings._test_unreadableManagedCodexAccountStore = false self.settings._test_managedCodexAccountStoreURL = nil self.settings._test_liveSystemCodexAccount = nil self.settings._test_codexReconciliationEnvironment = nil self.settings.userDefaults.removePersistentDomain(forName: self.suiteName) try? FileManager.default.removeItem(at: self.rootURL) } func makeService( store: (any ManagedCodexAccountStoring)? = nil, liveAuthSwapper: (any CodexLiveAuthSwapping)? = nil, activeSourceWriter: (any CodexActiveSourceWriting)? = nil, snapshotLoader: (any CodexAccountReconciliationSnapshotLoading)? = nil, accountScopedRefresher: (any CodexAccountScopedRefreshing)? = nil) -> CodexAccountPromotionService { CodexAccountPromotionService( store: store ?? self.fileStore, homeFactory: self.homeFactory, identityReader: self.identityReader, workspaceResolver: self.workspaceResolver, snapshotLoader: snapshotLoader ?? SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: self.settings), authMaterialReader: DefaultCodexAuthMaterialReader(), liveAuthSwapper: liveAuthSwapper ?? DefaultCodexLiveAuthSwapper(), activeSourceWriter: activeSourceWriter ?? SettingsStoreCodexActiveSourceWriter(settingsStore: self.settings), accountScopedRefresher: accountScopedRefresher ?? UsageStoreCodexAccountScopedRefresher(usageStore: self.usageStore), baseEnvironment: self.baseEnvironment, fileManager: .default) } func installDynamicCodexUsageLoader(usedPercent: Double = 12) { let baseSpec = self.usageStore.providerSpecs[.codex]! let liveHomePath = self.liveHomeURL.path let identityReader = self.identityReader self.usageStore .providerSpecs[.codex] = makeCodexProviderSpec(baseSpec: baseSpec) { let liveEmail = (try? identityReader.loadAccountIdentity(homePath: liveHomePath).email) ?? "unknown@example.com" return UsageSnapshot( primary: RateWindow( usedPercent: usedPercent, windowMinutes: 300, resetsAt: nil, resetDescription: nil), secondary: nil, updatedAt: Date(), identity: ProviderIdentitySnapshot( providerID: .codex, accountEmail: liveEmail, accountOrganization: nil, loginMethod: "Pro")) } } @discardableResult func createManagedAccount( id: UUID = UUID(), persistedEmail: String, authEmail: String? = nil, authAccountID: String? = nil, persistedProviderAccountID: String? = nil, useAuthAccountIDAsPersistedProviderAccountID: Bool = true, workspaceLabel: String? = nil, workspaceAccountID: String? = nil, plan: String = "Pro") throws -> ManagedCodexAccount { let homeURL = self.managedHomesURL.appendingPathComponent(id.uuidString, isDirectory: true) let createdAt = Date().timeIntervalSince1970 let authData = try self.writeOAuthAuthFile( homeURL: homeURL, email: authEmail ?? persistedEmail, plan: plan, accountID: authAccountID) let persistedProviderAccountIDValue: String? = if useAuthAccountIDAsPersistedProviderAccountID { persistedProviderAccountID ?? authAccountID } else { persistedProviderAccountID } return ManagedCodexAccount( id: id, email: persistedEmail, providerAccountID: persistedProviderAccountIDValue, workspaceLabel: workspaceLabel, workspaceAccountID: workspaceAccountID ?? authAccountID, authFingerprint: CodexAuthFingerprint.fingerprint(data: authData), managedHomePath: homeURL.path, createdAt: createdAt, updatedAt: createdAt, lastAuthenticatedAt: createdAt) } func persistAccounts(_ accounts: [ManagedCodexAccount]) throws { try self.fileStore.storeAccounts(ManagedCodexAccountSet( version: FileManagedCodexAccountStore.currentVersion, accounts: accounts)) } func loadAccounts() throws -> ManagedCodexAccountSet { try self.fileStore.loadAccounts() } @discardableResult func writeLiveOAuthAuthFile( email: String, plan: String = "Pro", accountID: String? = nil, apiKey: String? = nil) throws -> Data { try self.writeOAuthAuthFile( homeURL: self.liveHomeURL, email: email, plan: plan, accountID: accountID, apiKey: apiKey) } @discardableResult func writeLiveAPIKeyAuthFile(apiKey: String = "sk-live-only") throws -> Data { let data = try JSONSerialization.data( withJSONObject: ["OPENAI_API_KEY": apiKey], options: [.sortedKeys]) try FileManager.default.createDirectory(at: self.liveHomeURL, withIntermediateDirectories: true) try data.write(to: Self.authFileURL(for: self.liveHomeURL), options: .atomic) return data } @discardableResult func writeLiveOAuthAuthFileWithoutEmail(accountID: String, apiKey: String? = nil) throws -> Data { try self.writeOAuthAuthFileWithoutEmail( homeURL: self.liveHomeURL, accountID: accountID, apiKey: apiKey) } @discardableResult func writeManagedOAuthAuthFileWithoutEmail( for account: ManagedCodexAccount, accountID: String, apiKey: String? = nil) throws -> Data { try self.writeOAuthAuthFileWithoutEmail( homeURL: URL(fileURLWithPath: account.managedHomePath, isDirectory: true), accountID: accountID, apiKey: apiKey) } @discardableResult private func writeOAuthAuthFileWithoutEmail( homeURL: URL, accountID: String, apiKey: String? = nil) throws -> Data { try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true) let tokens: [String: Any] = [ "accessToken": "access-\(accountID)", "refreshToken": "refresh-\(accountID)", "accountId": accountID, ] var json: [String: Any] = [ "tokens": tokens, "last_refresh": "2026-04-05T00:00:00Z", ] if let apiKey { json["OPENAI_API_KEY"] = apiKey } let data = try JSONSerialization.data(withJSONObject: json, options: [.sortedKeys]) try data.write(to: Self.authFileURL(for: homeURL), options: .atomic) return data } func removeLiveAuthFile() throws { let authFileURL = Self.authFileURL(for: self.liveHomeURL) if FileManager.default.fileExists(atPath: authFileURL.path) { try FileManager.default.removeItem(at: authFileURL) } } func liveAuthData() throws -> Data? { let authFileURL = Self.authFileURL(for: self.liveHomeURL) guard FileManager.default.fileExists(atPath: authFileURL.path) else { return nil } return try Data(contentsOf: authFileURL) } func managedAuthData(for account: ManagedCodexAccount) throws -> Data { try Data(contentsOf: Self.authFileURL(for: URL(fileURLWithPath: account.managedHomePath, isDirectory: true))) } func managedHomeURLs() throws -> [URL] { let urls = try FileManager.default.contentsOfDirectory( at: self.managedHomesURL, includingPropertiesForKeys: nil) return urls.sorted { $0.lastPathComponent < $1.lastPathComponent } } func seedScopedRefreshState(email: String, identity: CodexIdentity) { let snapshot = UsageSnapshot( primary: RateWindow(usedPercent: 4, windowMinutes: 300, resetsAt: nil, resetDescription: nil), secondary: nil, updatedAt: Date(), identity: ProviderIdentitySnapshot( providerID: .codex, accountEmail: email, accountOrganization: nil, loginMethod: "Pro")) let credits = CreditsSnapshot(remaining: 3, events: [], updatedAt: Date()) self.usageStore._setSnapshotForTesting(snapshot, provider: .codex) self.usageStore.credits = credits self.usageStore.lastCreditsSnapshot = credits self.usageStore.lastCreditsSnapshotAccountKey = email self.usageStore.lastCreditsSource = .api self.usageStore.lastCodexAccountScopedRefreshGuard = CodexAccountScopedRefreshGuard( source: .liveSystem, identity: identity, accountKey: email) } @discardableResult private func writeOAuthAuthFile( homeURL: URL, email: String, plan: String, accountID: String?, apiKey: String? = nil) throws -> Data { try FileManager.default.createDirectory(at: homeURL, withIntermediateDirectories: true) var tokens: [String: Any] = [ "accessToken": "access-\(email)", "refreshToken": "refresh-\(email)", "idToken": Self.fakeJWT(email: email, plan: plan, accountID: accountID), ] if let accountID { tokens["accountId"] = accountID } var json: [String: Any] = [ "tokens": tokens, "last_refresh": "2026-04-05T00:00:00Z", ] if let apiKey { json["OPENAI_API_KEY"] = apiKey } let data = try JSONSerialization.data(withJSONObject: json, options: [.sortedKeys]) try data.write(to: Self.authFileURL(for: homeURL), options: .atomic) return data } private static func fakeJWT(email: String, plan: String, accountID: String?) -> String { let header = (try? JSONSerialization.data(withJSONObject: ["alg": "none"])) ?? Data() var authClaims: [String: Any] = [ "chatgpt_plan_type": plan, ] if let accountID { authClaims["chatgpt_account_id"] = accountID } let payload = (try? JSONSerialization.data(withJSONObject: [ "email": email, "chatgpt_plan_type": plan, "https://api.openai.com/auth": authClaims, ])) ?? Data() func base64URL(_ data: Data) -> String { data.base64EncodedString() .replacingOccurrences(of: "=", with: "") .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") } return "\(base64URL(header)).\(base64URL(payload))." } private static func authFileURL(for homeURL: URL) -> URL { homeURL.appendingPathComponent("auth.json", isDirectory: false) } } private func makeCodexProviderSpec( baseSpec: ProviderSpec, loader: @escaping @Sendable () async throws -> UsageSnapshot) -> ProviderSpec { let baseDescriptor = baseSpec.descriptor let strategy = TestPromotionCodexFetchStrategy(loader: loader) let descriptor = ProviderDescriptor( id: .codex, metadata: baseDescriptor.metadata, branding: baseDescriptor.branding, tokenCost: baseDescriptor.tokenCost, fetchPlan: ProviderFetchPlan( sourceModes: [.auto, .cli, .oauth], pipeline: ProviderFetchPipeline { _ in [strategy] }), cli: baseDescriptor.cli) return ProviderSpec( style: baseSpec.style, isEnabled: baseSpec.isEnabled, descriptor: descriptor, makeFetchContext: baseSpec.makeFetchContext) } private struct TestPromotionCodexFetchStrategy: ProviderFetchStrategy { let loader: @Sendable () async throws -> UsageSnapshot var id: String { "test-promotion-codex" } var kind: ProviderFetchKind { .cli } func isAvailable(_: ProviderFetchContext) async -> Bool { true } func fetch(_: ProviderFetchContext) async throws -> ProviderFetchResult { let snapshot = try await self.loader() return self.makeResult(usage: snapshot, sourceLabel: "test-promotion-codex") } func shouldFallback(on _: Error, context _: ProviderFetchContext) -> Bool { false } } final class RecordingManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable { let base: any ManagedCodexAccountStoring var storedSnapshots: [ManagedCodexAccountSet] = [] var onStore: (@Sendable (ManagedCodexAccountSet) throws -> Void)? init( base: any ManagedCodexAccountStoring, onStore: (@Sendable (ManagedCodexAccountSet) throws -> Void)? = nil) { self.base = base self.onStore = onStore } func loadAccounts() throws -> ManagedCodexAccountSet { try self.base.loadAccounts() } func storeAccounts(_ accounts: ManagedCodexAccountSet) throws { self.storedSnapshots.append(accounts) try self.onStore?(accounts) try self.base.storeAccounts(accounts) } func ensureFileExists() throws -> URL { try self.base.ensureFileExists() } } final class RecordingCodexLiveAuthSwapper: CodexLiveAuthSwapping, @unchecked Sendable { let base: any CodexLiveAuthSwapping var swapCallCount = 0 var swappedData: [Data] = [] var onSwap: (@Sendable (Data, URL) throws -> Void)? init( base: any CodexLiveAuthSwapping = DefaultCodexLiveAuthSwapper(), onSwap: (@Sendable (Data, URL) throws -> Void)? = nil) { self.base = base self.onSwap = onSwap } func swapLiveAuthData(_ data: Data, liveHomeURL: URL) throws { self.swapCallCount += 1 self.swappedData.append(data) try self.onSwap?(data, liveHomeURL) try self.base.swapLiveAuthData(data, liveHomeURL: liveHomeURL) } } enum PromotionTestError: Error, Equatable { case storeWriteFailed case swapFailed case unexpectedDisposition } private struct StubManagedCodexWorkspaceResolver: ManagedCodexWorkspaceResolving { let identities: [String: CodexOpenAIWorkspaceIdentity] func resolveWorkspaceIdentity( homePath _: String, providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity? { self.identities[providerAccountID] } } @MainActor struct StaticCodexAccountReconciliationSnapshotLoader: CodexAccountReconciliationSnapshotLoading { let snapshot: CodexAccountReconciliationSnapshot func loadSnapshot() -> CodexAccountReconciliationSnapshot { self.snapshot } } @MainActor final class ClosureCodexAccountScopedRefresher: CodexAccountScopedRefreshing { private let onRefresh: @MainActor (Bool) async -> Void init(onRefresh: @escaping @MainActor (Bool) async -> Void) { self.onRefresh = onRefresh } func refreshCodexAccountScopedState(allowDisabled: Bool) async { await self.onRefresh(allowDisabled) } }