145 lines
5.9 KiB
Swift
145 lines
5.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodexBarCore
|
|
|
|
struct ClaudeSwapAccountProjectionTests {
|
|
@Test
|
|
func `adapter failures mark retained account snapshots as stale`() {
|
|
#expect(ClaudeSwapAccountProjection.displayError(
|
|
accountError: nil,
|
|
adapterError: "timed out") == "Showing the last successful update: timed out")
|
|
#expect(ClaudeSwapAccountProjection.displayError(
|
|
accountError: "Token expired.",
|
|
adapterError: "timed out") == "Token expired.")
|
|
#expect(ClaudeSwapAccountProjection.displayError(
|
|
accountError: nil,
|
|
adapterError: "timed out",
|
|
switchError: "store locked") == "Account switch failed: store locked")
|
|
#expect(ClaudeSwapAccountProjection.displayError(
|
|
accountError: "API-key account",
|
|
adapterError: nil,
|
|
switchError: "store locked") == "Account switch failed: store locked")
|
|
}
|
|
|
|
private let now = Date(timeIntervalSince1970: 1_782_000_000)
|
|
|
|
@Test
|
|
func `projects rows into provider neutral snapshots with active account first`() throws {
|
|
let reset = Date(timeIntervalSince1970: 1_782_170_999)
|
|
let list = ClaudeSwapAccountList(
|
|
activeAccountNumber: 2,
|
|
accounts: [
|
|
ClaudeSwapAccountRow(
|
|
number: 1,
|
|
email: "work@example.com",
|
|
isActive: false,
|
|
usageStatus: .ok,
|
|
fiveHour: ClaudeSwapUsageWindow(usedPercent: 25, resetsAt: reset),
|
|
sevenDay: ClaudeSwapUsageWindow(usedPercent: 16.5, resetsAt: nil)),
|
|
ClaudeSwapAccountRow(
|
|
number: 2,
|
|
email: "personal@example.com",
|
|
isActive: true,
|
|
usageStatus: .ok,
|
|
fiveHour: ClaudeSwapUsageWindow(usedPercent: 80, resetsAt: nil),
|
|
sevenDay: nil),
|
|
])
|
|
|
|
let snapshots = ClaudeSwapAccountProjection.accountSnapshots(from: list, now: self.now)
|
|
#expect(snapshots.count == 2)
|
|
|
|
let active = try #require(snapshots.first)
|
|
#expect(active.id == ProviderAccountIdentity(source: "claude-swap", opaqueID: "2"))
|
|
#expect(active.provider == .claude)
|
|
#expect(active.displayLabel == "personal@example.com")
|
|
#expect(active.isActive == true)
|
|
#expect(active.canActivate == false)
|
|
#expect(active.error == nil)
|
|
#expect(active.sourceLabel == "claude-swap")
|
|
#expect(active.snapshot?.primary?.usedPercent == 80)
|
|
#expect(active.snapshot?.primary?.windowMinutes == 300)
|
|
#expect(active.snapshot?.secondary == nil)
|
|
#expect(active.snapshot?.updatedAt == self.now)
|
|
#expect(active.snapshot?.identity?.accountEmail == "personal@example.com")
|
|
#expect(active.snapshot?.identity?.loginMethod == "claude-swap")
|
|
|
|
let inactive = try #require(snapshots.last)
|
|
#expect(inactive.id.opaqueID == "1")
|
|
#expect(inactive.isActive == false)
|
|
#expect(inactive.canActivate == true)
|
|
#expect(inactive.snapshot?.primary?.resetsAt == reset)
|
|
#expect(inactive.snapshot?.secondary?.usedPercent == 16.5)
|
|
#expect(inactive.snapshot?.secondary?.windowMinutes == 10080)
|
|
}
|
|
|
|
@Test
|
|
func `maps sentinel statuses to per account errors without usage`() throws {
|
|
let rows: [(ClaudeSwapUsageStatus, String)] = [
|
|
(.tokenExpired, "Token expired"),
|
|
(.apiKey, "API-key account"),
|
|
(.keychainUnavailable, "Keychain"),
|
|
(.noCredentials, "No stored credentials"),
|
|
(.unavailable, "Usage fetch failed"),
|
|
(.unknown("mystery"), "mystery"),
|
|
]
|
|
|
|
for (index, entry) in rows.enumerated() {
|
|
let list = ClaudeSwapAccountList(
|
|
activeAccountNumber: nil,
|
|
accounts: [
|
|
ClaudeSwapAccountRow(
|
|
number: index + 1,
|
|
email: "a@b.c",
|
|
isActive: false,
|
|
usageStatus: entry.0,
|
|
fiveHour: nil,
|
|
sevenDay: nil),
|
|
])
|
|
let snapshot = try #require(
|
|
ClaudeSwapAccountProjection.accountSnapshots(from: list, now: self.now).first)
|
|
#expect(snapshot.snapshot == nil)
|
|
let error = try #require(snapshot.error)
|
|
#expect(error.contains(entry.1))
|
|
let expectedCanActivate = entry.0 == .apiKey || entry.0 == .unavailable
|
|
#expect(snapshot.canActivate == expectedCanActivate)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `ok row without windows reports missing usage instead of an empty card`() throws {
|
|
let list = ClaudeSwapAccountList(
|
|
activeAccountNumber: 1,
|
|
accounts: [
|
|
ClaudeSwapAccountRow(
|
|
number: 1,
|
|
email: "a@b.c",
|
|
isActive: true,
|
|
usageStatus: .ok,
|
|
fiveHour: nil,
|
|
sevenDay: nil),
|
|
])
|
|
|
|
let snapshot = try #require(ClaudeSwapAccountProjection.accountSnapshots(from: list, now: self.now).first)
|
|
#expect(snapshot.snapshot == nil)
|
|
#expect(snapshot.error == "No usage windows reported.")
|
|
}
|
|
|
|
@Test
|
|
func `falls back to ordinal label when email is empty`() throws {
|
|
let list = ClaudeSwapAccountList(
|
|
activeAccountNumber: nil,
|
|
accounts: [
|
|
ClaudeSwapAccountRow(
|
|
number: 3,
|
|
email: "",
|
|
isActive: false,
|
|
usageStatus: .noCredentials,
|
|
fiveHour: nil,
|
|
sevenDay: nil),
|
|
])
|
|
|
|
let snapshot = try #require(ClaudeSwapAccountProjection.accountSnapshots(from: list, now: self.now).first)
|
|
#expect(snapshot.displayLabel == "Account 3")
|
|
}
|
|
}
|