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 = [.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) } }