import CodexBarCore import Foundation import Testing @testable import CodexBar @MainActor struct SettingsStoreCoverageTests { @Test func `provider ordering and caching`() throws { let suite = "SettingsStoreCoverageTests-ordering" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let config = CodexBarConfig(providers: [ ProviderConfig(id: .zai), ProviderConfig(id: .codex), ProviderConfig(id: .claude), ]) try configStore.save(config) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) let ordered = settings.orderedProviders() let cached = settings.orderedProviders() #expect(ordered == cached) #expect(ordered.first == .zai) #expect(ordered.contains(.minimax)) let configRevisionBeforeOrder = settings.configRevision let backgroundRevisionBeforeOrder = settings.backgroundWorkSettingsRevision settings.moveProvider(fromOffsets: IndexSet(integer: 0), toOffset: 2) #expect(settings.orderedProviders() != ordered) #expect(settings.configRevision == configRevisionBeforeOrder + 1) #expect(settings.backgroundWorkSettingsRevision == backgroundRevisionBeforeOrder) let metadata = ProviderRegistry.shared.metadata let backgroundRevisionBeforeEnablement = settings.backgroundWorkSettingsRevision try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true) #expect(settings.backgroundWorkSettingsRevision == backgroundRevisionBeforeEnablement + 1) try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: false) let enabled = settings.enabledProvidersOrdered(metadataByProvider: metadata) #expect(enabled.contains(.codex)) } @Test func `disabling selected provider clears menu selection`() throws { let settings = Self.makeSettingsStore() let metadata = ProviderRegistry.shared.metadata try settings.setProviderEnabled(provider: .codex, metadata: #require(metadata[.codex]), enabled: true) try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: true) settings.selectedMenuProvider = .claude try settings.setProviderEnabled(provider: .claude, metadata: #require(metadata[.claude]), enabled: false) #expect(settings.selectedMenuProvider == nil) #expect(settings.enabledProvidersOrdered(metadataByProvider: metadata) == [.codex]) } @Test func `menu bar metric preferences and display modes`() { let settings = Self.makeSettingsStore() settings.setMenuBarMetricPreference(.average, for: .codex) #expect(settings.menuBarMetricPreference(for: .codex) == .automatic) settings.setMenuBarMetricPreference(.average, for: .gemini) #expect(settings.menuBarMetricPreference(for: .gemini) == .average) #expect(settings.menuBarMetricSupportsAverage(for: .gemini)) settings.setMenuBarMetricPreference(.secondary, for: .zai) #expect(settings.menuBarMetricPreference(for: .zai) == .secondary) settings.menuBarDisplayMode = .pace #expect(settings.menuBarDisplayMode == .pace) #expect(settings.historicalTrackingEnabled == false) settings.historicalTrackingEnabled = true #expect(settings.historicalTrackingEnabled == true) settings.resetTimesShowAbsolute = true #expect(settings.resetTimeDisplayStyle == .absolute) } @Test func `minimax settings snapshot uses selected token account as manual cookie`() { let settings = Self.makeSettingsStore(suiteName: "SettingsStoreCoverageTests-minimax-token-account") settings.minimaxCookieSource = .auto settings.minimaxCookieHeader = "HERTZ-SESSION=global" settings.addTokenAccount(provider: .minimax, label: "account", token: "HERTZ-SESSION=selected") let snapshot = settings.minimaxSettingsSnapshot(tokenOverride: nil) #expect(snapshot.cookieSource == .manual) #expect(snapshot.manualCookieHeader == "HERTZ-SESSION=selected") } @Test func `minimax settings snapshot falls back to global cookie without token accounts`() { let settings = Self.makeSettingsStore(suiteName: "SettingsStoreCoverageTests-minimax-global-cookie") settings.minimaxCookieSource = .auto settings.minimaxCookieHeader = "HERTZ-SESSION=global" let snapshot = settings.minimaxSettingsSnapshot(tokenOverride: nil) #expect(snapshot.cookieSource == .auto) #expect(snapshot.manualCookieHeader == "HERTZ-SESSION=global") } @Test func `copilot budget extras default off and persist in provider snapshot`() throws { let suite = "SettingsStoreCoverageTests-copilot-budget-extras" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let initial = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(initial.copilotBudgetExtrasEnabled == false) #expect(initial.copilotSettingsSnapshot(tokenOverride: nil).budgetExtrasEnabled == false) initial.copilotBudgetExtrasEnabled = true let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded.copilotBudgetExtrasEnabled) #expect(reloaded.copilotSettingsSnapshot(tokenOverride: nil).budgetExtrasEnabled) } @Test func `agent sessions default off and persist explicit opt in`() throws { let suite = "SettingsStoreCoverageTests-agent-sessions" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let initial = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(initial.agentSessionsEnabled == false) #expect(defaults.object(forKey: "agentSessionsEnabled") == nil) initial.agentSessionsEnabled = true #expect(defaults.object(forKey: "agentSessionsEnabled") as? Bool == true) let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded.agentSessionsEnabled) } @Test func `multi account menu layout persists and bridges legacy show all token accounts`() throws { let suite = "SettingsStoreCoverageTests-multi-account-layout" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let initial = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(initial.multiAccountMenuLayout == .segmented) initial.multiAccountMenuLayout = .stacked #expect(defaults.string(forKey: "multiAccountMenuLayout") == MultiAccountMenuLayout.stacked.rawValue) #expect(initial.showAllTokenAccountsInMenu) let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded.multiAccountMenuLayout == .stacked) reloaded.showAllTokenAccountsInMenu = false #expect(reloaded.multiAccountMenuLayout == .segmented) } @Test func `legacy show all token accounts migrates to stacked layout`() throws { let suite = "SettingsStoreCoverageTests-legacy-token-account-layout" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set(true, forKey: "showAllTokenAccountsInMenu") let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(settings.multiAccountMenuLayout == .stacked) } @Test func `token account mutations apply side effects`() { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .claude, label: "Primary", token: "token") #expect(settings.tokenAccounts(for: .claude).count == 1) #expect(settings.claudeCookieSource == .manual) let account = settings.selectedTokenAccount(for: .claude) #expect(account != nil) settings.setActiveTokenAccountIndex(10, for: .claude) #expect(settings.selectedTokenAccount(for: .claude)?.id == account?.id) if let id = account?.id { settings.removeTokenAccount(provider: .claude, accountID: id) } #expect(settings.tokenAccounts(for: .claude).isEmpty) settings.reloadTokenAccounts() } @Test func `token account update preserves identity and selection`() throws { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .copilot, label: "Primary", token: "token-1") settings.addTokenAccount(provider: .copilot, label: "Secondary", token: "token-2") settings.setActiveTokenAccountIndex(0, for: .copilot) let original = try #require(settings.selectedTokenAccount(for: .copilot)) settings.updateTokenAccount( provider: .copilot, accountID: original.id, label: "Primary (Pro)", token: "token-1b") let updated = try #require(settings.selectedTokenAccount(for: .copilot)) #expect(updated.id == original.id) #expect(updated.label == "Primary (Pro)") #expect(updated.token == "token-1b") #expect(settings.tokenAccounts(for: .copilot).count == 2) } @Test func `zai token account update preserves team metadata`() throws { let settings = Self.makeSettingsStore() settings.addTokenAccount( provider: .zai, label: "Team", token: "token-1", usageScope: "team", organizationID: "org-team", workspaceID: "proj-team") let original = try #require(settings.selectedTokenAccount(for: .zai)) settings.updateTokenAccount( provider: .zai, accountID: original.id, label: "Team Updated", token: "token-2") let updated = try #require(settings.selectedTokenAccount(for: .zai)) #expect(updated.usageScope == "team") #expect(updated.organizationID == "org-team") #expect(updated.workspaceID == "proj-team") } @Test func `copilot token accounts clear legacy api key fallback`() throws { let settings = Self.makeSettingsStore() settings.copilotAPIToken = "legacy-token" settings.addTokenAccount(provider: .copilot, label: "Primary", token: "token-1") #expect(settings.copilotAPIToken.isEmpty) #expect(settings.copilotSettingsSnapshot(tokenOverride: nil).apiToken == "token-1") settings.copilotAPIToken = "legacy-token" let account = try #require(settings.selectedTokenAccount(for: .copilot)) settings.removeTokenAccount(provider: .copilot, accountID: account.id) #expect(settings.tokenAccounts(for: .copilot).isEmpty) #expect(settings.copilotAPIToken.isEmpty) #expect(settings.copilotSettingsSnapshot(tokenOverride: nil).apiToken == nil) } @Test func `copilot settings snapshot carries selected account identifier`() { let settings = Self.makeSettingsStore() settings.addTokenAccount( provider: .copilot, label: "octocat (Pro)", token: "token-1", externalIdentifier: "github:user:123") let snapshot = settings.copilotSettingsSnapshot(tokenOverride: nil) #expect(snapshot.apiToken == "token-1") #expect(snapshot.selectedAccountExternalIdentifier == "github:user:123") } @Test func `copilot enterprise host persists in provider config`() throws { let suite = "SettingsStoreCoverageTests-copilot-enterprise-host" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let first = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) first.copilotEnterpriseHost = "https://octocorp.ghe.com/login" #expect(first.copilotEnterpriseHost == "https://octocorp.ghe.com/login") #expect(first.copilotSettingsSnapshot(tokenOverride: nil).enterpriseHost == "octocorp.ghe.com") let second = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(second.copilotEnterpriseHost == "https://octocorp.ghe.com/login") second.copilotEnterpriseHost = "github.com" #expect(second.copilotEnterpriseHost == "github.com") #expect(second.copilotSettingsSnapshot(tokenOverride: nil).enterpriseHost == nil) } @Test func `removing another token account preserves active selection`() throws { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .copilot, label: "A", token: "token-a") settings.addTokenAccount(provider: .copilot, label: "B", token: "token-b") settings.addTokenAccount(provider: .copilot, label: "C", token: "token-c") settings.setActiveTokenAccountIndex(1, for: .copilot) let activeBefore = try #require(settings.selectedTokenAccount(for: .copilot)) let accountToRemove = try #require(settings.tokenAccounts(for: .copilot).first) settings.removeTokenAccount(provider: .copilot, accountID: accountToRemove.id) let activeAfter = try #require(settings.selectedTokenAccount(for: .copilot)) #expect(activeAfter.id == activeBefore.id) #expect(activeAfter.label == "B") #expect(settings.tokenAccounts(for: .copilot).map(\.label) == ["B", "C"]) } @Test func `claude snapshot uses OAuth routing for OAuth token accounts`() { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .claude, label: "OAuth", token: "Bearer sk-ant-oat-account-token") let snapshot = settings.claudeSettingsSnapshot(tokenOverride: nil) #expect(snapshot.usageDataSource == .auto) #expect(snapshot.cookieSource == .off) #expect(snapshot.manualCookieHeader?.isEmpty == true) } @Test func `claude snapshot uses manual cookie routing for session key accounts`() { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .claude, label: "Cookie", token: "sk-ant-session-token") let snapshot = settings.claudeSettingsSnapshot(tokenOverride: nil) #expect(snapshot.usageDataSource == .auto) #expect(snapshot.cookieSource == .manual) #expect(snapshot.manualCookieHeader == "sessionKey=sk-ant-session-token") } @Test func `claude snapshot normalizes config manual cookie input through shared route`() { let settings = Self.makeSettingsStore() settings.claudeCookieSource = .manual settings.claudeCookieHeader = "Cookie: sessionKey=sk-ant-session-token; foo=bar" let snapshot = settings.claudeSettingsSnapshot(tokenOverride: nil) #expect(snapshot.usageDataSource == .auto) #expect(snapshot.cookieSource == .manual) #expect(snapshot.manualCookieHeader == "sessionKey=sk-ant-session-token; foo=bar") } @Test func `claude snapshot does not fall back to config cookie for malformed selected token account`() { let settings = Self.makeSettingsStore() settings.claudeCookieSource = .manual settings.claudeCookieHeader = "Cookie: sessionKey=sk-ant-config-cookie" settings.addTokenAccount(provider: .claude, label: "Malformed", token: "Cookie:") let snapshot = settings.claudeSettingsSnapshot(tokenOverride: nil) #expect(snapshot.cookieSource == .manual) #expect(snapshot.manualCookieHeader?.isEmpty == true) } @Test func `opencode go token accounts force manual cookie routing`() { let settings = Self.makeSettingsStore() settings.addTokenAccount(provider: .opencodego, label: "Go", token: "auth=go-cookie") let snapshot = settings.opencodegoSettingsSnapshot(tokenOverride: nil) #expect(settings.opencodegoCookieSource == .manual) #expect(snapshot.cookieSource == .manual) #expect(snapshot.manualCookieHeader == "auth=go-cookie") } @Test func `opencode go snapshot preserves nil workspace id when settings are unset`() { let settings = Self.makeSettingsStore() let snapshot = settings.opencodegoSettingsSnapshot(tokenOverride: nil) #expect(settings.opencodegoWorkspaceID.isEmpty) #expect(snapshot.workspaceID == nil) } @Test func `token cost usage source detection`() throws { let fileManager = FileManager.default let root = fileManager.temporaryDirectory.appendingPathComponent( "token-cost-\(UUID().uuidString)", isDirectory: true) let codexRoot = root.appendingPathComponent("sessions", isDirectory: true) try fileManager.createDirectory(at: codexRoot, withIntermediateDirectories: true) let codexFile = codexRoot.appendingPathComponent("usage.jsonl") fileManager.createFile(atPath: codexFile.path, contents: Data("{}".utf8)) #expect(SettingsStore.hasAnyTokenCostUsageSources( env: ["CODEX_HOME": root.path], fileManager: fileManager)) let claudeRoot = fileManager.temporaryDirectory.appendingPathComponent( "claude-\(UUID().uuidString)", isDirectory: true) let claudeProjects = claudeRoot.appendingPathComponent("projects", isDirectory: true) try fileManager.createDirectory(at: claudeProjects, withIntermediateDirectories: true) let claudeFile = claudeProjects.appendingPathComponent("usage.jsonl") fileManager.createFile(atPath: claudeFile.path, contents: Data("{}".utf8)) #expect(SettingsStore.hasAnyTokenCostUsageSources( env: ["CLAUDE_CONFIG_DIR": claudeRoot.path], fileManager: fileManager)) let metadataOnlyHome = fileManager.temporaryDirectory.appendingPathComponent( "claude-desktop-metadata-\(UUID().uuidString)", isDirectory: true) let metadataFile = metadataOnlyHome .appendingPathComponent("Library/Application Support/Claude/claude-code-sessions", isDirectory: true) .appendingPathComponent("account-id/org-id/local_session.json", isDirectory: false) try fileManager.createDirectory(at: metadataFile.deletingLastPathComponent(), withIntermediateDirectories: true) try Data(#"{"cliSessionId":"desktop-cli-session"}"#.utf8).write(to: metadataFile) #expect(!SettingsStore.hasAnyTokenCostUsageSources( env: [:], fileManager: fileManager, homeDirectory: metadataOnlyHome)) let desktopHome = fileManager.temporaryDirectory.appendingPathComponent( "claude-desktop-\(UUID().uuidString)", isDirectory: true) let desktopProjects = desktopHome .appendingPathComponent("Library", isDirectory: true) .appendingPathComponent("Application Support", isDirectory: true) .appendingPathComponent("Claude", isDirectory: true) .appendingPathComponent("local-agent-mode-sessions", isDirectory: true) .appendingPathComponent("workspace-id", isDirectory: true) .appendingPathComponent("session-id", isDirectory: true) .appendingPathComponent("local_agent", isDirectory: true) .appendingPathComponent(".claude", isDirectory: true) .appendingPathComponent("projects", isDirectory: true) try fileManager.createDirectory(at: desktopProjects, withIntermediateDirectories: true) let desktopFile = desktopProjects .appendingPathComponent("project-a", isDirectory: true) .appendingPathComponent("session-a.jsonl", isDirectory: false) try fileManager.createDirectory(at: desktopFile.deletingLastPathComponent(), withIntermediateDirectories: true) fileManager.createFile(atPath: desktopFile.path, contents: Data("{}".utf8)) #expect(SettingsStore.hasAnyTokenCostUsageSources( env: [:], fileManager: fileManager, homeDirectory: desktopHome)) let desktopCodeHome = fileManager.temporaryDirectory.appendingPathComponent( "claude-desktop-code-\(UUID().uuidString)", isDirectory: true) let desktopCodeFile = desktopCodeHome .appendingPathComponent("Library/Application Support/Claude/claude-code-sessions", isDirectory: true) .appendingPathComponent("account-id/org-id/.claude/projects/project-a", isDirectory: true) .appendingPathComponent("session-a.jsonl", isDirectory: false) try fileManager.createDirectory( at: desktopCodeFile.deletingLastPathComponent(), withIntermediateDirectories: true) fileManager.createFile(atPath: desktopCodeFile.path, contents: Data("{}".utf8)) #expect(SettingsStore.hasAnyTokenCostUsageSources( env: [:], fileManager: fileManager, homeDirectory: desktopCodeHome)) } @Test func `ensure token loaders execute`() { let settings = Self.makeSettingsStore() settings.ensureZaiAPITokenLoaded() settings.ensureSyntheticAPITokenLoaded() settings.ensureCodexCookieLoaded() settings.ensureClaudeCookieLoaded() settings.ensureCursorCookieLoaded() settings.ensureOpenCodeCookieLoaded() settings.ensureFactoryCookieLoaded() settings.ensureMiniMaxCookieLoaded() settings.ensureMiniMaxAPITokenLoaded() settings.ensureKimiAuthTokenLoaded() settings.ensureKimiK2APITokenLoaded() settings.ensureAugmentCookieLoaded() settings.ensureAmpCookieLoaded() settings.ensureOllamaCookieLoaded() settings.ensureCopilotAPITokenLoaded() settings.ensureTokenAccountsLoaded() #expect(settings.zaiAPIToken.isEmpty) #expect(settings.syntheticAPIToken.isEmpty) } @Test func `keychain disable forces manual cookie sources`() throws { let suite = "SettingsStoreCoverageTests-keychain" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) settings.codexCookieSource = .auto settings.claudeCookieSource = .auto settings.kimiCookieSource = .off settings.debugDisableKeychainAccess = true #expect(settings.codexCookieSource == .manual) #expect(settings.claudeCookieSource == .manual) #expect(settings.kimiCookieSource == .off) } @Test func `claude keychain prompt mode defaults to only on user action`() { let settings = Self.makeSettingsStore() #expect(settings.claudeOAuthKeychainPromptMode == .onlyOnUserAction) } @Test func `claude keychain prompt mode persists across store reload`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-prompt-mode" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let first = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) first.claudeOAuthKeychainPromptMode = .never #expect( defaults.string(forKey: "claudeOAuthKeychainPromptMode") == ClaudeOAuthKeychainPromptMode.never.rawValue) let second = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(second.claudeOAuthKeychainPromptMode == .never) } @Test func `claude keychain prompt mode invalid raw falls back to only on user action`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-prompt-mode-invalid" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set("invalid-mode", forKey: "claudeOAuthKeychainPromptMode") let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(settings.claudeOAuthKeychainPromptMode == .onlyOnUserAction) } @Test func `claude keychain read strategy defaults to security framework`() { let settings = Self.makeSettingsStore() #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) } @Test func `claude keychain read strategy persists across store reload`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-read-strategy" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let first = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) first.claudeOAuthKeychainReadStrategy = .securityFramework #expect( defaults.string(forKey: "claudeOAuthKeychainReadStrategy") == ClaudeOAuthKeychainReadStrategy.securityFramework.rawValue) let second = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(second.claudeOAuthKeychainReadStrategy == .securityFramework) } @Test func `claude legacy security CLI read strategy preserves no prompt intent`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-read-strategy-migration" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set( ClaudeOAuthKeychainReadStrategy.securityCLIExperimental.rawValue, forKey: "claudeOAuthKeychainReadStrategy") let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) #expect(settings.claudeOAuthKeychainPromptMode == .never) #expect(settings.claudeOAuthPromptFreeCredentialsEnabled) #expect( defaults.string(forKey: "claudeOAuthKeychainReadStrategy") == ClaudeOAuthKeychainReadStrategy.securityFramework.rawValue) #expect( defaults.string(forKey: "claudeOAuthKeychainPromptMode") == ClaudeOAuthKeychainPromptMode.never.rawValue) } @Test func `claude legacy security CLI migration preserves explicit prompt policy`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-explicit-prompt-migration" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set( ClaudeOAuthKeychainReadStrategy.securityCLIExperimental.rawValue, forKey: "claudeOAuthKeychainReadStrategy") defaults.set( ClaudeOAuthKeychainPromptMode.always.rawValue, forKey: "claudeOAuthKeychainPromptMode") let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) #expect(settings.claudeOAuthKeychainPromptMode == .always) #expect(!settings.claudeOAuthPromptFreeCredentialsEnabled) } @Test func `claude keychain read strategy invalid raw falls back to security framework`() throws { let suite = "SettingsStoreCoverageTests-claude-keychain-read-strategy-invalid" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) defaults.set("invalid-strategy", forKey: "claudeOAuthKeychainReadStrategy") let configStore = testConfigStore(suiteName: suite) let settings = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) } @Test func `claude prompt free credentials toggle maps to never prompt policy`() { let settings = Self.makeSettingsStore() #expect(settings.claudeOAuthPromptFreeCredentialsEnabled == false) settings.claudeOAuthPromptFreeCredentialsEnabled = true #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) #expect(settings.claudeOAuthKeychainPromptMode == .never) settings.claudeOAuthPromptFreeCredentialsEnabled = false #expect(settings.claudeOAuthKeychainReadStrategy == .securityFramework) #expect(settings.claudeOAuthKeychainPromptMode == .onlyOnUserAction) } @Test func `upsert antigravity oauth account adds and updates active token account`() throws { let settings = Self.makeSettingsStore() let first = AntigravityOAuthCredentials( accessToken: "first-access", refreshToken: "first-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: "user@example.com") let updated = AntigravityOAuthCredentials( accessToken: "updated-access", refreshToken: "first-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_100), email: "user@example.com") settings.upsertAntigravityOAuthAccount(first) settings.upsertAntigravityOAuthAccount(updated) let accounts = settings.tokenAccounts(for: .antigravity) #expect(accounts.count == 1) let account = try #require(accounts.first) #expect(account.label == "user@example.com") #expect(account.externalIdentifier == "user@example.com") #expect(settings.selectedTokenAccount(for: .antigravity)?.id == account.id) let decoded = try #require(AntigravityOAuthCredentialsStore.credentials(fromTokenAccountValue: account.token)) #expect(decoded.accessToken == "updated-access") } @Test func `upsert antigravity oauth account does not merge missing email accounts by fallback label`() { let settings = Self.makeSettingsStore() let first = AntigravityOAuthCredentials( accessToken: "first-access", refreshToken: "first-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: nil) let second = AntigravityOAuthCredentials( accessToken: "second-access", refreshToken: "second-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_100), email: nil) settings.upsertAntigravityOAuthAccount(first) settings.upsertAntigravityOAuthAccount(second) let accounts = settings.tokenAccounts(for: .antigravity) #expect(accounts.count == 2) #expect(accounts.map(\.label) == ["Google Account 1", "Google Account 2"]) #expect(settings.selectedTokenAccount(for: .antigravity)?.id == accounts.last?.id) } @Test func `removing last antigravity oauth account clears matching shared credentials`() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("antigravity-shared-removal-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } let sharedStore = AntigravityOAuthCredentialsStore( fileURL: root.appendingPathComponent("oauth_creds.json")) let credentials = AntigravityOAuthCredentials( accessToken: "shared-access", refreshToken: "shared-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: "user@example.com") try sharedStore.save(credentials) let settings = Self.makeSettingsStore( suiteName: "SettingsStoreCoverageTests-antigravity-remove-shared", antigravityOAuthCredentialsStore: sharedStore) settings.upsertAntigravityOAuthAccount(credentials) let account = try #require(settings.selectedTokenAccount(for: .antigravity)) settings.removeTokenAccount(provider: .antigravity, accountID: account.id) #expect(settings.tokenAccounts(for: .antigravity).isEmpty) #expect(try sharedStore.load() == nil) } @Test func `removing antigravity oauth account preserves freshly reauthenticated credentials`() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("antigravity-shared-reauth-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } let sharedStore = AntigravityOAuthCredentialsStore( fileURL: root.appendingPathComponent("oauth_creds.json")) let removed = AntigravityOAuthCredentials( accessToken: "removed-access", refreshToken: "removed-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: "user@example.com") let refreshed = AntigravityOAuthCredentials( accessToken: "fresh-access", refreshToken: "fresh-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_100), email: "user@example.com") try sharedStore.save(refreshed) let settings = Self.makeSettingsStore( suiteName: "SettingsStoreCoverageTests-antigravity-preserve-reauth", antigravityOAuthCredentialsStore: sharedStore) settings.upsertAntigravityOAuthAccount(removed) let account = try #require(settings.selectedTokenAccount(for: .antigravity)) settings.removeTokenAccount(provider: .antigravity, accountID: account.id) #expect(try sharedStore.load() == refreshed) } @Test func `removing antigravity oauth account preserves different shared credentials`() async throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("antigravity-shared-preserve-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: root) } let sharedStore = AntigravityOAuthCredentialsStore( fileURL: root.appendingPathComponent("oauth_creds.json")) let removed = AntigravityOAuthCredentials( accessToken: "removed-access", refreshToken: "removed-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: "removed@example.com") let shared = AntigravityOAuthCredentials( accessToken: "shared-access", refreshToken: "shared-refresh", expiryDate: Date(timeIntervalSince1970: 1_700_000_000), email: "shared@example.com") try sharedStore.save(shared) let settings = Self.makeSettingsStore( suiteName: "SettingsStoreCoverageTests-antigravity-preserve-shared", antigravityOAuthCredentialsStore: sharedStore) settings.upsertAntigravityOAuthAccount(removed) let account = try #require(settings.selectedTokenAccount(for: .antigravity)) settings.removeTokenAccount(provider: .antigravity, accountID: account.id) #expect(settings.tokenAccounts(for: .antigravity).isEmpty) try await Task.sleep(nanoseconds: 50_000_000) #expect(try sharedStore.load() == shared) } @Test func `weekly progress work days defaults to nil and persists across store reload`() throws { let suite = "SettingsStoreCoverageTests-weekly-progress-work-days" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) let configStore = testConfigStore(suiteName: suite) let fresh = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(fresh.weeklyProgressWorkDays == nil) fresh.weeklyProgressWorkDays = 5 #expect(defaults.object(forKey: "weeklyProgressWorkDays") as? Int == 5) let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded.weeklyProgressWorkDays == 5) fresh.weeklyProgressWorkDays = 4 #expect(reloaded.weeklyProgressWorkDays == 5) let reloaded2 = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded2.weeklyProgressWorkDays == 4) reloaded2.weeklyProgressWorkDays = 7 let reloaded3 = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded3.weeklyProgressWorkDays == 7) reloaded3.weeklyProgressWorkDays = nil #expect(defaults.object(forKey: "weeklyProgressWorkDays") == nil) let reloaded4 = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) #expect(reloaded4.weeklyProgressWorkDays == nil) } private static func makeSettingsStore( suiteName: String = "SettingsStoreCoverageTests", antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore = AntigravityOAuthCredentialsStore()) -> SettingsStore { let defaults = UserDefaults(suiteName: suiteName)! defaults.removePersistentDomain(forName: suiteName) defaults.set(false, forKey: "debugDisableKeychainAccess") let configStore = testConfigStore(suiteName: suiteName) return Self.makeSettingsStore( userDefaults: defaults, configStore: configStore, antigravityOAuthCredentialsStore: antigravityOAuthCredentialsStore) } private static func makeSettingsStore( userDefaults: UserDefaults, configStore: CodexBarConfigStore, antigravityOAuthCredentialsStore: AntigravityOAuthCredentialsStore = AntigravityOAuthCredentialsStore()) -> SettingsStore { SettingsStore( userDefaults: userDefaults, configStore: configStore, zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore(), codexCookieStore: InMemoryCookieHeaderStore(), claudeCookieStore: InMemoryCookieHeaderStore(), cursorCookieStore: InMemoryCookieHeaderStore(), opencodeCookieStore: InMemoryCookieHeaderStore(), factoryCookieStore: InMemoryCookieHeaderStore(), minimaxCookieStore: InMemoryMiniMaxCookieStore(), minimaxAPITokenStore: InMemoryMiniMaxAPITokenStore(), kimiTokenStore: InMemoryKimiTokenStore(), kimiK2TokenStore: InMemoryKimiK2TokenStore(), augmentCookieStore: InMemoryCookieHeaderStore(), ampCookieStore: InMemoryCookieHeaderStore(), copilotTokenStore: InMemoryCopilotTokenStore(), tokenAccountStore: InMemoryTokenAccountStore(), antigravityOAuthCredentialsStore: antigravityOAuthCredentialsStore) } private static func waitForSharedAntigravityCredentials( in store: AntigravityOAuthCredentialsStore, matches predicate: (AntigravityOAuthCredentials?) -> Bool) async throws -> AntigravityOAuthCredentials? { for _ in 0..<100 { let credentials = try store.load() if predicate(credentials) { return credentials } try await Task.sleep(nanoseconds: 10_000_000) } return try store.load() } }