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

306 lines
13 KiB
Swift

import CodexBarCore
import Foundation
import Testing
@testable import CodexBar
struct UsageStorePlanUtilizationClaudeIdentityBoundaryTests {
@MainActor
@Test
func `claude history without identity falls back to last resolved account`() async {
let store = UsageStorePlanUtilizationTests.makeStore()
let snapshot = 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"))
store._setSnapshotForTesting(snapshot, provider: .claude)
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: snapshot,
now: Date(timeIntervalSince1970: 1_700_000_000))
let identitylessSnapshot = UsageSnapshot(
primary: snapshot.primary,
secondary: snapshot.secondary,
updatedAt: snapshot.updatedAt)
store._setSnapshotForTesting(identitylessSnapshot, provider: .claude)
let history = store.planUtilizationHistory(for: .claude)
#expect(findSeries(history, name: .session, windowMinutes: 300)?.entries.last?.usedPercent == 10)
#expect(findSeries(history, name: .weekly, windowMinutes: 10080)?.entries.last?.usedPercent == 20)
}
@MainActor
@Test
func `established account accepts same owner after access token rotation`() async throws {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "a", count: 64)
let accountIdentity = UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")
let start = Date(timeIntervalSince1970: 1_700_000_000)
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 30),
claudeOAuthPersistentRefHash: "account-a-ref",
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthActiveAccountObservation: .stable(identity: accountIdentity),
isClaudeOAuthSample: true,
now: start)
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 30),
claudeOAuthPersistentRefHash: "account-a-ref",
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthActiveAccountObservation: .stable(identity: accountIdentity),
isClaudeOAuthSample: true,
now: start.addingTimeInterval(30 * 60))
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 50),
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthKeychainCredentialMismatch: true,
claudeOAuthActiveAccountObservation: .stable(identity: accountIdentity),
isClaudeOAuthSample: true,
now: start.addingTimeInterval(2 * 60 * 60))
let key = try #require(
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
let buckets = try #require(store.planUtilizationHistory[.claude])
#expect(findSeries(buckets.accounts[key] ?? [], name: .session, windowMinutes: 300)?
.entries.map(\.usedPercent) == [30, 50])
}
@MainActor
@Test
func `first sighting without keychain match is quarantined`() async {
let store = UsageStorePlanUtilizationTests.makeStore()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 90,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date())
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: snapshot,
claudeOAuthHistoryOwnerIdentifier: String(repeating: "s", count: 64),
claudeOAuthKeychainCredentialMismatch: true,
claudeOAuthActiveAccountObservation: .stable(
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-current")),
isClaudeOAuthSample: true)
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults).isEmpty)
#expect(store.planUtilizationHistory[.claude] == nil)
}
@MainActor
@Test
func `file backed owner records history when keychain comparison is unavailable`() async throws {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "e", count: 64)
let key = try #require(
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 90),
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthKeychainCredentialUnavailable: true,
claudeOAuthActiveAccountObservation: .stable(
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-current")),
isClaudeOAuthSample: true)
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults).isEmpty)
#expect(UsageStore.loadClaudeOAuthAccountBindingCandidateMap(
from: store.settings.userDefaults).isEmpty)
let buckets = try #require(store.planUtilizationHistory[.claude])
#expect(findSeries(buckets.accounts[key] ?? [], name: .session, windowMinutes: 300)?
.entries.map(\.usedPercent) == [90])
}
@MainActor
@Test
func `absent keychain still quarantines an owner bound to another account`() async {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "f", count: 64)
store.persistClaudeOAuthAccountUuidMap([
owner: UsageStore._activeClaudeAccountIdentityForTesting("uuid-A"),
])
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 90),
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthKeychainCredentialAbsent: true,
claudeOAuthActiveAccountObservation: .stable(
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")),
isClaudeOAuthSample: true)
#expect(store.planUtilizationHistory[.claude] == nil)
}
@MainActor
@Test
func `absent keychain records an unbound file owner`() async throws {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "b", count: 64)
let key = try #require(
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: self.snapshot(usedPercent: 80),
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthKeychainCredentialAbsent: true,
claudeOAuthActiveAccountObservation: .stable(
identity: UsageStore._activeClaudeAccountIdentityForTesting("uuid-current")),
isClaudeOAuthSample: true)
let buckets = try #require(store.planUtilizationHistory[.claude])
#expect(findSeries(buckets.accounts[key] ?? [], name: .session, windowMinutes: 300)?
.entries.map(\.usedPercent) == [80])
}
@MainActor
@Test
func `account change during identity capture cannot bind or write history`() async {
let store = UsageStorePlanUtilizationTests.makeStore()
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 90,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date())
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: snapshot,
claudeOAuthPersistentRefHash: "account-a-ref",
claudeOAuthHistoryOwnerIdentifier: String(repeating: "r", count: 64),
claudeOAuthActiveAccountObservation: .changed,
isClaudeOAuthSample: true)
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults).isEmpty)
#expect(store.planUtilizationHistory[.claude] == nil)
}
@MainActor
@Test
func `missing active account identity preserves owner scoped history`() async throws {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "c", count: 64)
let key = try #require(
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 35,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date())
await UsageStore.withActiveClaudeAccountUuidForTesting(nil) {
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: snapshot,
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthActiveAccountObservation: .stable(identity: nil),
isClaudeOAuthSample: true)
}
let buckets = try #require(store.planUtilizationHistory[.claude])
#expect(findSeries(buckets.accounts[key] ?? [], name: .session, windowMinutes: 300)?
.entries.map(\.usedPercent) == [35])
}
@MainActor
@Test
func `explicit oauth credential ignores Claude Code account identity`() async throws {
let store = UsageStorePlanUtilizationTests.makeStore()
let owner = String(repeating: "d", count: 64)
let key = try #require(
UsageStore._claudeOAuthPlanUtilizationAccountKeyForTesting(historyOwnerIdentifier: owner))
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 45,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date())
await UsageStore.withActiveClaudeAccountUuidForTesting("claude-code-account") {
await store.recordPlanUtilizationHistorySample(
provider: .claude,
snapshot: snapshot,
claudeOAuthHistoryOwnerIdentifier: owner,
claudeOAuthActiveAccountObservation: .changed,
isClaudeOAuthSample: true)
}
let buckets = try #require(store.planUtilizationHistory[.claude])
#expect(findSeries(buckets.accounts[key] ?? [], name: .session, windowMinutes: 300)?
.entries.map(\.usedPercent) == [45])
#expect(UsageStore.loadClaudeOAuthAccountUuidMap(from: store.settings.userDefaults).isEmpty)
}
@Test
func `claude oauth history scope requires full auth fingerprint stability`() {
let stablePersistentRefHash = UsageStore._stableClaudeKeychainPersistentRefHashForTesting(
beforeFetchFingerprintToken: "stable-fingerprint",
afterFetchFingerprintToken: "stable-fingerprint",
beforeFetchPersistentRefHash: "stable-ref",
afterFetchPersistentRefHash: "stable-ref")
let changedFingerprintPersistentRefHash = UsageStore._stableClaudeKeychainPersistentRefHashForTesting(
beforeFetchFingerprintToken: "before-fingerprint",
afterFetchFingerprintToken: "after-fingerprint",
beforeFetchPersistentRefHash: "stable-ref",
afterFetchPersistentRefHash: "stable-ref")
#expect(stablePersistentRefHash == "stable-ref")
#expect(changedFingerprintPersistentRefHash == nil)
}
@Test
func `credential change around account read invalidates the observation`() {
let identityA = UsageStore._activeClaudeAccountIdentityForTesting("uuid-A")
let identityB = UsageStore._activeClaudeAccountIdentityForTesting("uuid-B")
let stable = UsageStore._claudeOAuthActiveAccountObservationForTesting(
identityBeforeFetch: identityB,
identityAfterFetch: identityB)
let changed = UsageStore._claudeOAuthActiveAccountObservationForTesting(
identityBeforeFetch: identityA,
identityAfterFetch: identityB)
let unstable = UsageStore._claudeOAuthActiveAccountObservationForTesting(
identityBeforeFetch: identityB,
identityAfterFetch: identityB,
beforeFetchWasStable: false)
#expect(stable == .stable(identity: identityB))
#expect(changed == .changed)
#expect(unstable == .changed)
}
private func snapshot(usedPercent: Double) -> UsageSnapshot {
UsageSnapshot(
primary: RateWindow(
usedPercent: usedPercent,
windowMinutes: 300,
resetsAt: nil,
resetDescription: nil),
secondary: nil,
updatedAt: Date())
}
}