1763 lines
72 KiB
Swift
1763 lines
72 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Observation
|
|
import Testing
|
|
@testable import CodexBar
|
|
|
|
@Suite(.serialized)
|
|
@MainActor
|
|
// swiftlint:disable:next type_body_length
|
|
struct SettingsStoreTests {
|
|
private final class ObservationFlag: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var value = false
|
|
|
|
func set() {
|
|
self.lock.lock()
|
|
self.value = true
|
|
self.lock.unlock()
|
|
}
|
|
|
|
func get() -> Bool {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.value
|
|
}
|
|
}
|
|
|
|
private final class BoolRecorder: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var values: [Bool] = []
|
|
|
|
func append(_ value: Bool) {
|
|
self.lock.lock()
|
|
self.values.append(value)
|
|
self.lock.unlock()
|
|
}
|
|
|
|
func get() -> [Bool] {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.values
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `default refresh frequency is five minutes`() throws {
|
|
let suite = "SettingsStoreTests-default"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.refreshFrequency == .fiveMinutes)
|
|
#expect(store.refreshFrequency.seconds == 300)
|
|
#expect(defaults.string(forKey: "refreshFrequency") == RefreshFrequency.fiveMinutes.rawValue)
|
|
}
|
|
|
|
@Test
|
|
func `repairs unrecognized refresh frequency raw value`() throws {
|
|
let suite = "SettingsStoreTests-invalid-refresh"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set("legacyValue", forKey: "refreshFrequency")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.refreshFrequency == .fiveMinutes)
|
|
#expect(defaults.string(forKey: "refreshFrequency") == RefreshFrequency.fiveMinutes.rawValue)
|
|
}
|
|
|
|
@Test
|
|
func `persists refresh frequency across instances`() throws {
|
|
let suite = "SettingsStoreTests-persist"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
storeA.refreshFrequency = .fifteenMinutes
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.refreshFrequency == .fifteenMinutes)
|
|
#expect(storeB.refreshFrequency.seconds == 900)
|
|
}
|
|
|
|
@Test
|
|
func `refresh on open defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-refresh-on-open"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.refreshAllProvidersOnMenuOpen == false)
|
|
store.refreshAllProvidersOnMenuOpen = true
|
|
|
|
let reloaded = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(reloaded.refreshAllProvidersOnMenuOpen == true)
|
|
}
|
|
|
|
@Test
|
|
func `exhausted reset time display defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-exhausted-reset-time"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.menuBarShowsResetTimeWhenExhausted == false)
|
|
store.menuBarShowsResetTimeWhenExhausted = true
|
|
|
|
let reloaded = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(reloaded.menuBarShowsResetTimeWhenExhausted == true)
|
|
}
|
|
|
|
@Test
|
|
func `weekly confetti setting defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-weekly-confetti"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.confettiOnWeeklyLimitResetsEnabled == false)
|
|
storeA.confettiOnWeeklyLimitResetsEnabled = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.confettiOnWeeklyLimitResetsEnabled == true)
|
|
}
|
|
|
|
@Test
|
|
func `session confetti setting defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-session-confetti"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.confettiOnSessionLimitResetsEnabled == false)
|
|
storeA.confettiOnSessionLimitResetsEnabled = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.confettiOnSessionLimitResetsEnabled == true)
|
|
}
|
|
|
|
@Test
|
|
func `provider storage setting defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-provider-storage"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.providerStorageFootprintsEnabled == false)
|
|
#expect(defaultsA.bool(forKey: "providerStorageFootprintsEnabled") == false)
|
|
storeA.providerStorageFootprintsEnabled = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.providerStorageFootprintsEnabled == true)
|
|
}
|
|
|
|
@Test
|
|
func `providers sorted alphabetically defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-providers-sorted-alpha"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.providersSortedAlphabetically == false)
|
|
storeA.providersSortedAlphabetically = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.providersSortedAlphabetically == true)
|
|
}
|
|
|
|
@Test
|
|
func `alphabetical provider order puts enabled first then sorts by name`() {
|
|
let metadata = ProviderDescriptorRegistry.metadata
|
|
let enabled: Set<UsageProvider> = [.cursor, .claude, .codex]
|
|
let ordered = CodexBarConfig.alphabeticalProviderOrder(
|
|
enablement: { enabled.contains($0) })
|
|
|
|
#expect(Set(ordered) == Set(UsageProvider.allCases))
|
|
|
|
let displayName: (UsageProvider) -> String = { metadata[$0]?.displayName ?? $0.rawValue }
|
|
let enabledPart = ordered.filter { enabled.contains($0) }
|
|
let disabledPart = ordered.filter { !enabled.contains($0) }
|
|
// Enabled providers occupy the top of the list, ahead of every disabled provider.
|
|
#expect(Array(ordered.prefix(enabled.count)) == enabledPart)
|
|
#expect(ordered == enabledPart + disabledPart)
|
|
let isSortedByName: ([UsageProvider]) -> Bool = { group in
|
|
group == group.sorted {
|
|
displayName($0).localizedCaseInsensitiveCompare(displayName($1)) == .orderedAscending
|
|
}
|
|
}
|
|
#expect(isSortedByName(enabledPart))
|
|
#expect(isSortedByName(disabledPart))
|
|
}
|
|
|
|
@Test
|
|
func `provider changelog links setting defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-provider-changelog-links"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.providerChangelogLinksEnabled == false)
|
|
#expect(defaultsA.bool(forKey: "providerChangelogLinksEnabled") == false)
|
|
storeA.providerChangelogLinksEnabled = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.providerChangelogLinksEnabled == true)
|
|
}
|
|
|
|
@Test
|
|
func `hide critters setting defaults off and persists`() throws {
|
|
let suite = "SettingsStoreTests-hide-critters"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.menuBarHidesCritters == false)
|
|
#expect(defaultsA.bool(forKey: "menuBarHidesCritters") == false)
|
|
storeA.menuBarHidesCritters = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.menuBarHidesCritters == true)
|
|
}
|
|
|
|
@Test
|
|
func `persists selected menu provider across instances`() throws {
|
|
let suite = "SettingsStoreTests-selectedMenuProvider"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
storeA.selectedMenuProvider = .claude
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.selectedMenuProvider == .claude)
|
|
}
|
|
|
|
@Test
|
|
func `persists merged menu last selected was overview across instances`() throws {
|
|
let suite = "SettingsStoreTests-merged-last-overview"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
storeA.mergedMenuLastSelectedWasOverview = true
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.mergedMenuLastSelectedWasOverview == true)
|
|
}
|
|
|
|
@Test
|
|
func `merged overview selected providers persists and normalizes across instances`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-selection"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
storeA.mergedOverviewSelectedProviders = [.opencode, .codex, .opencode, .claude]
|
|
#expect(storeA.mergedOverviewSelectedProviders == [.opencode, .codex, .claude])
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.mergedOverviewSelectedProviders == [.opencode, .codex, .claude])
|
|
}
|
|
|
|
@Test
|
|
func `merged overview selected providers ignores invalid raw values`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-invalid-raw"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(["codex", "unknown-provider", "claude", "codex"], forKey: "mergedOverviewSelectedProviders")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .claude])
|
|
}
|
|
|
|
@Test
|
|
func `resolved merged overview providers defaults to first three when selection empty`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-default-first-three"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode, .warp]
|
|
let resolved = store.resolvedMergedOverviewProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [.codex, .claude, .cursor])
|
|
}
|
|
|
|
@Test
|
|
func `resolved merged overview providers honors explicit empty selection`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-explicit-empty"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = []
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode, .warp]
|
|
let resolved = store.resolvedMergedOverviewProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [])
|
|
}
|
|
|
|
@Test
|
|
func `resolved merged overview providers uses provider order not selection order`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-order"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = [.opencode, .codex, .cursor]
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode]
|
|
let resolved = store.resolvedMergedOverviewProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [.codex, .cursor, .opencode])
|
|
}
|
|
|
|
@Test
|
|
func `reconcile merged overview selection removes unavailable without auto fill`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-reconcile"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = [.codex, .claude, .opencode]
|
|
let activeProviders: [UsageProvider] = [.codex, .cursor, .gemini, .opencode]
|
|
|
|
let resolved = store.reconcileMergedOverviewSelectedProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [.codex, .opencode])
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .opencode])
|
|
}
|
|
|
|
@Test
|
|
func `reconcile merged overview selection does not clobber stored preference when three or fewer`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-three-or-fewer"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = [.codex, .claude, .cursor]
|
|
let activeProviders: [UsageProvider] = [.codex, .claude]
|
|
|
|
let resolved = store.reconcileMergedOverviewSelectedProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [.codex, .claude])
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .claude, .cursor])
|
|
}
|
|
|
|
@Test
|
|
func `reconcile merged overview selection ignores stale subset without persisting auto fill when three or fewer`()
|
|
throws
|
|
{
|
|
let suite = "SettingsStoreTests-merged-overview-three-or-fewer-subset"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = [.codex]
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor]
|
|
|
|
let resolved = store.reconcileMergedOverviewSelectedProviders(activeProviders: activeProviders)
|
|
|
|
#expect(resolved == [.codex, .claude, .cursor])
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex])
|
|
}
|
|
|
|
@Test
|
|
func `merged overview selection allows deselecting providers when three or fewer`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-deselect-three-or-fewer"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor]
|
|
#expect(store.resolvedMergedOverviewProviders(activeProviders: activeProviders) == activeProviders)
|
|
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .claude,
|
|
isSelected: false,
|
|
activeProviders: activeProviders)
|
|
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .cursor])
|
|
#expect(store.resolvedMergedOverviewProviders(activeProviders: activeProviders) == [.codex, .cursor])
|
|
}
|
|
|
|
@Test
|
|
func `merged overview selection applies when same active set is reordered`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-ordered-context"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let initialActiveProviders: [UsageProvider] = [.codex, .claude, .cursor]
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .claude,
|
|
isSelected: false,
|
|
activeProviders: initialActiveProviders)
|
|
|
|
let reorderedActiveProviders: [UsageProvider] = [.cursor, .codex, .claude]
|
|
let resolved = store.resolvedMergedOverviewProviders(activeProviders: reorderedActiveProviders)
|
|
|
|
#expect(resolved == [.cursor, .codex])
|
|
}
|
|
|
|
@Test
|
|
func `merged overview selection allows deselecting providers when more than three active`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-deselect-subset"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.mergedOverviewSelectedProviders = [.codex, .claude, .cursor]
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode]
|
|
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .cursor,
|
|
isSelected: false,
|
|
activeProviders: activeProviders)
|
|
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .claude])
|
|
#expect(store.resolvedMergedOverviewProviders(activeProviders: activeProviders) == [.codex, .claude])
|
|
}
|
|
|
|
@Test
|
|
func `reconcile merged overview selection preserves stored subset when active drops to three or fewer`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-preserve-subset-across-drop"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode]
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .claude,
|
|
isSelected: false,
|
|
activeProviders: activeProviders)
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .opencode,
|
|
isSelected: true,
|
|
activeProviders: activeProviders)
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .cursor, .opencode])
|
|
|
|
let reducedActiveProviders: [UsageProvider] = [.codex, .claude, .cursor]
|
|
let resolvedWhenReduced = store.reconcileMergedOverviewSelectedProviders(
|
|
activeProviders: reducedActiveProviders)
|
|
|
|
#expect(resolvedWhenReduced == [.codex, .claude, .cursor])
|
|
#expect(store.mergedOverviewSelectedProviders == [.codex, .cursor, .opencode])
|
|
|
|
let resolvedWhenRestored = store.resolvedMergedOverviewProviders(activeProviders: activeProviders)
|
|
#expect(resolvedWhenRestored == [.codex, .cursor, .opencode])
|
|
}
|
|
|
|
@Test
|
|
func `reconcile merged overview selection clears preference when no providers active`() throws {
|
|
let suite = "SettingsStoreTests-merged-overview-clear-on-empty-active"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let activeProviders: [UsageProvider] = [.codex, .claude, .cursor, .opencode]
|
|
_ = store.setMergedOverviewProviderSelection(
|
|
provider: .codex,
|
|
isSelected: false,
|
|
activeProviders: activeProviders)
|
|
#expect(store.resolvedMergedOverviewProviders(activeProviders: activeProviders) == [.claude, .cursor])
|
|
|
|
let resolvedWhenEmpty = store.reconcileMergedOverviewSelectedProviders(activeProviders: [])
|
|
#expect(resolvedWhenEmpty == [])
|
|
|
|
let resolvedAfterReenable = store.resolvedMergedOverviewProviders(activeProviders: activeProviders)
|
|
#expect(resolvedAfterReenable == [.codex, .claude, .cursor])
|
|
}
|
|
|
|
@Test
|
|
func `persists open code workspace ID across instances`() throws {
|
|
let suite = "SettingsStoreTests-opencode-workspace"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
storeA.opencodeWorkspaceID = "wrk_01KEJ50SHK9YR41HSRSJ6QTFCM"
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
#expect(storeB.opencodeWorkspaceID == "wrk_01KEJ50SHK9YR41HSRSJ6QTFCM")
|
|
}
|
|
|
|
@Test
|
|
func `defaults session quota notifications to enabled`() throws {
|
|
let key = "sessionQuotaNotificationsEnabled"
|
|
let suite = "SettingsStoreTests-sessionQuotaNotifications"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(store.sessionQuotaNotificationsEnabled == true)
|
|
#expect(defaults.bool(forKey: key) == true)
|
|
}
|
|
|
|
@Test
|
|
func `defaults quota warnings to disabled with global thresholds and sound`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-defaults"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.quotaWarningNotificationsEnabled == false)
|
|
#expect(store.quotaWarningThresholds == [50, 20])
|
|
#expect(store.quotaWarningWindowEnabled(.session) == true)
|
|
#expect(store.quotaWarningWindowEnabled(.weekly) == true)
|
|
#expect(store.quotaWarningSoundEnabled == true)
|
|
#expect(store.quotaWarningOnScreenAlertEnabled == false)
|
|
#expect(store.quotaWarningMarkersVisible == true)
|
|
#expect(defaults.array(forKey: "quotaWarningThresholds") as? [Int] == [50, 20])
|
|
#expect(defaults.object(forKey: "quotaWarningSessionEnabled") as? Bool == true)
|
|
#expect(defaults.object(forKey: "quotaWarningWeeklyEnabled") as? Bool == true)
|
|
#expect(defaults.bool(forKey: "quotaWarningSoundEnabled") == true)
|
|
#expect(defaults.object(forKey: "quotaWarningOnScreenAlertEnabled") as? Bool == false)
|
|
#expect(defaults.object(forKey: "quotaWarningMarkersVisible") as? Bool == true)
|
|
}
|
|
|
|
@Test
|
|
func `on-screen quota warning preference persists`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-on-screen-alert"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.quotaWarningOnScreenAlertEnabled = true
|
|
|
|
let reloaded = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(reloaded.quotaWarningOnScreenAlertEnabled == true)
|
|
}
|
|
|
|
@Test
|
|
func `global quota warning windows persist independently`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-window-enabled"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.setQuotaWarningWindowEnabled(.weekly, enabled: false)
|
|
|
|
#expect(store.quotaWarningWindowEnabled(.session) == true)
|
|
#expect(store.quotaWarningWindowEnabled(.weekly) == false)
|
|
#expect(defaults.object(forKey: "quotaWarningWeeklyEnabled") as? Bool == false)
|
|
}
|
|
|
|
@Test
|
|
func `sanitizes invalid quota warning thresholds from defaults`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-sanitize"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set([120, 20, 20, -5, 50], forKey: "quotaWarningThresholds")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.quotaWarningThresholds == [99, 50, 20, 0])
|
|
#expect(defaults.array(forKey: "quotaWarningThresholds") as? [Int] == [99, 50, 20, 0])
|
|
}
|
|
|
|
@Test
|
|
func `quota warning threshold pair resolves blanks and clamps bounds`() {
|
|
#expect(QuotaWarningThresholds.resolved(upper: nil, lower: nil) == [50, 20])
|
|
#expect(QuotaWarningThresholds.resolved(upper: nil, lower: 10) == [50, 10])
|
|
#expect(QuotaWarningThresholds.resolved(upper: 10, lower: nil) == [10, 0])
|
|
#expect(QuotaWarningThresholds.resolved(upper: 120, lower: -5) == [99, 0])
|
|
}
|
|
|
|
@Test
|
|
func `provider quota warning override resolves before global thresholds`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-provider-override"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
store.quotaWarningThresholds = [50, 20]
|
|
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [50, 20])
|
|
#expect(store.explicitQuotaWarningThresholds(provider: .codex, window: .session) == nil)
|
|
store.setQuotaWarningThresholds(provider: .codex, window: .session, thresholds: [10])
|
|
#expect(store.explicitQuotaWarningThresholds(provider: .codex, window: .session) == [10])
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [10])
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .weekly) == [50, 20])
|
|
|
|
store.setQuotaWarningThresholds(provider: .codex, window: .session, thresholds: nil)
|
|
#expect(store.explicitQuotaWarningThresholds(provider: .codex, window: .session) == nil)
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [50, 20])
|
|
}
|
|
|
|
@Test
|
|
func `provider quota warning stale editor save does not restore cleared override`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-provider-cleared-override"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
store.quotaWarningThresholds = [50, 20]
|
|
store.setQuotaWarningOverride(provider: .codex, window: .session, thresholds: [70, 30], enabled: true)
|
|
let staleEditorThresholds = store.resolvedQuotaWarningThresholds(provider: .codex, window: .session)
|
|
|
|
store.setQuotaWarningOverride(provider: .codex, window: .session, thresholds: nil, enabled: nil)
|
|
store.setQuotaWarningThresholdsIfOverridden(
|
|
provider: .codex,
|
|
window: .session,
|
|
thresholds: staleEditorThresholds)
|
|
|
|
#expect(store.hasQuotaWarningOverride(provider: .codex, window: .session) == false)
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [50, 20])
|
|
}
|
|
|
|
@Test
|
|
func `provider quota warning inherited thresholds stay inherited after no-op editor save`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-provider-inherited-thresholds"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
store.quotaWarningThresholds = [50, 20]
|
|
store.setQuotaWarningOverride(provider: .codex, window: .session, thresholds: nil, enabled: true)
|
|
|
|
let resolvedEditorThresholds = store.resolvedQuotaWarningThresholds(provider: .codex, window: .session)
|
|
store.setQuotaWarningThresholdsIfOverridden(
|
|
provider: .codex,
|
|
window: .session,
|
|
thresholds: resolvedEditorThresholds)
|
|
|
|
let sessionConfig = store.providerConfig(for: .codex)?.quotaWarnings?.session
|
|
#expect(sessionConfig?.enabled == true)
|
|
#expect(sessionConfig?.thresholds == nil)
|
|
|
|
store.quotaWarningThresholds = [80, 40]
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [80, 40])
|
|
}
|
|
|
|
@Test
|
|
func `global quota warning thresholds resolve independently by window`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-window-thresholds"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.setQuotaWarningThresholds(.session, thresholds: [25])
|
|
store.setQuotaWarningThresholds(.weekly, thresholds: [75, 10])
|
|
|
|
#expect(store.quotaWarningThresholds(.session) == [25])
|
|
#expect(store.quotaWarningThresholds(.weekly) == [75, 10])
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .session) == [25])
|
|
#expect(store.resolvedQuotaWarningThresholds(provider: .codex, window: .weekly) == [75, 10])
|
|
}
|
|
|
|
@Test
|
|
func `provider quota warning windows override global enablement independently`() throws {
|
|
let suite = "SettingsStoreTests-quota-warning-provider-window-override"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.setQuotaWarningWindowEnabled(.weekly, enabled: false)
|
|
#expect(store.quotaWarningEnabled(provider: .codex, window: .weekly) == false)
|
|
|
|
store.setQuotaWarningWindowEnabled(provider: .codex, window: .weekly, enabled: true)
|
|
store.setQuotaWarningWindowEnabled(provider: .codex, window: .session, enabled: false)
|
|
#expect(store.quotaWarningEnabled(provider: .codex, window: .weekly) == true)
|
|
#expect(store.quotaWarningEnabled(provider: .codex, window: .session) == false)
|
|
#expect(store.hasQuotaWarningOverride(provider: .codex, window: .weekly) == true)
|
|
#expect(store.hasQuotaWarningOverride(provider: .codex, window: .session) == true)
|
|
|
|
store.setQuotaWarningWindowEnabled(provider: .codex, window: .weekly, enabled: nil)
|
|
#expect(store.quotaWarningEnabled(provider: .codex, window: .weekly) == false)
|
|
}
|
|
|
|
@Test
|
|
func `defaults claude usage source to auto`() throws {
|
|
let suite = "SettingsStoreTests-claude-source"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.claudeUsageDataSource == .auto)
|
|
}
|
|
|
|
@Test
|
|
func `defaults codex usage source to auto`() throws {
|
|
let suite = "SettingsStoreTests-codex-source"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.codexUsageDataSource == .auto)
|
|
}
|
|
|
|
@Test
|
|
func `defaults kilo usage source to auto`() throws {
|
|
let suite = "SettingsStoreTests-kilo-source"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.kiloUsageDataSource == .auto)
|
|
}
|
|
|
|
@Test
|
|
func `persists kilo usage source across instances`() throws {
|
|
let suite = "SettingsStoreTests-kilo-source-persist"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
storeA.kiloUsageDataSource = .cli
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.kiloUsageDataSource == .cli)
|
|
}
|
|
|
|
@Test
|
|
func `kilo extras only apply in auto mode`() throws {
|
|
let suite = "SettingsStoreTests-kilo-extras"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.kiloExtrasEnabled = true
|
|
#expect(store.kiloExtrasEnabled)
|
|
|
|
store.kiloUsageDataSource = .api
|
|
#expect(!store.kiloExtrasEnabled)
|
|
|
|
store.kiloUsageDataSource = .auto
|
|
#expect(store.kiloExtrasEnabled)
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func `apply external config does not broadcast`() throws {
|
|
let suite = "SettingsStoreTests-external-config"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
final class NotificationCounter: @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var value = 0
|
|
|
|
func increment() {
|
|
self.lock.lock()
|
|
self.value += 1
|
|
self.lock.unlock()
|
|
}
|
|
|
|
func get() -> Int {
|
|
self.lock.lock()
|
|
defer { self.lock.unlock() }
|
|
return self.value
|
|
}
|
|
}
|
|
|
|
let notifications = NotificationCounter()
|
|
let token = NotificationCenter.default.addObserver(
|
|
forName: .codexbarProviderConfigDidChange,
|
|
object: store,
|
|
queue: .main)
|
|
{ _ in
|
|
notifications.increment()
|
|
}
|
|
defer { NotificationCenter.default.removeObserver(token) }
|
|
|
|
store.applyExternalConfig(store.configSnapshot, reason: "test-external")
|
|
|
|
#expect(notifications.get() == 0)
|
|
}
|
|
|
|
@Test
|
|
func `config notifications classify order and provider changes`() throws {
|
|
let suite = "SettingsStoreTests-config-change-impact"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suite),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
let impacts = BoolRecorder()
|
|
let token = NotificationCenter.default.addObserver(
|
|
forName: .codexbarProviderConfigDidChange,
|
|
object: store,
|
|
queue: .main)
|
|
{ notification in
|
|
impacts.append(notification.userInfo?["affectsBackgroundWork"] as? Bool ?? true)
|
|
}
|
|
defer { NotificationCenter.default.removeObserver(token) }
|
|
|
|
store.setProviderOrder(Array(store.orderedProviders().reversed()))
|
|
store.codexUsageDataSource = .cli
|
|
|
|
#expect(impacts.get() == [false, true])
|
|
}
|
|
|
|
@Test
|
|
func `external config ignores order-only changes for background work`() throws {
|
|
let suite = "SettingsStoreTests-external-config-impact"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: testConfigStore(suiteName: suite),
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let initialConfigRevision = store.configRevision
|
|
let initialBackgroundRevision = store.backgroundWorkSettingsRevision
|
|
var reordered = store.configSnapshot
|
|
reordered.providers.reverse()
|
|
store.applyExternalConfig(reordered, reason: "order-only")
|
|
|
|
#expect(store.configRevision == initialConfigRevision + 1)
|
|
#expect(store.backgroundWorkSettingsRevision == initialBackgroundRevision)
|
|
#expect(store.orderedProviders() == reordered.providers.map(\.id))
|
|
|
|
var changed = store.configSnapshot
|
|
let codexIndex = try #require(changed.providers.firstIndex(where: { $0.id == .codex }))
|
|
changed.providers[codexIndex].source = .cli
|
|
store.applyExternalConfig(changed, reason: "provider-source", affectsBackgroundWork: false)
|
|
|
|
#expect(store.backgroundWorkSettingsRevision == initialBackgroundRevision + 1)
|
|
}
|
|
|
|
@Test
|
|
func `persists zai API region across instances`() throws {
|
|
let suite = "SettingsStoreTests-zai-region"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
storeA.zaiAPIRegion = .bigmodelCN
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
#expect(storeB.zaiAPIRegion == .bigmodelCN)
|
|
}
|
|
|
|
@Test
|
|
func `persists mini max API region across instances`() throws {
|
|
let suite = "SettingsStoreTests-minimax-region"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
storeA.minimaxAPIRegion = .chinaMainland
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore())
|
|
|
|
#expect(storeB.minimaxAPIRegion == .chinaMainland)
|
|
}
|
|
|
|
@Test
|
|
func `defaults open AI web access to disabled`() throws {
|
|
let suite = "SettingsStoreTests-openai-web"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.openAIWebAccessEnabled == false)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == false)
|
|
#expect(store.openAIWebBatterySaverEnabled == false)
|
|
#expect(defaults.bool(forKey: "openAIWebBatterySaverEnabled") == false)
|
|
#expect(store.codexCookieSource == .off)
|
|
}
|
|
|
|
@Test
|
|
func `infers open AI web access enabled for legacy configured codex cookies`() throws {
|
|
let suite = "SettingsStoreTests-openai-web-legacy"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.removeObject(forKey: "openAIWebAccessEnabled")
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
try configStore.save(CodexBarConfig(providers: [
|
|
ProviderConfig(id: .codex, cookieSource: .auto),
|
|
]))
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.openAIWebAccessEnabled == true)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == true)
|
|
#expect(store.openAIWebBatterySaverEnabled == false)
|
|
#expect(defaults.bool(forKey: "openAIWebBatterySaverEnabled") == false)
|
|
#expect(store.codexCookieSource == .auto)
|
|
}
|
|
|
|
@Test
|
|
func `imports legacy open AI web access defaults key`() throws {
|
|
let suite = "SettingsStoreTests-openai-web-legacy-key"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.removeObject(forKey: "openAIWebAccessEnabled")
|
|
defaults.set(false, forKey: "openAIWebAccess")
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
try configStore.save(CodexBarConfig(providers: [
|
|
ProviderConfig(id: .codex, cookieSource: .auto),
|
|
]))
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.openAIWebAccessEnabled == false)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == false)
|
|
}
|
|
|
|
@Test
|
|
func `infers open AI web access enabled for legacy codex config with implicit auto cookies`() throws {
|
|
let suite = "SettingsStoreTests-openai-web-legacy-implicit-auto"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.removeObject(forKey: "openAIWebAccessEnabled")
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
try configStore.save(CodexBarConfig(providers: [
|
|
ProviderConfig(id: .codex),
|
|
]))
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.openAIWebAccessEnabled == true)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == true)
|
|
#expect(store.openAIWebBatterySaverEnabled == false)
|
|
#expect(defaults.bool(forKey: "openAIWebBatterySaverEnabled") == false)
|
|
#expect(store.codexCookieSource == .auto)
|
|
}
|
|
|
|
@Test
|
|
func `disabling open AI web access turns codex cookie source off`() throws {
|
|
let suite = "SettingsStoreTests-openai-web-toggle"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
store.codexCookieSource = .auto
|
|
#expect(store.codexCookieSource == .auto)
|
|
|
|
store.openAIWebAccessEnabled = false
|
|
#expect(store.codexCookieSource == .off)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == false)
|
|
|
|
store.openAIWebAccessEnabled = true
|
|
#expect(store.codexCookieSource == .auto)
|
|
#expect(defaults.bool(forKey: "openAIWebAccessEnabled") == true)
|
|
}
|
|
|
|
@Test
|
|
func `open AI web battery saver persists separately from extras availability`() throws {
|
|
let suite = "SettingsStoreTests-openai-web-battery-saver"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(false, forKey: "debugDisableKeychainAccess")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.openAIWebBatterySaverEnabled == false)
|
|
|
|
store.openAIWebBatterySaverEnabled = false
|
|
#expect(defaults.bool(forKey: "openAIWebBatterySaverEnabled") == false)
|
|
|
|
store.openAIWebAccessEnabled = true
|
|
#expect(store.openAIWebBatterySaverEnabled == false)
|
|
}
|
|
|
|
@Test
|
|
func `codex spark usage visibility defaults on persists and refreshes only menus`() async throws {
|
|
let suite = "SettingsStoreTests-codex-spark-usage-visible"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.codexSparkUsageVisible)
|
|
let backgroundRevision = store.backgroundWorkSettingsRevision
|
|
let menuDidChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
menuDidChange.set()
|
|
}
|
|
store.codexSparkUsageVisible = false
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
#expect(store.backgroundWorkSettingsRevision == backgroundRevision)
|
|
#expect(menuDidChange.get())
|
|
|
|
let reloaded = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(reloaded.codexSparkUsageVisible == false)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token updates on defaults change`() async throws {
|
|
let suite = "SettingsStoreTests-observation-defaults"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.statusChecksEnabled.toggle()
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token updates on cost summary display style changes`() async throws {
|
|
let suite = "SettingsStoreTests-observation-cost-summary-display-style"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.costSummaryDisplayStyle = .costSubmenu
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token ignores merged switcher selection churn`() async throws {
|
|
let suite = "SettingsStoreTests-observation-switcher-selection"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.selectedMenuProvider = .claude
|
|
store.mergedMenuLastSelectedWasOverview.toggle()
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == false)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token updates on per-window quota threshold changes`() async throws {
|
|
let suite = "SettingsStoreTests-observation-quota-threshold-windows"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
func expectObservation(
|
|
for window: QuotaWarningWindow,
|
|
thresholds: [Int]) async
|
|
{
|
|
let didChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.setQuotaWarningThresholds(window, thresholds: thresholds)
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
await expectObservation(for: .session, thresholds: [70, 30])
|
|
await expectObservation(for: .weekly, thresholds: [80, 40])
|
|
}
|
|
|
|
@Test
|
|
func `quota warning threshold setters ignore unchanged values`() async throws {
|
|
let suite = "SettingsStoreTests-observation-quota-threshold-noop"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
store.setQuotaWarningThresholds(.session, thresholds: [70, 30])
|
|
|
|
let didChange = ObservationFlag()
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.setQuotaWarningThresholds(.session, thresholds: [70, 30])
|
|
try await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == false)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token updates on weekly progress work days changes`() async throws {
|
|
let suite = "SettingsStoreTests-observation-weekly-progress-work-days"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.weeklyProgressWorkDays = 5
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `config backed settings trigger observation`() async throws {
|
|
let suite = "SettingsStoreTests-observation-config"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.codexCookieSource
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.codexCookieSource = .manual
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `menu observation token updates on codex active source change`() async throws {
|
|
let suite = "SettingsStoreTests-observation-codex-active-source"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let didChange = ObservationFlag()
|
|
|
|
withObservationTracking {
|
|
_ = store.menuObservationToken
|
|
} onChange: {
|
|
didChange.set()
|
|
}
|
|
|
|
store.codexActiveSource = .liveSystem
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
|
|
#expect(didChange.get() == true)
|
|
}
|
|
|
|
@Test
|
|
func `provider order defaults to all cases`() throws {
|
|
let suite = "SettingsStoreTests-providerOrder-default"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.orderedProviders() == UsageProvider.allCases)
|
|
}
|
|
|
|
@Test
|
|
func `provider order persists and appends new providers`() throws {
|
|
let suite = "SettingsStoreTests-providerOrder-persist"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
// Partial list to mimic "older version" missing providers.
|
|
let config = CodexBarConfig(providers: [
|
|
ProviderConfig(id: .gemini),
|
|
ProviderConfig(id: .codex),
|
|
])
|
|
try configStore.save(config)
|
|
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let legacyOrder: [UsageProvider] = [.gemini, .codex]
|
|
let appendedProviders = UsageProvider.allCases.filter { !legacyOrder.contains($0) }
|
|
#expect(storeA.orderedProviders() == legacyOrder + appendedProviders)
|
|
|
|
// Move one provider; ensure it's persisted across instances.
|
|
let antigravityIndex = try #require(storeA.orderedProviders().firstIndex(of: .antigravity))
|
|
storeA.moveProvider(fromOffsets: IndexSet(integer: antigravityIndex), toOffset: 0)
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.orderedProviders().first == .antigravity)
|
|
}
|
|
|
|
@Test
|
|
func `setting alibaba API key enables provider`() throws {
|
|
let suite = "SettingsStoreTests-alibaba-enable-on-token"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let metadata = try #require(ProviderDescriptorRegistry.metadata[.alibaba])
|
|
store.setProviderEnabled(provider: .alibaba, metadata: metadata, enabled: false)
|
|
|
|
store.alibabaCodingPlanAPIToken = "cpk-test-token"
|
|
|
|
#expect(store.isProviderEnabled(provider: .alibaba, metadata: metadata))
|
|
}
|
|
|
|
@Test
|
|
func `alibaba provider auto enables on startup when token exists`() throws {
|
|
let suite = "SettingsStoreTests-alibaba-auto-enable-startup"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let config = CodexBarConfig(providers: [
|
|
ProviderConfig(id: .alibaba, enabled: false, apiKey: "cpk-startup-token"),
|
|
])
|
|
try configStore.save(config)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
let metadata = try #require(ProviderDescriptorRegistry.metadata[.alibaba])
|
|
#expect(store.isProviderEnabled(provider: .alibaba, metadata: metadata))
|
|
}
|
|
|
|
@Test
|
|
func `cost comparison periods default off and persist`() throws {
|
|
let suite = "SettingsStoreTests-cost-comparison-periods"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(!storeA.costComparisonPeriodsEnabled)
|
|
storeA.costComparisonPeriodsEnabled = true
|
|
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
#expect(storeB.costComparisonPeriodsEnabled)
|
|
}
|
|
|
|
@Test
|
|
func `cost summary display style defaults to both and persists`() throws {
|
|
let suite = "SettingsStoreTests-cost-summary-display-style"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.costSummaryDisplayStyle == .both)
|
|
|
|
storeA.costSummaryDisplayStyle = .costSubmenu
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.costSummaryDisplayStyle == .costSubmenu)
|
|
|
|
storeB.costSummaryDisplayStyleRaw = "legacy-style"
|
|
#expect(storeB.costSummaryDisplayStyle == .both)
|
|
}
|
|
|
|
@Test
|
|
func `missing cost summary display style preserves existing enabled cost summary`() throws {
|
|
let suite = "SettingsStoreTests-cost-summary-display-style-upgrade"
|
|
let defaults = try #require(UserDefaults(suiteName: suite))
|
|
defaults.removePersistentDomain(forName: suite)
|
|
defaults.set(true, forKey: "tokenCostUsageEnabled")
|
|
defaults.removeObject(forKey: "costSummaryDisplayStyle")
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
|
|
let store = SettingsStore(
|
|
userDefaults: defaults,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(store.costSummaryDisplayStyle == .both)
|
|
#expect(defaults.string(forKey: "costSummaryDisplayStyle") == CostSummaryDisplayStyle.both.rawValue)
|
|
}
|
|
|
|
@Test
|
|
func `enabling cost summary preserves both display style across relaunch`() throws {
|
|
let suite = "SettingsStoreTests-cost-summary-display-style-enable"
|
|
let defaultsA = try #require(UserDefaults(suiteName: suite))
|
|
defaultsA.removePersistentDomain(forName: suite)
|
|
let configStore = testConfigStore(suiteName: suite)
|
|
let storeA = SettingsStore(
|
|
userDefaults: defaultsA,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeA.costSummaryDisplayStyle == .both)
|
|
#expect(defaultsA.string(forKey: "costSummaryDisplayStyle") == nil)
|
|
|
|
storeA.costUsageEnabled = true
|
|
|
|
#expect(storeA.costSummaryDisplayStyle == .both)
|
|
#expect(defaultsA.string(forKey: "costSummaryDisplayStyle") == nil)
|
|
|
|
let defaultsB = try #require(UserDefaults(suiteName: suite))
|
|
let storeB = SettingsStore(
|
|
userDefaults: defaultsB,
|
|
configStore: configStore,
|
|
zaiTokenStore: NoopZaiTokenStore(),
|
|
syntheticTokenStore: NoopSyntheticTokenStore())
|
|
|
|
#expect(storeB.costSummaryDisplayStyle == .both)
|
|
}
|
|
}
|