import CodexBarCore import Foundation import SwiftUI import Testing @testable import CodexBar @MainActor struct ProviderSettingsDescriptorTests { @Test func `toggle I ds are unique across providers`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-unique") var seenToggleIDs: Set = [] var seenActionIDs: Set = [] var seenPickerIDs: Set = [] for provider in UsageProvider.allCases { let context = fixture.settingsContext(provider: provider) let impl = try #require(ProviderCatalog.implementation(for: provider)) let toggles = impl.settingsToggles(context: context) for toggle in toggles { #expect(!seenToggleIDs.contains(toggle.id)) seenToggleIDs.insert(toggle.id) for action in toggle.actions { #expect(!seenActionIDs.contains(action.id)) seenActionIDs.insert(action.id) } } let pickers = impl.settingsPickers(context: context) for picker in pickers { #expect(!seenPickerIDs.contains(picker.id)) seenPickerIDs.insert(picker.id) } } } @Test func `openai exposes project id setting`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-openai-project") let context = fixture.settingsContext(provider: .openai) let fields = OpenAIAPIProviderImplementation().settingsFields(context: context) let project = try #require(fields.first(where: { $0.id == "openai-project-id" })) project.binding.wrappedValue = "proj_abc" #expect(project.title == "Project ID") #expect(project.subtitle.contains(OpenAIAPISettingsReader.projectIDEnvironmentKey)) #expect(fixture.settings.openAIAPIProjectID == "proj_abc") #expect(fixture.settings.providerConfig(for: .openai)?.sanitizedWorkspaceID == "proj_abc") } @Test func `codex exposes usage and cookie pickers`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-codex") let context = fixture.settingsContext(provider: .codex) let pickers = CodexProviderImplementation().settingsPickers(context: context) let toggles = CodexProviderImplementation().settingsToggles(context: context) #expect(pickers.contains(where: { $0.id == "codex-usage-source" })) let cookiePicker = try #require(pickers.first(where: { $0.id == "codex-cookie-source" })) #expect(cookiePicker.placement == .connection) #expect(toggles.contains(where: { $0.id == "codex-historical-tracking" })) let sparkToggle = try #require(toggles.first(where: { $0.id == "codex-spark-usage-visible" })) #expect(sparkToggle.title == "Show Codex Spark usage") #expect(sparkToggle.subtitle.contains("menu and provider preview")) #expect(sparkToggle.binding.wrappedValue) #expect(sparkToggle.isEnabled?() == true) sparkToggle.binding.wrappedValue = false #expect(fixture.settings.codexSparkUsageVisible == false) fixture.settings.showOptionalCreditsAndExtraUsage = false #expect(sparkToggle.isEnabled?() == false) } @Test func `antigravity usage source picker clarifies local ide and agy`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-antigravity-source") let context = fixture.settingsContext(provider: .antigravity) let pickers = AntigravityProviderImplementation().settingsPickers(context: context) let usagePicker = try #require(pickers.first(where: { $0.id == "antigravity-usage-source" })) #expect(usagePicker.options.map(\.title) == ["Auto", "Google OAuth", "Local API / agy CLI"]) #expect(usagePicker.subtitle == "Auto tries Antigravity app, agy CLI, then IDE; OAuth follows for selected or signed-in accounts.") } @Test func `codex exposes open AI web extras toggle as default off opt in`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-codex-openai-toggle") let context = fixture.settingsContext(provider: .codex) let toggles = CodexProviderImplementation().settingsToggles(context: context) let extrasToggle = try #require(toggles.first(where: { $0.id == "codex-openai-web-extras" })) #expect(extrasToggle.binding.wrappedValue == false) #expect(extrasToggle.subtitle.contains("Optional.")) #expect(extrasToggle.subtitle.contains("Turn this on")) let batterySaverToggle = try #require(toggles.first(where: { $0.id == "codex-openai-web-battery-saver" })) #expect(batterySaverToggle.binding.wrappedValue == false) #expect(batterySaverToggle.isVisible?() == false) fixture.settings.openAIWebAccessEnabled = true #expect(batterySaverToggle.isVisible?() == true) } @Test func `claude exposes usage and cookie pickers`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude") fixture.settings.debugDisableKeychainAccess = false let context = fixture.settingsContext(provider: .claude) let pickers = ClaudeProviderImplementation().settingsPickers(context: context) let usagePicker = try #require(pickers.first(where: { $0.id == "claude-usage-source" })) #expect(usagePicker.placement == .connection) #expect(pickers.contains(where: { $0.id == "claude-cookie-source" })) let toggles = ClaudeProviderImplementation().settingsToggles(context: context) #expect(!toggles.contains(where: { $0.id == "claude-peak-hours" })) let keychainPicker = try #require(pickers.first(where: { $0.id == "claude-keychain-prompt-policy" })) let optionIDs = Set(keychainPicker.options.map(\.id)) #expect(optionIDs.contains(ClaudeOAuthKeychainPromptMode.never.rawValue)) #expect(optionIDs.contains(ClaudeOAuthKeychainPromptMode.onlyOnUserAction.rawValue)) #expect(optionIDs.contains(ClaudeOAuthKeychainPromptMode.always.rawValue)) #expect(keychainPicker.isEnabled?() ?? true) } @Test func `claude prompt policy picker remains visible for prompt free toggle`() throws { let fixture = try self.makeSettingsFixture( suite: "ProviderSettingsDescriptorTests-claude-prompt-visible-prompt-free") fixture.settings.debugDisableKeychainAccess = false fixture.settings.claudeOAuthPromptFreeCredentialsEnabled = true let context = fixture.settingsContext(provider: .claude) let pickers = ClaudeProviderImplementation().settingsPickers(context: context) let keychainPicker = try #require(pickers.first(where: { $0.id == "claude-keychain-prompt-policy" })) #expect(keychainPicker.isVisible?() ?? true) #expect(keychainPicker.binding.wrappedValue == ClaudeOAuthKeychainPromptMode.never.rawValue) } @Test func `claude avoid keychain prompts toggle is disabled when global keychain disabled`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude-prompt-free-disabled") fixture.settings.debugDisableKeychainAccess = true fixture.settings.claudeOAuthPromptFreeCredentialsEnabled = true let context = fixture.settingsContext(provider: .claude) let toggles = ClaudeProviderImplementation().settingsToggles(context: context) let promptFreeToggle = try #require(toggles.first(where: { $0.id == "claude-oauth-prompt-free-credentials" })) #expect(promptFreeToggle.isEnabled?() == false) #expect(promptFreeToggle.binding.wrappedValue == true) promptFreeToggle.binding.wrappedValue = false #expect(fixture.settings.claudeOAuthPromptFreeCredentialsEnabled == true) fixture.settings.debugDisableKeychainAccess = false #expect(promptFreeToggle.isEnabled?() == true) #expect(promptFreeToggle.binding.wrappedValue == true) } @Test func `claude keychain prompt policy picker disabled when global keychain disabled`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude-keychain-disabled") fixture.settings.debugDisableKeychainAccess = true let context = fixture.settingsContext(provider: .claude) let pickers = ClaudeProviderImplementation().settingsPickers(context: context) let keychainPicker = try #require(pickers.first(where: { $0.id == "claude-keychain-prompt-policy" })) #expect(keychainPicker.isEnabled?() == false) let subtitle = keychainPicker.dynamicSubtitle?() ?? "" #expect(subtitle.localizedCaseInsensitiveContains("inactive")) } @Test func `claude web extras auto disables when leaving CLI`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-claude-invariant") let settings = fixture.settings settings.debugMenuEnabled = true settings.claudeUsageDataSource = .cli settings.claudeWebExtrasEnabled = true settings.claudeUsageDataSource = .oauth #expect(settings.claudeWebExtrasEnabled == false) } @Test func `kilo exposes usage source picker and api field only`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-kilo") let context = fixture.settingsContext(provider: .kilo) let implementation = KiloProviderImplementation() let toggles = implementation.settingsToggles(context: context) let pickers = implementation.settingsPickers(context: context) let fields = implementation.settingsFields(context: context) #expect(toggles.isEmpty) #expect(pickers.contains(where: { $0.id == "kilo-usage-source" })) #expect(fields.contains(where: { $0.id == "kilo-api-key" })) } @Test func `copilot budget secondary picker appears before cookie picker`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-copilot-budget-pickers") fixture.settings.copilotBudgetExtrasEnabled = true let context = fixture.settingsContext(provider: .copilot) let pickers = CopilotProviderImplementation().settingsPickers(context: context) #expect(pickers.map(\.id) == ["copilot-icon-secondary-window", "copilot-budget-cookie-source"]) #expect(pickers.first?.title == "Menu bar secondary metric") #expect(pickers.first?.placement == .menuBar) #expect(pickers.last?.placement == .connection) } @Test func `kiro menu bar display picker uses the menu bar placement`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-kiro-placement") let context = fixture.settingsContext(provider: .kiro) let pickers = KiroProviderImplementation().settingsPickers(context: context) let picker = try #require(pickers.first(where: { $0.id == "kiroMenuBarDisplay" })) #expect(picker.placement == .menuBar) } @Test func `copilot manual cookie field is labelled and refreshable`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-copilot-budget-field") fixture.settings.copilotBudgetExtrasEnabled = true fixture.settings.copilotBudgetCookieSource = .manual let context = fixture.settingsContext(provider: .copilot) let fields = CopilotProviderImplementation().settingsFields(context: context) let field = try #require(fields.first { $0.id == "copilot-budget-cookie-header" }) #expect(field.title == "Manual GitHub Cookie header") #expect(field.subtitle.contains("Treat this value like a password")) #expect(field.actions.map(\.id) == ["refresh-copilot-budget-cookie"]) } @Test func `kimi exposes usage source picker plus api and cookie fields`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-kimi") let context = fixture.settingsContext(provider: .kimi) let implementation = KimiProviderImplementation() let pickers = implementation.settingsPickers(context: context) let fields = implementation.settingsFields(context: context) let usagePicker = try #require(pickers.first(where: { $0.id == "kimi-usage-source" })) #expect(usagePicker.options.map(\.id) == ["auto", "api", "web"]) #expect(usagePicker.subtitle == "Auto tries your configured API key, then a signed-in Kimi Code CLI credential, then browser cookies.") #expect(usagePicker.placement == .connection) #expect(usagePicker.trailingText?() == nil) fixture.store.lastSourceLabels[.kimi] = "Kimi Code CLI" #expect(usagePicker.trailingText?() == "Kimi Code CLI") #expect(pickers.contains(where: { $0.id == "kimi-cookie-source" })) #expect(fields.contains(where: { $0.id == "kimi-api-key" })) #expect(fields.contains(where: { $0.id == "kimi-cookie" })) } @Test func `kimi presentation follows selected source label`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-kimi-presentation") fixture.settings.kimiUsageDataSource = .api let metadata = try #require(ProviderDescriptorRegistry.metadata[.kimi]) let context = fixture.presentationContext(provider: .kimi, metadata: metadata) let detailLine = KimiProviderImplementation() .presentation(context: context) .detailLine(context) #expect(detailLine == "api") } @Test func `deepgram exposes api key and project id fields`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-deepgram") let context = fixture.settingsContext(provider: .deepgram) let implementation = DeepgramProviderImplementation() let fields = implementation.settingsFields(context: context) #expect(fields.contains(where: { $0.id == "deepgram-api-key" })) #expect(fields.contains(where: { $0.id == "deepgram-project-id" })) // Basic presence checks for Deepgram settings fields (layout copied from OpenRouter) _ = try #require(fields.first(where: { $0.id == "deepgram-project-id" })) _ = try #require(fields.first(where: { $0.id == "deepgram-api-key" })) } @Test func `alibaba presentation follows store source label`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-alibaba-presentation") let metadata = try #require(ProviderDescriptorRegistry.metadata[.alibaba]) let context = fixture.presentationContext(provider: .alibaba, metadata: metadata) let detailLine = AlibabaCodingPlanProviderImplementation() .presentation(context: context) .detailLine(context) #expect(detailLine == fixture.store.sourceLabel(for: .alibaba)) } @Test func `devin presentation follows store source label`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-devin-presentation") fixture.store.lastSourceLabels[.devin] = "web" let metadata = try #require(ProviderDescriptorRegistry.metadata[.devin]) let context = fixture.presentationContext(provider: .devin, metadata: metadata) let detailLine = DevinProviderImplementation() .presentation(context: context) .detailLine(context) #expect(detailLine == "web") } @Test func `alibaba token plan settings expose cookie controls`() throws { let fixture = try self.makeSettingsFixture(suite: "ProviderSettingsDescriptorTests-alibaba-token-plan-settings") fixture.settings.alibabaTokenPlanCookieSource = .manual let context = fixture.settingsContext(provider: .alibabatokenplan) let implementation = AlibabaTokenPlanProviderImplementation() let pickers = implementation.settingsPickers(context: context) let fields = implementation.settingsFields(context: context) #expect(pickers.contains(where: { $0.id == "alibaba-token-plan-cookie-source" })) #expect(fields.contains(where: { $0.id == "alibaba-token-plan-cookie" })) #expect(fields.first?.actions.contains(where: { $0.id == "alibaba-token-plan-open-dashboard" }) == true) } private func makeSettingsFixture(suite: String) throws -> ProviderSettingsFixture { let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let settings = SettingsStore( userDefaults: defaults, configStore: testConfigStore(suiteName: suite), zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) let store = UsageStore( fetcher: UsageFetcher(environment: [:]), browserDetection: BrowserDetection(cacheTTL: 0), settings: settings) return ProviderSettingsFixture(settings: settings, store: store) } private struct ProviderSettingsFixture { let settings: SettingsStore let store: UsageStore private let state = ProviderSettingsContextState() @MainActor func settingsContext(provider: UsageProvider) -> ProviderSettingsContext { let settings = self.settings let store = self.store let state = self.state return ProviderSettingsContext( provider: provider, settings: settings, store: store, boolBinding: { keyPath in Binding( get: { settings[keyPath: keyPath] }, set: { settings[keyPath: keyPath] = $0 }) }, stringBinding: { keyPath in Binding( get: { settings[keyPath: keyPath] }, set: { settings[keyPath: keyPath] = $0 }) }, statusText: { id in state.statusByID[id] }, setStatusText: { id, text in if let text { state.statusByID[id] = text } else { state.statusByID.removeValue(forKey: id) } }, lastAppActiveRunAt: { id in state.lastRunAtByID[id] }, setLastAppActiveRunAt: { id, date in if let date { state.lastRunAtByID[id] = date } else { state.lastRunAtByID.removeValue(forKey: id) } }, requestConfirmation: { _ in }, runLoginFlow: {}) } @MainActor func presentationContext(provider: UsageProvider, metadata: ProviderMetadata) -> ProviderPresentationContext { ProviderPresentationContext( provider: provider, settings: self.settings, store: self.store, metadata: metadata) } } private final class ProviderSettingsContextState { var statusByID: [String: String] = [:] var lastRunAtByID: [String: Date] = [:] } }