906 lines
46 KiB
Swift
906 lines
46 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
struct UsageStorePlanUtilizationClaudeIdentityTests {
|
|
@MainActor
|
|
@Test
|
|
func `selected token account chooses matching bucket`() throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
store.settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token")
|
|
store.settings.addTokenAccount(provider: .claude, label: "Bob", token: "bob-token")
|
|
let accounts = store.settings.tokenAccounts(for: .claude)
|
|
let alice = try #require(accounts.first)
|
|
let bob = try #require(accounts.last)
|
|
let aliceKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: alice))
|
|
let bobKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: bob))
|
|
|
|
store.settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(accounts: [
|
|
aliceKey: [planSeries(name: .weekly, windowMinutes: 10080, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 20),
|
|
])],
|
|
bobKey: [planSeries(name: .weekly, windowMinutes: 10080, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_086_400), usedPercent: 50),
|
|
])],
|
|
])
|
|
|
|
#expect(store.planUtilizationHistory(for: .claude) == [
|
|
planSeries(name: .weekly, windowMinutes: 10080, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 20),
|
|
]),
|
|
])
|
|
|
|
store.settings.setActiveTokenAccountIndex(1, for: .claude)
|
|
#expect(store.planUtilizationHistory(for: .claude) == [
|
|
planSeries(name: .weekly, windowMinutes: 10080, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_086_400), usedPercent: 50),
|
|
]),
|
|
])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `fetched non selected accounts persist into separate claude buckets`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
store.settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token")
|
|
store.settings.addTokenAccount(provider: .claude, label: "Bob", token: "bob-token")
|
|
let accounts = store.settings.tokenAccounts(for: .claude)
|
|
let alice = try #require(accounts.first)
|
|
let bob = try #require(accounts.last)
|
|
let bobKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: bob))
|
|
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 20, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
tertiary: RateWindow(usedPercent: 30, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "bob@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "max"))
|
|
|
|
await store.recordFetchedTokenAccountPlanUtilizationHistory(
|
|
provider: .claude,
|
|
samples: [(account: bob, snapshot: snapshot)],
|
|
selectedAccount: alice)
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let histories = try #require(buckets.accounts[bobKey])
|
|
#expect(findSeries(histories, name: .session, windowMinutes: 300)?.entries.last?.usedPercent == 10)
|
|
#expect(findSeries(histories, name: .weekly, windowMinutes: 10080)?.entries.last?.usedPercent == 20)
|
|
#expect(findSeries(histories, name: .opus, windowMinutes: 10080)?.entries.last?.usedPercent == 30)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `first resolved claude token account adopts unscoped history`() throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
store.settings.addTokenAccount(provider: .claude, label: "Alice", token: "alice-token")
|
|
let alice = try #require(store.settings.tokenAccounts(for: .claude).first)
|
|
let aliceKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: alice))
|
|
let bootstrap = planSeries(name: .session, windowMinutes: 300, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 15),
|
|
])
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(unscoped: [bootstrap])
|
|
store.settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
|
|
let history = store.planUtilizationHistory(for: .claude)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
|
|
#expect(history == [bootstrap])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(buckets.accounts[aliceKey] == [bootstrap])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `claude oauth credential owner separates switched account history`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let accountBOwner = self.oauthOwnerIdentifier("b")
|
|
let accountASnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 10, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 20, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "alice@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "max"))
|
|
let accountAKey = try #require(
|
|
UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: accountASnapshot))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: accountASnapshot,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
|
|
let accountBSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 70, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: RateWindow(usedPercent: 80, windowMinutes: 10080, resetsAt: nil, resetDescription: nil),
|
|
updatedAt: Date())
|
|
let accountBKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: accountBOwner))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: accountBSnapshot,
|
|
claudeOAuthPersistentRefHash: "account-b-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: accountBOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_007_200))
|
|
store._setSnapshotForTesting(accountBSnapshot, provider: .claude)
|
|
|
|
let selectedHistory = store.planUtilizationHistory(for: .claude)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let accountAHistory = try #require(buckets.accounts[accountAKey])
|
|
let accountBHistory = try #require(buckets.accounts[accountBKey])
|
|
|
|
#expect(buckets.preferredAccountKey == accountBKey)
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(findSeries(accountAHistory, name: .session, windowMinutes: 300)?.entries.last?.usedPercent == 10)
|
|
#expect(findSeries(accountAHistory, name: .weekly, windowMinutes: 10080)?.entries.last?.usedPercent == 20)
|
|
#expect(findSeries(accountBHistory, name: .session, windowMinutes: 300)?.entries.last?.usedPercent == 70)
|
|
#expect(findSeries(accountBHistory, name: .weekly, windowMinutes: 10080)?.entries.last?.usedPercent == 80)
|
|
#expect(findSeries(selectedHistory, name: .session, windowMinutes: 300)?.entries.last?.usedPercent == 70)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `claude oauth credential owner wins over configured token account`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let oauthOwner = self.oauthOwnerIdentifier("a")
|
|
store.settings.addTokenAccount(provider: .claude, label: "Unrelated", token: "unrelated-token")
|
|
store.settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
let selectedAccount = try #require(store.settings.selectedTokenAccount(for: .claude))
|
|
let tokenAccountKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: selectedAccount))
|
|
let oauthAccountKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: oauthOwner))
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 45, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: snapshot,
|
|
claudeOAuthPersistentRefHash: "oauth-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: oauthOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
store._setSnapshotForTesting(snapshot, provider: .claude)
|
|
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(buckets.preferredAccountKey == oauthAccountKey)
|
|
#expect(buckets.accounts[tokenAccountKey] == nil)
|
|
#expect(findSeries(buckets.accounts[oauthAccountKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [45])
|
|
#expect(selection.accountKey == oauthAccountKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [45])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `claude oauth without credential ownership is not persisted`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
store.settings.addTokenAccount(provider: .claude, label: "Unrelated", token: "unrelated-token")
|
|
store.settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
let selectedAccount = try #require(store.settings.selectedTokenAccount(for: .claude))
|
|
let tokenAccountKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: selectedAccount))
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 55, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: snapshot,
|
|
claudeOAuthPersistentRefHash: "row-only-ref",
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
store._setSnapshotForTesting(snapshot, provider: .claude)
|
|
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(buckets.accounts.isEmpty)
|
|
#expect(selection.accountKey == tokenAccountKey)
|
|
#expect(selection.histories.isEmpty)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `coalesced claude oauth sample still switches preferred account`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let accountAOwner = self.oauthOwnerIdentifier("a")
|
|
let accountBOwner = self.oauthOwnerIdentifier("b")
|
|
let accountAKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: accountAOwner))
|
|
let accountBKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: accountBOwner))
|
|
let hourStart = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 70),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: accountAOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart)
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 40),
|
|
claudeOAuthPersistentRefHash: "account-b-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: accountBOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(5 * 60))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 60),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: accountAOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(10 * 60))
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
#expect(buckets.preferredAccountKey == accountAKey)
|
|
#expect(findSeries(buckets.accounts[accountAKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [70])
|
|
#expect(findSeries(buckets.accounts[accountBKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [40])
|
|
#expect(selection.accountKey == accountAKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [70])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `same dir account switch quarantines stale background credential`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let ownerA = self.oauthOwnerIdentifier("a")
|
|
let ownerB = self.oauthOwnerIdentifier("b")
|
|
let keyA = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: ownerA))
|
|
let keyB = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: ownerB))
|
|
let hourStart = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
// 1) Active account A (~/.claude.json = uuid-A). Two stable exact-Keychain observations bind owner_A
|
|
// to A; the short confirmation interval does not add a second history point.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-A") {
|
|
await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 30),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart)
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 30),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(30 * 60))
|
|
}
|
|
}
|
|
|
|
do {
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(findSeries(buckets.accounts[keyA] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [30])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
}
|
|
|
|
// 2) `/login` switched the active account to B (~/.claude.json = uuid-B), but the gated BACKGROUND
|
|
// poll still serves the STALE owner_A credential. Prompt-free Keychain comparison is unavailable,
|
|
// but the existing owner_A -> uuid-A binding detects the mismatch, so this must be quarantined:
|
|
// no new sample lands.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-B") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 90),
|
|
claudeOAuthPersistentRefHash: nil,
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthKeychainCredentialUnavailable: true,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(2 * 60 * 60))
|
|
}
|
|
}
|
|
|
|
do {
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
// key_A history is UNCHANGED (the stale sample was dropped, not appended).
|
|
#expect(findSeries(buckets.accounts[keyA] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [30])
|
|
// Critically, the quarantined sample did NOT leak into the shared unscoped bucket. This is the
|
|
// assertion that catches the naive-nil bug (nil accountKey writes to `unscoped`, not nowhere).
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(buckets.accounts[keyB] == nil)
|
|
}
|
|
|
|
// 3) A USER-INITIATED refresh yields account B's real credential (owner_B), with exact current-Keychain
|
|
// match evidence. Recovery: bind owner_B -> uuid-B, write to key_B, and leave key_A untouched.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-B") {
|
|
await ProviderInteractionContext.$current.withValue(.userInitiated) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 75),
|
|
claudeOAuthPersistentRefHash: "account-b-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerB,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(3 * 60 * 60))
|
|
}
|
|
}
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(findSeries(buckets.accounts[keyB] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [75])
|
|
#expect(findSeries(buckets.accounts[keyA] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [30])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `exact keychain match arms account map on background poll`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let ownerA = self.oauthOwnerIdentifier("a")
|
|
let ownerB = self.oauthOwnerIdentifier("b")
|
|
let keyA = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: ownerA))
|
|
let keyB = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: ownerB))
|
|
let hourStart = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
// 1) First run after upgrading: the map is empty. A background poll serves owner_A while
|
|
// ~/.claude.json reports uuid-A, and exact current-Keychain evidence corroborates the credential.
|
|
// The first observation stages a candidate; a later identical observation confirms the binding.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-A") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 30),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart)
|
|
}
|
|
}
|
|
|
|
let accountAIdentity = UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")
|
|
let accountBIdentity = UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")
|
|
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults)[ownerA] == nil)
|
|
#expect(UsageStore.loadClaudeOAuthAccountBindingCandidateMap(
|
|
from: store.settings.userDefaults)[ownerA]?.identity == accountAIdentity)
|
|
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-A") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 30),
|
|
claudeOAuthPersistentRefHash: "account-a-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthActiveAccountObservation: .stable(identity: accountAIdentity),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(30 * 60))
|
|
}
|
|
}
|
|
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(
|
|
from: store.settings.userDefaults)[ownerA] == accountAIdentity)
|
|
|
|
// 2) `/login` switched to account B (~/.claude.json = uuid-B), but the gated BACKGROUND poll still
|
|
// serves the stale owner_A credential. It no longer matches the current Keychain item, and the
|
|
// existing owner_A -> uuid-A binding detects the UUID mismatch, so the sample is quarantined.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-B") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 90),
|
|
claudeOAuthPersistentRefHash: nil,
|
|
claudeOAuthHistoryOwnerIdentifier: ownerA,
|
|
claudeOAuthKeychainCredentialMismatch: true,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(2 * 60 * 60))
|
|
}
|
|
}
|
|
|
|
do {
|
|
let mapAfterBackground = UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults)
|
|
#expect(mapAfterBackground[ownerA] == accountAIdentity)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(findSeries(buckets.accounts[keyA] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [30])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
}
|
|
|
|
// 3) A background poll now yields account B's real credential (owner_B) with exact current-Keychain
|
|
// match evidence. That evidence, not the interaction label, binds owner_B -> uuid-B and writes key_B.
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-B") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 75),
|
|
claudeOAuthPersistentRefHash: "account-b-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerB,
|
|
claudeOAuthActiveAccountObservation: .stable(
|
|
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(3 * 60 * 60))
|
|
}
|
|
}
|
|
|
|
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults)[ownerB] == nil)
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting("uuid-B") {
|
|
await ProviderInteractionContext.$current.withValue(.background) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 75),
|
|
claudeOAuthPersistentRefHash: "account-b-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: ownerB,
|
|
claudeOAuthActiveAccountObservation: .stable(identity: accountBIdentity),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(3 * 60 * 60 + 30 * 60))
|
|
}
|
|
}
|
|
|
|
let mapAfterRecovery = UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults)
|
|
#expect(mapAfterRecovery[ownerB] == accountBIdentity)
|
|
#expect(mapAfterRecovery[ownerA] == accountAIdentity)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(findSeries(buckets.accounts[keyB] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [75])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `coalesced claude oauth sample without owner cannot switch preferred account`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let scopedOwner = self.oauthOwnerIdentifier("c")
|
|
let scopedAccountKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: scopedOwner))
|
|
let hourStart = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 70),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart)
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 40),
|
|
claudeOAuthPersistentRefHash: "scoped-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: scopedOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(5 * 60))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 60),
|
|
isClaudeOAuthSample: true,
|
|
now: hourStart.addingTimeInterval(10 * 60))
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
#expect(buckets.preferredAccountKey == scopedAccountKey)
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(findSeries(buckets.accounts[scopedAccountKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [40])
|
|
#expect(selection.accountKey == scopedAccountKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [40])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `reloaded scoped claude oauth preference wins over configured token account`() throws {
|
|
let oauthOwner = self.oauthOwnerIdentifier("a")
|
|
let oauthAccountKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: oauthOwner))
|
|
let oauthHistory = planSeries(name: .session, windowMinutes: 300, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 45),
|
|
])
|
|
let store = self.makeReloadedStoreWithConfiguredTokenAccount(
|
|
buckets: PlanUtilizationHistoryBuckets(
|
|
preferredAccountKey: oauthAccountKey,
|
|
accounts: [oauthAccountKey: [oauthHistory]]))
|
|
|
|
#expect(store.lastSourceLabels[.claude] == nil)
|
|
#expect(store.settings.selectedTokenAccount(for: .claude) != nil)
|
|
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
|
|
#expect(selection.accountKey == oauthAccountKey)
|
|
#expect(selection.histories == [oauthHistory])
|
|
#expect(store.planUtilizationHistory[.claude]?.preferredAccountKey == oauthAccountKey)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `reloaded unscoped claude oauth preference wins over configured token account`() {
|
|
let oauthHistory = planSeries(name: .session, windowMinutes: 300, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 55),
|
|
])
|
|
let store = self.makeReloadedStoreWithConfiguredTokenAccount(
|
|
buckets: PlanUtilizationHistoryBuckets(
|
|
preferredAccountKey: "__unscoped__",
|
|
unscoped: [oauthHistory]))
|
|
|
|
#expect(store.lastSourceLabels[.claude] == nil)
|
|
#expect(store.settings.selectedTokenAccount(for: .claude) != nil)
|
|
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
|
|
#expect(selection.accountKey == nil)
|
|
#expect(selection.histories == [oauthHistory])
|
|
#expect(store.planUtilizationHistory[.claude]?.preferredAccountKey == "__unscoped__")
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `later token account sample supersedes claude oauth preference`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let oauthOwner = self.oauthOwnerIdentifier("a")
|
|
store.settings.addTokenAccount(provider: .claude, label: "Selected", token: "selected-token")
|
|
store.settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
let selectedAccount = try #require(store.settings.selectedTokenAccount(for: .claude))
|
|
let tokenAccountKey = try #require(
|
|
UsageStore._planUtilizationTokenAccountKeyForTesting(provider: .claude, account: selectedAccount))
|
|
let oauthAccountKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: oauthOwner))
|
|
let oauthSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 45, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: oauthSnapshot,
|
|
claudeOAuthPersistentRefHash: "oauth-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: oauthOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
|
|
let tokenSnapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 20, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: tokenSnapshot,
|
|
account: selectedAccount,
|
|
now: Date(timeIntervalSince1970: 1_700_007_200))
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let selection = store.planUtilizationHistorySelection(for: .claude)
|
|
#expect(buckets.preferredAccountKey == tokenAccountKey)
|
|
#expect(findSeries(buckets.accounts[oauthAccountKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [45])
|
|
#expect(selection.accountKey == tokenAccountKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [20])
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `first claude oauth owner quarantines legacy unscoped history`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let currentOwner = self.oauthOwnerIdentifier("c")
|
|
let legacy = planSeries(name: .session, windowMinutes: 300, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 25),
|
|
])
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(unscoped: [legacy])
|
|
|
|
let snapshot = UsageSnapshot(
|
|
primary: RateWindow(usedPercent: 60, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
let accountKey = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: currentOwner))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: snapshot,
|
|
claudeOAuthPersistentRefHash: "current-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: currentOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_007_200))
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
let scoped = try #require(buckets.accounts[accountKey])
|
|
#expect(buckets.unscoped == [legacy])
|
|
#expect(findSeries(scoped, name: .session, windowMinutes: 300)?.entries.map(\.usedPercent) == [60])
|
|
#expect(buckets.preferredAccountKey == accountKey)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `provenance-less claude oauth credentials stay isolated across restart`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let accountAOwner = self.oauthOwnerIdentifier("a")
|
|
let accountBOwner = self.oauthOwnerIdentifier("b")
|
|
let accountAKey = try #require(UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: accountAOwner))
|
|
let accountBKey = try #require(UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: accountBOwner))
|
|
|
|
await UsageStore.withActiveClaudeAccountUuidForTesting(nil) {
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 10),
|
|
claudeOAuthHistoryOwnerIdentifier: accountAOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 75),
|
|
claudeOAuthHistoryOwnerIdentifier: accountBOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_007_200))
|
|
}
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(buckets.unscoped.isEmpty)
|
|
#expect(findSeries(buckets.accounts[accountAKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [10])
|
|
#expect(findSeries(buckets.accounts[accountBKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [75])
|
|
|
|
let reloaded = self.makeReloadedStoreWithConfiguredTokenAccount(buckets: buckets)
|
|
let selection = reloaded.planUtilizationHistorySelection(for: .claude)
|
|
#expect(selection.accountKey == accountBKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [75])
|
|
#expect(reloaded.planUtilizationHistory[.claude]?.accounts[accountAKey] != nil)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `same keychain reference credential replacement stays isolated across restart`() async throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let originalOwner = self.oauthOwnerIdentifier("c")
|
|
let replacementOwner = self.oauthOwnerIdentifier("d")
|
|
let originalKey = try #require(UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: originalOwner,
|
|
persistentRefHash: "same-row-ref"))
|
|
let replacementKey = try #require(UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: replacementOwner,
|
|
persistentRefHash: "same-row-ref"))
|
|
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 25),
|
|
claudeOAuthPersistentRefHash: "same-row-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: originalOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_000_000))
|
|
await store.recordPlanUtilizationHistorySample(
|
|
provider: .claude,
|
|
snapshot: self.identitylessClaudeSnapshot(usedPercent: 80),
|
|
claudeOAuthPersistentRefHash: "same-row-ref",
|
|
claudeOAuthHistoryOwnerIdentifier: replacementOwner,
|
|
isClaudeOAuthSample: true,
|
|
now: Date(timeIntervalSince1970: 1_700_007_200))
|
|
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
#expect(originalKey != replacementKey)
|
|
#expect(findSeries(buckets.accounts[originalKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [25])
|
|
#expect(findSeries(buckets.accounts[replacementKey] ?? [], name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [80])
|
|
|
|
let reloaded = self.makeReloadedStoreWithConfiguredTokenAccount(buckets: buckets)
|
|
let selection = reloaded.planUtilizationHistorySelection(for: .claude)
|
|
#expect(selection.accountKey == replacementKey)
|
|
#expect(findSeries(selection.histories, name: .session, windowMinutes: 300)?
|
|
.entries.map(\.usedPercent) == [80])
|
|
#expect(reloaded.planUtilizationHistory[.claude]?.accounts[originalKey] != nil)
|
|
}
|
|
|
|
@Test
|
|
func `claude oauth history key is stable for one credential owner`() throws {
|
|
let owner = self.oauthOwnerIdentifier("a")
|
|
let first = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
|
|
let refreshed = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: " \(owner.uppercased()) "))
|
|
let switched = try #require(
|
|
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(
|
|
historyOwnerIdentifier: self.oauthOwnerIdentifier("b")))
|
|
|
|
#expect(first == refreshed)
|
|
#expect(first != switched)
|
|
#expect(first != owner)
|
|
#expect(first.hasPrefix("__claude_oauth__:"))
|
|
#expect(first.dropFirst("__claude_oauth__:".count).count == 64)
|
|
}
|
|
|
|
@Test
|
|
func `same claude email separates team and personal plan history keys`() throws {
|
|
let team = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: "Team Org",
|
|
loginMethod: "Claude Team"))
|
|
let max = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "Claude Max"))
|
|
|
|
let teamKey = try #require(UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: team))
|
|
let maxKey = try #require(UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: max))
|
|
|
|
#expect(teamKey != maxKey)
|
|
}
|
|
|
|
@Test
|
|
func `claude email only identity keeps legacy history key`() throws {
|
|
let snapshot = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: nil))
|
|
|
|
let identityKey = try #require(
|
|
UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: snapshot))
|
|
let legacyKey = try #require(
|
|
UsageStore._legacyClaudePlanUtilizationEmailAccountKeyForTesting(snapshot: snapshot))
|
|
|
|
#expect(identityKey == legacyKey)
|
|
}
|
|
|
|
@Test
|
|
func `claude compact and branded plan labels share history key`() throws {
|
|
let compact = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "Max"))
|
|
let branded = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: nil,
|
|
loginMethod: "Claude Max"))
|
|
|
|
let compactKey = try #require(
|
|
UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: compact))
|
|
let brandedKey = try #require(
|
|
UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: branded))
|
|
|
|
#expect(compactKey == brandedKey)
|
|
}
|
|
|
|
@MainActor
|
|
@Test
|
|
func `new claude email discriminator adopts legacy email history`() throws {
|
|
let store = UsageStorePlanUtilizationTests.makeStore()
|
|
let snapshot = UsageSnapshot(
|
|
primary: nil,
|
|
secondary: nil,
|
|
updatedAt: Date(),
|
|
identity: ProviderIdentitySnapshot(
|
|
providerID: .claude,
|
|
accountEmail: "person@example.com",
|
|
accountOrganization: "Team Org",
|
|
loginMethod: "Claude Team"))
|
|
let legacyKey = try #require(
|
|
UsageStore._legacyClaudePlanUtilizationEmailAccountKeyForTesting(snapshot: snapshot))
|
|
let accountKey = try #require(
|
|
UsageStore._planUtilizationAccountKeyForTesting(provider: .claude, snapshot: snapshot))
|
|
let legacyWeekly = planSeries(name: .weekly, windowMinutes: 10080, entries: [
|
|
planEntry(at: Date(timeIntervalSince1970: 1_700_000_000), usedPercent: 42),
|
|
])
|
|
store.planUtilizationHistory[.claude] = PlanUtilizationHistoryBuckets(
|
|
preferredAccountKey: legacyKey,
|
|
accounts: [
|
|
legacyKey: [legacyWeekly],
|
|
])
|
|
store._setSnapshotForTesting(snapshot, provider: .claude)
|
|
|
|
let history = store.planUtilizationHistory(for: .claude)
|
|
let buckets = try #require(store.planUtilizationHistory[.claude])
|
|
|
|
#expect(history == [legacyWeekly])
|
|
#expect(buckets.accounts[legacyKey] == nil)
|
|
#expect(buckets.accounts[accountKey] == [legacyWeekly])
|
|
#expect(buckets.preferredAccountKey == accountKey)
|
|
}
|
|
|
|
private func oauthOwnerIdentifier(_ character: Character) -> String {
|
|
String(repeating: String(character), count: 64)
|
|
}
|
|
|
|
private func identitylessClaudeSnapshot(usedPercent: Double) -> UsageSnapshot {
|
|
UsageSnapshot(
|
|
primary: RateWindow(
|
|
usedPercent: usedPercent,
|
|
windowMinutes: 300,
|
|
resetsAt: nil,
|
|
resetDescription: nil),
|
|
secondary: nil,
|
|
updatedAt: Date())
|
|
}
|
|
|
|
@MainActor
|
|
private func makeReloadedStoreWithConfiguredTokenAccount(
|
|
buckets: PlanUtilizationHistoryBuckets) -> UsageStore
|
|
{
|
|
let suiteName = "UsageStorePlanUtilizationClaudeReload-\(UUID().uuidString)"
|
|
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
|
fatalError("Failed to create isolated UserDefaults suite for tests")
|
|
}
|
|
defaults.removePersistentDomain(forName: suiteName)
|
|
let historyStore = testPlanUtilizationHistoryStore(suiteName: suiteName)
|
|
historyStore.save([.claude: buckets])
|
|
let settings = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suiteName),
|
|
tokenAccountStore: InMemoryTokenAccountStore())
|
|
settings.addTokenAccount(provider: .claude, label: "Unrelated", token: "unrelated-token")
|
|
settings.setActiveTokenAccountIndex(0, for: .claude)
|
|
let store = UsageStore(
|
|
fetcher: UsageFetcher(),
|
|
browserDetection: BrowserDetection(cacheTTL: 0),
|
|
settings: settings,
|
|
planUtilizationHistoryStore: historyStore,
|
|
startupBehavior: .testing)
|
|
// Cancel the background decode and apply the disk-loaded buckets
|
|
// synchronously so callers can immediately query without racing the
|
|
// utility-priority load task.
|
|
store._cancelPlanUtilizationHistoryLoadForTesting()
|
|
store.planUtilizationHistory = store.planUtilizationHistoryStore.load()
|
|
return store
|
|
}
|
|
}
|