410 lines
19 KiB
Swift
410 lines
19 KiB
Swift
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<String> = []
|
|
var seenActionIDs: Set<String> = []
|
|
var seenPickerIDs: Set<String> = []
|
|
|
|
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] = [:]
|
|
}
|
|
}
|