959 lines
45 KiB
Swift
959 lines
45 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
struct ManagedCodexAccountServiceTests {
|
|
@Test
|
|
func `upsert preserves uuid for matching canonical email`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let fileURL = root.appendingPathComponent("managed.json", isDirectory: false)
|
|
let store = FileManagedCodexAccountStore(fileURL: fileURL, fileManager: .default)
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
|
|
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let first = try await service.authenticateManagedAccount()
|
|
let second = try await service.authenticateManagedAccount()
|
|
let snapshot = try store.loadAccounts()
|
|
|
|
#expect(first.id == second.id)
|
|
#expect(second.email == "user@example.com")
|
|
#expect(second.providerAccountID == "account-live")
|
|
#expect(snapshot.accounts.count == 1)
|
|
#expect(second.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
|
|
}
|
|
|
|
@Test
|
|
func `new authentication appends managed account without implicit selection side effect`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let firstID = try #require(UUID(uuidString: "11111111-1111-1111-1111-111111111111"))
|
|
let firstAccount = ManagedCodexAccount(
|
|
id: firstID,
|
|
email: "first@example.com",
|
|
managedHomePath: root.appendingPathComponent("accounts/first", isDirectory: true).path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: 1,
|
|
accounts: [firstAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-second"), email: "second@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let authenticated = try await service.authenticateManagedAccount()
|
|
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(authenticated.email == "second@example.com")
|
|
#expect(authenticated.providerAccountID == "account-second")
|
|
}
|
|
|
|
@Test
|
|
func `same email provider backed workspaces coexist across sequential add account flows`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: []))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "workspace-personal"), email: "alice@example.com", plan: "Pro"),
|
|
.init(identity: .providerAccount(id: "workspace-team"), email: "alice@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
|
|
"workspace-personal": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "workspace-personal",
|
|
workspaceLabel: "Personal"),
|
|
"workspace-team": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "workspace-team",
|
|
workspaceLabel: "Team"),
|
|
]))
|
|
|
|
let personal = try await service.authenticateManagedAccount()
|
|
let team = try await service.authenticateManagedAccount()
|
|
|
|
let storedPersonal = try #require(
|
|
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-personal"))
|
|
let storedTeam = try #require(
|
|
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-team"))
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(personal.id == storedPersonal.id)
|
|
#expect(team.id == storedTeam.id)
|
|
#expect(personal.id != team.id)
|
|
#expect(storedPersonal.providerAccountID == "workspace-personal")
|
|
#expect(storedPersonal.workspaceLabel == "Personal")
|
|
#expect(storedTeam.providerAccountID == "workspace-team")
|
|
#expect(storedTeam.workspaceLabel == "Team")
|
|
#expect(storedPersonal.managedHomePath != storedTeam.managedHomePath)
|
|
#expect(FileManager.default.fileExists(atPath: storedPersonal.managedHomePath))
|
|
#expect(FileManager.default.fileExists(atPath: storedTeam.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `same workspace provider id with different emails does not overwrite existing account`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let existingID = try #require(UUID(uuidString: "10101010-2020-3030-4040-505050505050"))
|
|
let existingAccount = ManagedCodexAccount(
|
|
id: existingID,
|
|
email: "mi.chaelfmk5542@gmail.com",
|
|
providerAccountID: "team-4107",
|
|
workspaceLabel: "4107",
|
|
workspaceAccountID: "team-4107",
|
|
managedHomePath: existingHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [existingAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "team-4107"), email: "mich.aelfmk5542@gmail.com", plan: "Team"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
|
|
"team-4107": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "team-4107",
|
|
workspaceLabel: "4107"),
|
|
]))
|
|
|
|
let added = try await service.authenticateManagedAccount()
|
|
|
|
let original = try #require(
|
|
store.snapshot.account(email: "mi.chaelfmk5542@gmail.com", providerAccountID: "team-4107"))
|
|
let newAccount = try #require(
|
|
store.snapshot.account(email: "mich.aelfmk5542@gmail.com", providerAccountID: "team-4107"))
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(original.id == existingID)
|
|
#expect(original.managedHomePath == existingHome.path)
|
|
#expect(newAccount.id == added.id)
|
|
#expect(newAccount.id != existingID)
|
|
#expect(newAccount.workspaceLabel == "4107")
|
|
#expect(FileManager.default.fileExists(atPath: existingHome.path))
|
|
#expect(FileManager.default.fileExists(atPath: newAccount.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `selected workspace is persisted and used as account identity`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: []))
|
|
let workspaces = [
|
|
CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "workspace-personal",
|
|
workspaceLabel: "Personal"),
|
|
CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "workspace-team",
|
|
workspaceLabel: "Team"),
|
|
]
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: WritingManagedCodexLoginRunner(
|
|
credentials: CodexOAuthCredentials(
|
|
accessToken: "access-token",
|
|
refreshToken: "refresh-token",
|
|
idToken: nil,
|
|
accountId: "workspace-personal",
|
|
lastRefresh: nil)),
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "workspace-personal"), email: "alice@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver(
|
|
identities: Dictionary(uniqueKeysWithValues: workspaces.map { ($0.workspaceAccountID, $0) }),
|
|
availableIdentities: workspaces),
|
|
workspaceSelector: StubManagedCodexWorkspaceSelector(selectedWorkspaceID: "workspace-team"))
|
|
|
|
let account = try await service.authenticateManagedAccount()
|
|
let credentials = try CodexOAuthCredentialsStore.load(env: ["CODEX_HOME": account.managedHomePath])
|
|
|
|
#expect(account.providerAccountID == "workspace-team")
|
|
#expect(account.workspaceLabel == "Team")
|
|
#expect(credentials.accountId == "workspace-team")
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
}
|
|
|
|
@Test
|
|
func `reauth keeps previous home when store write fails`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let existingAccountID = try #require(UUID(uuidString: "11111111-2222-3333-4444-555555555555"))
|
|
let existingAccount = ManagedCodexAccount(
|
|
id: existingAccountID,
|
|
email: "user@example.com",
|
|
managedHomePath: existingHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = FailingManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: 1,
|
|
accounts: [existingAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
await #expect(throws: TestManagedCodexAccountStoreError.writeFailed) {
|
|
try await service.authenticateManagedAccount()
|
|
}
|
|
|
|
let newHome = root.appendingPathComponent("accounts/account-1", isDirectory: true)
|
|
#expect(FileManager.default.fileExists(atPath: existingHome.path))
|
|
#expect(FileManager.default.fileExists(atPath: newHome.path) == false)
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
#expect(store.snapshot.accounts.first?.managedHomePath == existingHome.path)
|
|
}
|
|
|
|
@Test
|
|
func `reauth reconciles by provider account id before existing account id`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let alphaHome = root.appendingPathComponent("accounts/alpha", isDirectory: true)
|
|
let betaHome = root.appendingPathComponent("accounts/beta", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: alphaHome, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: betaHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let alphaID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-111111111111"))
|
|
let betaID = try #require(UUID(uuidString: "BBBBBBBB-CCCC-DDDD-EEEE-222222222222"))
|
|
let alphaAccount = ManagedCodexAccount(
|
|
id: alphaID,
|
|
email: "shared@example.com",
|
|
providerAccountID: "account-alpha",
|
|
managedHomePath: alphaHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let betaAccount = ManagedCodexAccount(
|
|
id: betaID,
|
|
email: "shared@example.com",
|
|
providerAccountID: "account-beta",
|
|
managedHomePath: betaHome.path,
|
|
createdAt: 2,
|
|
updatedAt: 2,
|
|
lastAuthenticatedAt: 2)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: 1,
|
|
accounts: [alphaAccount, betaAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-beta"), email: "SHARED@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let account = try await service.authenticateManagedAccount(existingAccountID: alphaAccount.id)
|
|
|
|
let storedAlpha = try #require(store.snapshot.account(id: alphaAccount.id))
|
|
let storedBeta = try #require(store.snapshot.account(id: betaAccount.id))
|
|
#expect(account.id == betaAccount.id)
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(storedAlpha.email == "shared@example.com")
|
|
#expect(storedAlpha.providerAccountID == "account-alpha")
|
|
#expect(storedAlpha.managedHomePath == alphaHome.path)
|
|
#expect(storedBeta.email == "shared@example.com")
|
|
#expect(storedBeta.providerAccountID == "account-beta")
|
|
#expect(storedBeta.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
|
|
#expect(storedBeta.managedHomePath != betaHome.path)
|
|
#expect(FileManager.default.fileExists(atPath: alphaHome.path))
|
|
#expect(FileManager.default.fileExists(atPath: betaHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: storedBeta.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `reauth to different account does not overwrite existing account id match`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let aliceHome = root.appendingPathComponent("accounts/alice", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: aliceHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let aliceID = try #require(UUID(uuidString: "12121212-3434-5656-7878-909090909090"))
|
|
let aliceAccount = ManagedCodexAccount(
|
|
id: aliceID,
|
|
email: "alice@example.com",
|
|
providerAccountID: "account-alice",
|
|
managedHomePath: aliceHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [aliceAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-bob"), email: "bob@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let account = try await service.authenticateManagedAccount(existingAccountID: aliceID)
|
|
|
|
let storedAlice = try #require(store.snapshot.account(id: aliceID))
|
|
let storedBob = try #require(store.snapshot.account(email: "bob@example.com"))
|
|
#expect(account.id != aliceID)
|
|
#expect(account.id == storedBob.id)
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(storedAlice.email == "alice@example.com")
|
|
#expect(storedAlice.providerAccountID == "account-alice")
|
|
#expect(storedAlice.managedHomePath == aliceHome.path)
|
|
#expect(storedBob.email == "bob@example.com")
|
|
#expect(storedBob.providerAccountID == "account-bob")
|
|
#expect(storedBob.managedHomePath.hasPrefix(root.standardizedFileURL.path + "/"))
|
|
#expect(storedBob.managedHomePath != aliceHome.path)
|
|
#expect(FileManager.default.fileExists(atPath: aliceHome.path))
|
|
#expect(FileManager.default.fileExists(atPath: storedBob.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `reauth on same email different workspace does not overwrite selected workspace`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let personalHome = root.appendingPathComponent("accounts/personal", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: personalHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let personalID = try #require(UUID(uuidString: "31313131-4242-5353-6464-757575757575"))
|
|
let personalAccount = ManagedCodexAccount(
|
|
id: personalID,
|
|
email: "alice@example.com",
|
|
providerAccountID: "workspace-personal",
|
|
workspaceLabel: "Personal",
|
|
workspaceAccountID: "workspace-personal",
|
|
managedHomePath: personalHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [personalAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "workspace-team"), email: "alice@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
|
|
"workspace-team": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "workspace-team",
|
|
workspaceLabel: "Team"),
|
|
]))
|
|
|
|
let account = try await service.authenticateManagedAccount(existingAccountID: personalID)
|
|
|
|
let storedPersonal = try #require(store.snapshot.account(id: personalID))
|
|
let storedTeam = try #require(
|
|
store.snapshot.account(email: "alice@example.com", providerAccountID: "workspace-team"))
|
|
#expect(account.id == storedTeam.id)
|
|
#expect(account.id != personalID)
|
|
#expect(store.snapshot.accounts.count == 2)
|
|
#expect(storedPersonal.providerAccountID == "workspace-personal")
|
|
#expect(storedPersonal.workspaceLabel == "Personal")
|
|
#expect(storedPersonal.managedHomePath == personalHome.path)
|
|
#expect(storedTeam.providerAccountID == "workspace-team")
|
|
#expect(storedTeam.workspaceLabel == "Team")
|
|
#expect(storedTeam.managedHomePath != personalHome.path)
|
|
#expect(FileManager.default.fileExists(atPath: personalHome.path))
|
|
#expect(FileManager.default.fileExists(atPath: storedTeam.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `legacy row collapses onto provider backed row when provider id resolves elsewhere`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let legacyHome = root.appendingPathComponent("accounts/legacy", isDirectory: true)
|
|
let providerHome = root.appendingPathComponent("accounts/provider", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: legacyHome, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: providerHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let legacyID = try #require(UUID(uuidString: "AAAAAAAA-1111-2222-3333-444444444444"))
|
|
let providerID = try #require(UUID(uuidString: "BBBBBBBB-1111-2222-3333-444444444444"))
|
|
let legacyAccount = ManagedCodexAccount(
|
|
id: legacyID,
|
|
email: "shared@example.com",
|
|
managedHomePath: legacyHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let providerAccount = ManagedCodexAccount(
|
|
id: providerID,
|
|
email: "shared@example.com",
|
|
providerAccountID: "account-real",
|
|
managedHomePath: providerHome.path,
|
|
createdAt: 2,
|
|
updatedAt: 2,
|
|
lastAuthenticatedAt: 2)
|
|
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [legacyAccount, providerAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-real"), email: "shared@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let account = try await service.authenticateManagedAccount(existingAccountID: legacyAccount.id)
|
|
|
|
#expect(account.id == providerAccount.id)
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
#expect(store.snapshot.accounts.first?.id == providerAccount.id)
|
|
#expect(store.snapshot.accounts.first?.providerAccountID == "account-real")
|
|
#expect(FileManager.default.fileExists(atPath: legacyHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: providerHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `fresh provider login removes stale legacy row without explicit existing account id`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let legacyHome = root.appendingPathComponent("accounts/legacy", isDirectory: true)
|
|
let providerHome = root.appendingPathComponent("accounts/provider", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: legacyHome, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: providerHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let legacyID = try #require(UUID(uuidString: "CCCCCCCC-1111-2222-3333-444444444444"))
|
|
let providerID = try #require(UUID(uuidString: "DDDDDDDD-1111-2222-3333-444444444444"))
|
|
let legacyAccount = ManagedCodexAccount(
|
|
id: legacyID,
|
|
email: "shared@example.com",
|
|
managedHomePath: legacyHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let providerAccount = ManagedCodexAccount(
|
|
id: providerID,
|
|
email: "shared@example.com",
|
|
providerAccountID: "account-real",
|
|
managedHomePath: providerHome.path,
|
|
createdAt: 2,
|
|
updatedAt: 2,
|
|
lastAuthenticatedAt: 2)
|
|
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [legacyAccount, providerAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-real"), email: "shared@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let account = try await service.authenticateManagedAccount()
|
|
|
|
#expect(account.id == providerAccount.id)
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
#expect(store.snapshot.accounts.first?.id == providerAccount.id)
|
|
#expect(FileManager.default.fileExists(atPath: legacyHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: providerHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `authentication persists workspace metadata and tolerates missing workspace label`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: FileManagedCodexAccountStore.currentVersion, accounts: []))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(identity: .providerAccount(id: "account-live"), email: "user@example.com", plan: "Pro"),
|
|
.init(identity: .providerAccount(id: "account-fallback"), email: "fallback@example.com", plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver(identities: [
|
|
"account-live": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "account-live",
|
|
workspaceLabel: "Team Alpha"),
|
|
"account-fallback": CodexOpenAIWorkspaceIdentity(
|
|
workspaceAccountID: "account-fallback",
|
|
workspaceLabel: nil),
|
|
]))
|
|
|
|
let labeled = try await service.authenticateManagedAccount()
|
|
let fallback = try await service.authenticateManagedAccount()
|
|
|
|
#expect(labeled.providerAccountID == "account-live")
|
|
#expect(labeled.workspaceAccountID == "account-live")
|
|
#expect(labeled.workspaceLabel == "Team Alpha")
|
|
#expect(fallback.providerAccountID == "account-fallback")
|
|
#expect(fallback.workspaceAccountID == "account-fallback")
|
|
#expect(fallback.workspaceLabel == nil)
|
|
}
|
|
|
|
@Test
|
|
func `reauth preserves stored provider metadata when refresh cannot resolve account id`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let existingHome = root.appendingPathComponent("accounts/existing", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: existingHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let existingID = try #require(UUID(uuidString: "EEEEEEEE-1111-2222-3333-444444444444"))
|
|
let existingAccount = ManagedCodexAccount(
|
|
id: existingID,
|
|
email: "user@example.com",
|
|
providerAccountID: "account-live",
|
|
workspaceLabel: "Team Alpha",
|
|
workspaceAccountID: "account-live",
|
|
managedHomePath: existingHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(accounts: ManagedCodexAccountSet(
|
|
version: FileManagedCodexAccountStore.currentVersion,
|
|
accounts: [existingAccount]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.accounts([
|
|
.init(
|
|
identity: .emailOnly(normalizedEmail: "user@example.com"),
|
|
email: "user@example.com",
|
|
plan: "Pro"),
|
|
]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
let account = try await service.authenticateManagedAccount(existingAccountID: existingID)
|
|
let stored = try #require(store.snapshot.account(id: existingID))
|
|
|
|
#expect(account.id == existingID)
|
|
#expect(account.providerAccountID == "account-live")
|
|
#expect(account.workspaceAccountID == "account-live")
|
|
#expect(account.workspaceLabel == "Team Alpha")
|
|
#expect(stored.providerAccountID == "account-live")
|
|
#expect(stored.workspaceAccountID == "account-live")
|
|
#expect(stored.workspaceLabel == "Team Alpha")
|
|
#expect(FileManager.default.fileExists(atPath: existingHome.path) == false)
|
|
#expect(FileManager.default.fileExists(atPath: account.managedHomePath))
|
|
}
|
|
|
|
@Test
|
|
func `auth failure cleanup uses managed root safety check`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let outsideHome = FileManager.default.temporaryDirectory.appendingPathComponent(
|
|
UUID().uuidString,
|
|
isDirectory: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: root)
|
|
try? FileManager.default.removeItem(at: outsideHome)
|
|
}
|
|
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: 1, accounts: []))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: UnsafeManagedCodexHomeFactory(root: root, homeURL: outsideHome),
|
|
loginRunner: StubManagedCodexLoginRunner(
|
|
result: CodexLoginRunner.Result(outcome: .failed(status: 1), output: "nope")),
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
await #expect(throws: ManagedCodexAccountServiceError.self) {
|
|
try await service.authenticateManagedAccount()
|
|
}
|
|
|
|
#expect(FileManager.default.fileExists(atPath: outsideHome.path))
|
|
#expect(store.snapshot.accounts.isEmpty)
|
|
}
|
|
|
|
@Test
|
|
func `auth failure preserves codex login result output`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let loginResult = CodexLoginRunner.Result(
|
|
outcome: .failed(status: 42),
|
|
output: "OAuth callback used the wrong browser profile")
|
|
let service = ManagedCodexAccountService(
|
|
store: InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: 1, accounts: [])),
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner(result: loginResult),
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
do {
|
|
_ = try await service.authenticateManagedAccount()
|
|
Issue.record("Expected managed Codex login failure")
|
|
} catch let error as ManagedCodexAccountServiceError {
|
|
guard case let .loginFailed(capturedResult) = error else {
|
|
Issue.record("Expected loginFailed, got \(error)")
|
|
return
|
|
}
|
|
#expect(capturedResult == loginResult)
|
|
#expect(error.userFacingMessage.contains("codex --version"))
|
|
#expect(error.userFacingMessage.contains("OAuth callback used the wrong browser profile"))
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `remove deletes managed home under managed root`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let home = root.appendingPathComponent("accounts/account-a", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let accountID = try #require(UUID(uuidString: "AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"))
|
|
let account = ManagedCodexAccount(
|
|
id: accountID,
|
|
email: "user@example.com",
|
|
managedHomePath: home.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
try await service.removeManagedAccount(id: account.id)
|
|
|
|
#expect(store.snapshot.accounts.isEmpty)
|
|
#expect(FileManager.default.fileExists(atPath: home.path) == false)
|
|
}
|
|
|
|
@Test
|
|
func `remove keeps remaining managed account records`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let firstHome = root.appendingPathComponent("accounts/account-a", isDirectory: true)
|
|
let secondHome = root.appendingPathComponent("accounts/account-b", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: firstHome, withIntermediateDirectories: true)
|
|
try FileManager.default.createDirectory(at: secondHome, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let firstID = try #require(UUID(uuidString: "AAAAAAAA-1111-1111-1111-111111111111"))
|
|
let secondID = try #require(UUID(uuidString: "BBBBBBBB-2222-2222-2222-222222222222"))
|
|
let first = ManagedCodexAccount(
|
|
id: firstID,
|
|
email: "first@example.com",
|
|
managedHomePath: firstHome.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let second = ManagedCodexAccount(
|
|
id: secondID,
|
|
email: "second@example.com",
|
|
managedHomePath: secondHome.path,
|
|
createdAt: 2,
|
|
updatedAt: 2,
|
|
lastAuthenticatedAt: 2)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(
|
|
version: 1,
|
|
accounts: [first, second]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
try await service.removeManagedAccount(id: second.id)
|
|
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
#expect(store.snapshot.accounts.first?.id == first.id)
|
|
#expect(FileManager.default.fileExists(atPath: secondHome.path) == false)
|
|
}
|
|
|
|
@Test
|
|
func `remove keeps persisted account when store write fails`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let home = root.appendingPathComponent("accounts/account-a", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
|
|
let accountID = try #require(UUID(uuidString: "CCCCCCCC-DDDD-EEEE-FFFF-000000000000"))
|
|
let account = ManagedCodexAccount(
|
|
id: accountID,
|
|
email: "user@example.com",
|
|
managedHomePath: home.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = FailingManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
await #expect(throws: TestManagedCodexAccountStoreError.writeFailed) {
|
|
try await service.removeManagedAccount(id: account.id)
|
|
}
|
|
|
|
#expect(store.snapshot.accounts.count == 1)
|
|
#expect(store.snapshot.accounts.first?.managedHomePath == home.path)
|
|
#expect(FileManager.default.fileExists(atPath: home.path))
|
|
}
|
|
|
|
@Test
|
|
func `remove drops account but leaves unsafe home untouched`() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
let outsideRoot = FileManager.default.temporaryDirectory.appendingPathComponent(
|
|
UUID().uuidString,
|
|
isDirectory: true)
|
|
try FileManager.default.createDirectory(at: outsideRoot, withIntermediateDirectories: true)
|
|
defer {
|
|
try? FileManager.default.removeItem(at: root)
|
|
try? FileManager.default.removeItem(at: outsideRoot)
|
|
}
|
|
|
|
let accountID = try #require(UUID(uuidString: "BBBBBBBB-CCCC-DDDD-EEEE-FFFFFFFFFFFF"))
|
|
let account = ManagedCodexAccount(
|
|
id: accountID,
|
|
email: "user@example.com",
|
|
managedHomePath: outsideRoot.path,
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
lastAuthenticatedAt: 1)
|
|
let store = InMemoryManagedCodexAccountStore(
|
|
accounts: ManagedCodexAccountSet(version: 1, accounts: [account]))
|
|
let service = ManagedCodexAccountService(
|
|
store: store,
|
|
homeFactory: TestManagedCodexHomeFactory(root: root),
|
|
loginRunner: StubManagedCodexLoginRunner.success,
|
|
identityReader: StubManagedCodexIdentityReader.emails([]),
|
|
workspaceResolver: StubManagedCodexWorkspaceResolver())
|
|
|
|
try await service.removeManagedAccount(id: account.id)
|
|
|
|
#expect(store.snapshot.accounts.isEmpty)
|
|
#expect(FileManager.default.fileExists(atPath: outsideRoot.path))
|
|
}
|
|
}
|
|
|
|
private final class InMemoryManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
|
|
var snapshot: ManagedCodexAccountSet
|
|
|
|
init(accounts: ManagedCodexAccountSet) {
|
|
self.snapshot = accounts
|
|
}
|
|
|
|
func loadAccounts() throws -> ManagedCodexAccountSet {
|
|
self.snapshot
|
|
}
|
|
|
|
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
|
|
self.snapshot = accounts
|
|
}
|
|
|
|
func ensureFileExists() throws -> URL {
|
|
FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
}
|
|
}
|
|
|
|
private final class FailingManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
|
|
var snapshot: ManagedCodexAccountSet
|
|
|
|
init(accounts: ManagedCodexAccountSet) {
|
|
self.snapshot = accounts
|
|
}
|
|
|
|
func loadAccounts() throws -> ManagedCodexAccountSet {
|
|
self.snapshot
|
|
}
|
|
|
|
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
|
|
_ = accounts
|
|
throw TestManagedCodexAccountStoreError.writeFailed
|
|
}
|
|
|
|
func ensureFileExists() throws -> URL {
|
|
FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
}
|
|
}
|
|
|
|
private final class TestManagedCodexHomeFactory: ManagedCodexHomeProducing, @unchecked Sendable {
|
|
let root: URL
|
|
private let lock = NSLock()
|
|
private var index: Int = 0
|
|
|
|
init(root: URL) {
|
|
self.root = root
|
|
}
|
|
|
|
private func nextPathComponent() -> String {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
self.index += 1
|
|
return "accounts/account-\(self.index)"
|
|
}
|
|
|
|
func makeHomeURL() -> URL {
|
|
self.root.appendingPathComponent(self.nextPathComponent(), isDirectory: true)
|
|
}
|
|
|
|
func validateManagedHomeForDeletion(_ url: URL) throws {
|
|
try ManagedCodexHomeFactory(root: self.root).validateManagedHomeForDeletion(url)
|
|
}
|
|
}
|
|
|
|
private struct UnsafeManagedCodexHomeFactory: ManagedCodexHomeProducing {
|
|
let root: URL
|
|
let homeURL: URL
|
|
|
|
func makeHomeURL() -> URL {
|
|
self.homeURL
|
|
}
|
|
|
|
func validateManagedHomeForDeletion(_ url: URL) throws {
|
|
try ManagedCodexHomeFactory(root: self.root).validateManagedHomeForDeletion(url)
|
|
}
|
|
}
|
|
|
|
private struct StubManagedCodexLoginRunner: ManagedCodexLoginRunning {
|
|
let result: CodexLoginRunner.Result
|
|
|
|
func run(homePath: String, timeout: TimeInterval) async -> CodexLoginRunner.Result {
|
|
self.result
|
|
}
|
|
|
|
static let success = StubManagedCodexLoginRunner(
|
|
result: CodexLoginRunner.Result(outcome: .success, output: "ok"))
|
|
}
|
|
|
|
private struct WritingManagedCodexLoginRunner: ManagedCodexLoginRunning {
|
|
let credentials: CodexOAuthCredentials
|
|
|
|
func run(homePath: String, timeout _: TimeInterval) async -> CodexLoginRunner.Result {
|
|
do {
|
|
try CodexOAuthCredentialsStore.save(self.credentials, env: ["CODEX_HOME": homePath])
|
|
return CodexLoginRunner.Result(outcome: .success, output: "ok")
|
|
} catch {
|
|
return CodexLoginRunner.Result(outcome: .failed(status: 1), output: String(describing: error))
|
|
}
|
|
}
|
|
}
|
|
|
|
private enum TestManagedCodexAccountStoreError: Error, Equatable {
|
|
case writeFailed
|
|
}
|
|
|
|
private final class StubManagedCodexIdentityReader: ManagedCodexIdentityReading, @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var identities: [CodexAuthBackedAccount]
|
|
|
|
init(identities: [CodexAuthBackedAccount]) {
|
|
self.identities = identities
|
|
}
|
|
|
|
func loadAccountIdentity(homePath _: String) throws -> CodexAuthBackedAccount {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
guard !self.identities.isEmpty else {
|
|
return CodexAuthBackedAccount(identity: .unresolved, email: nil, plan: nil)
|
|
}
|
|
return self.identities.removeFirst()
|
|
}
|
|
|
|
static func emails(_ emails: [String]) -> StubManagedCodexIdentityReader {
|
|
StubManagedCodexIdentityReader(identities: emails.map { email in
|
|
CodexAuthBackedAccount(
|
|
identity: CodexIdentityResolver.resolve(accountId: nil, email: email),
|
|
email: email,
|
|
plan: "Pro")
|
|
})
|
|
}
|
|
|
|
static func accounts(_ accounts: [CodexAuthBackedAccount]) -> StubManagedCodexIdentityReader {
|
|
StubManagedCodexIdentityReader(identities: accounts)
|
|
}
|
|
}
|
|
|
|
private struct StubManagedCodexWorkspaceResolver: ManagedCodexWorkspaceResolving {
|
|
let identities: [String: CodexOpenAIWorkspaceIdentity]
|
|
let availableIdentities: [CodexOpenAIWorkspaceIdentity]
|
|
|
|
init(
|
|
identities: [String: CodexOpenAIWorkspaceIdentity] = [:],
|
|
availableIdentities: [CodexOpenAIWorkspaceIdentity] = [])
|
|
{
|
|
self.identities = identities
|
|
self.availableIdentities = availableIdentities
|
|
}
|
|
|
|
func resolveWorkspaceIdentity(
|
|
homePath _: String,
|
|
providerAccountID: String) async -> CodexOpenAIWorkspaceIdentity?
|
|
{
|
|
self.identities[providerAccountID]
|
|
}
|
|
|
|
func availableWorkspaceIdentities(homePath _: String) async -> [CodexOpenAIWorkspaceIdentity] {
|
|
self.availableIdentities
|
|
}
|
|
}
|
|
|
|
private struct StubManagedCodexWorkspaceSelector: ManagedCodexWorkspaceSelecting {
|
|
let selectedWorkspaceID: String?
|
|
|
|
func selectWorkspace(
|
|
email _: String,
|
|
currentWorkspaceID _: String?,
|
|
workspaces: [CodexOpenAIWorkspaceIdentity]) async -> CodexOpenAIWorkspaceIdentity?
|
|
{
|
|
workspaces.first { $0.workspaceAccountID == self.selectedWorkspaceID }
|
|
}
|
|
}
|