Files
steipete--codexbar/Tests/CodexBarTests/CodexAccountPromotionExecutionTests.swift
T
2026-07-13 12:22:33 +08:00

311 lines
14 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
@Suite(.serialized)
@MainActor
struct CodexAccountPromotionExecutionTests {
@Test
func `executor import store failure cleans up imported home and maps managed store error`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-import-cleanup")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
try container.persistAccounts([target])
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
let context = try await self.makeContext(container: container, targetID: target.id)
let executor = CodexDisplacedLivePreservationExecutor(
store: RecordingManagedCodexAccountStore(base: container.fileStore) { _ in
throw PromotionTestError.storeWriteFailed
},
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
}
#expect(try container.managedHomeURLs().count == 1)
#expect(try container.loadAccounts().accounts.count == 1)
}
@Test
func `executor refresh failure leaves live auth untouched and keeps copied managed auth`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-refresh-failure")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
let existingManagedLive = try container.createManagedAccount(
persistedEmail: "alpha@example.com",
authAccountID: "acct-alpha")
try container.persistAccounts([target, existingManagedLive])
let originalManagedAuthData = try container.managedAuthData(for: existingManagedLive)
let liveAuthData = try container.writeLiveOAuthAuthFile(
email: "alpha@example.com",
accountID: "acct-alpha",
apiKey: "sk-refreshed-live")
let originalLiveAuthData = try #require(try container.liveAuthData())
let context = try await self.makeContext(container: container, targetID: target.id)
let plan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
let executor = CodexDisplacedLivePreservationExecutor(
store: RecordingManagedCodexAccountStore(base: container.fileStore) { accounts in
if accounts.account(id: existingManagedLive.id)?
.lastAuthenticatedAt != existingManagedLive.lastAuthenticatedAt
{
throw PromotionTestError.storeWriteFailed
}
},
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
try executor.execute(plan: plan, context: context)
}
let accounts = try container.loadAccounts().accounts
let persistedManagedLive = try #require(accounts.first(where: { $0.id == existingManagedLive.id }))
#expect(try container.liveAuthData() == originalLiveAuthData)
#expect(try container.managedAuthData(for: persistedManagedLive) != originalManagedAuthData)
#expect(try container.managedAuthData(for: persistedManagedLive) == liveAuthData)
}
@Test
func `executor import verifies persisted account after concurrent duplicate collision`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-import-collision-repair")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
let concurrentID = UUID()
let concurrentHomeURL = container.managedHomesURL.appendingPathComponent(
concurrentID.uuidString,
isDirectory: true)
try FileManager.default.createDirectory(at: concurrentHomeURL, withIntermediateDirectories: true)
let concurrentManaged = ManagedCodexAccount(
id: concurrentID,
email: "alpha@example.com",
providerAccountID: "acct-alpha",
workspaceLabel: "Personal",
workspaceAccountID: "acct-alpha",
managedHomePath: concurrentHomeURL.path,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: 1)
try container.persistAccounts([target])
let liveAuthData = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
let context = try await self.makeContext(container: container, targetID: target.id)
let executor = CodexDisplacedLivePreservationExecutor(
store: ConcurrentDuplicateManagedCodexAccountStore(
base: container.fileStore,
concurrentAccount: concurrentManaged),
homeFactory: container.homeFactory,
fileManager: .default)
let result = try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
#expect(result.displacedLiveDisposition == .alreadyManaged(managedAccountID: concurrentManaged.id))
let accounts = try container.loadAccounts().accounts
let repaired = try #require(accounts.first(where: { $0.id == concurrentManaged.id }))
#expect(repaired.managedHomePath != concurrentHomeURL.path)
#expect(try container.managedAuthData(for: repaired) == liveAuthData)
}
@Test
func `executor refresh filesystem failure maps to managed store error`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-refresh-filesystem-failure")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
let existingManagedLive = try container.createManagedAccount(
persistedEmail: "alpha@example.com",
authAccountID: "acct-alpha")
try container.persistAccounts([target, existingManagedLive])
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
let context = try await self.makeContext(container: container, targetID: target.id)
let plan = CodexDisplacedLivePreservationPlanner().makePlan(context: context)
let managedHomeURL = URL(fileURLWithPath: existingManagedLive.managedHomePath, isDirectory: true)
try FileManager.default.removeItem(at: managedHomeURL)
try Data("blocked".utf8).write(to: managedHomeURL)
let executor = CodexDisplacedLivePreservationExecutor(
store: container.fileStore,
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
try executor.execute(plan: plan, context: context)
}
}
@Test
func `executor legacy import repair ignores provider backed rows with the same email`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-legacy-import-provider-same-email")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
let providerManaged = try container.createManagedAccount(
persistedEmail: "alpha@example.com",
authAccountID: "acct-existing")
try container.persistAccounts([target, providerManaged])
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com")
let context = try await self.makeContext(container: container, targetID: target.id)
let originalProviderManaged = try #require(try container.loadAccounts().account(id: providerManaged.id))
let executor = CodexDisplacedLivePreservationExecutor(
store: DroppingLegacyImportedAccountStore(
base: container.fileStore,
preservedProviderBackedAccount: originalProviderManaged),
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
try executor.execute(plan: .importNew(reason: .noExistingManagedDestination), context: context)
}
let persistedProviderManaged = try #require(try container.loadAccounts().account(id: providerManaged.id))
#expect(persistedProviderManaged.providerAccountID == originalProviderManaged.providerAccountID)
#expect(persistedProviderManaged.managedHomePath == originalProviderManaged.managedHomePath)
}
@Test
func `executor reject preserves stable error mapping`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-reject-mapping")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
try container.persistAccounts([target])
let context = try await self.makeContext(container: container, targetID: target.id)
let executor = CodexDisplacedLivePreservationExecutor(
store: container.fileStore,
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.liveAccountAPIKeyOnlyUnsupported) {
try executor.execute(plan: .reject(reason: .liveAPIKeyOnlyUnsupported), context: context)
}
#expect(throws: CodexAccountPromotionError.liveAccountUnreadable) {
try executor.execute(plan: .reject(reason: .liveUnreadable), context: context)
}
#expect(throws: CodexAccountPromotionError.liveAccountMissingIdentityForPreservation) {
try executor.execute(plan: .reject(reason: .liveIdentityMissingForPreservation), context: context)
}
}
@Test
func `executor rejects target as preservation destination`() async throws {
let container = try CodexAccountPromotionTestContainer(
suiteName: "CodexAccountPromotionExecutionTests-target-destination")
defer { container.tearDown() }
let target = try container.createManagedAccount(
persistedEmail: "beta@example.com",
authAccountID: "acct-beta")
try container.persistAccounts([target])
_ = try container.writeLiveOAuthAuthFile(email: "alpha@example.com", accountID: "acct-alpha")
let context = try await self.makeContext(container: container, targetID: target.id)
let executor = CodexDisplacedLivePreservationExecutor(
store: container.fileStore,
homeFactory: container.homeFactory,
fileManager: .default)
#expect(throws: CodexAccountPromotionError.managedStoreCommitFailed) {
try executor.execute(
plan: .refreshExisting(
destination: context.target,
reason: .readableHomeIdentityMatch),
context: context)
}
}
private func makeContext(
container: CodexAccountPromotionTestContainer,
targetID: UUID)
async throws -> PreparedPromotionContext
{
let builder = PreparedPromotionContextBuilder(
store: container.fileStore,
workspaceResolver: container.workspaceResolver,
snapshotLoader: SettingsStoreCodexAccountReconciliationSnapshotLoader(settingsStore: container.settings),
authMaterialReader: DefaultCodexAuthMaterialReader(),
baseEnvironment: container.baseEnvironment,
fileManager: .default)
return try await builder.build(targetID: targetID)
}
}
private final class ConcurrentDuplicateManagedCodexAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
let base: any ManagedCodexAccountStoring
let concurrentAccount: ManagedCodexAccount
private var didInjectConcurrentAccount = false
init(base: any ManagedCodexAccountStoring, concurrentAccount: ManagedCodexAccount) {
self.base = base
self.concurrentAccount = concurrentAccount
}
func loadAccounts() throws -> ManagedCodexAccountSet {
if self.didInjectConcurrentAccount == false {
self.didInjectConcurrentAccount = true
let current = try self.base.loadAccounts()
try self.base.storeAccounts(ManagedCodexAccountSet(
version: current.version,
accounts: current.accounts + [self.concurrentAccount]))
}
return try self.base.loadAccounts()
}
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
try self.base.storeAccounts(accounts)
}
func ensureFileExists() throws -> URL {
try self.base.ensureFileExists()
}
}
private final class DroppingLegacyImportedAccountStore: ManagedCodexAccountStoring, @unchecked Sendable {
let base: any ManagedCodexAccountStoring
let preservedProviderBackedAccount: ManagedCodexAccount
init(base: any ManagedCodexAccountStoring, preservedProviderBackedAccount: ManagedCodexAccount) {
self.base = base
self.preservedProviderBackedAccount = preservedProviderBackedAccount
}
func loadAccounts() throws -> ManagedCodexAccountSet {
try self.base.loadAccounts()
}
func storeAccounts(_ accounts: ManagedCodexAccountSet) throws {
let filteredAccounts = accounts.accounts.filter {
$0.id == self.preservedProviderBackedAccount.id || $0.providerAccountID != nil
}
try self.base.storeAccounts(ManagedCodexAccountSet(
version: accounts.version,
accounts: filteredAccounts))
}
func ensureFileExists() throws -> URL {
try self.base.ensureFileExists()
}
}