From aaa436653386c7b00414ddd176348c2d775b88d0 Mon Sep 17 00:00:00 2001 From: Ashlee Radka Date: Tue, 24 Feb 2026 14:39:00 -0500 Subject: [PATCH] fix: clear stale pairing overrides during v4 migration Co-Authored-By: Claude --- clients/ios/App/AppDelegate.swift | 7 ++++ .../vellum-assistant/App/AppDelegate.swift | 32 +++++++++++++++++++ ...SettingsStoreOverrideResolutionTests.swift | 25 ++++++--------- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/clients/ios/App/AppDelegate.swift b/clients/ios/App/AppDelegate.swift index 6f3eee3ba10..1531cf991e3 100644 --- a/clients/ios/App/AppDelegate.swift +++ b/clients/ios/App/AppDelegate.swift @@ -181,6 +181,13 @@ class AppDelegate: NSObject, UIApplicationDelegate { defaults.removeObject(forKey: "devLocalPairingEnabled") defaults.removeObject(forKey: "iosPairingUseOverride") + // Clear stale override values — the isOverrideEnabled gate was removed + // in M9, so any non-empty override now applies unconditionally. Users + // who had values typed in but the toggle OFF would have those stale + // values silently activate on upgrade. + defaults.removeObject(forKey: PairingConfiguration.gatewayOverrideKey) + defaults.removeObject(forKey: PairingConfiguration.tokenOverrideKey) + // Mark migration as done defaults.set(true, forKey: Self.pairingV4MigrationKey) diff --git a/clients/macos/vellum-assistant/App/AppDelegate.swift b/clients/macos/vellum-assistant/App/AppDelegate.swift index 0baebd897f7..3b7fc4182ac 100644 --- a/clients/macos/vellum-assistant/App/AppDelegate.swift +++ b/clients/macos/vellum-assistant/App/AppDelegate.swift @@ -131,6 +131,12 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { // renders EmptyView — we handle settings in the main window panel). UserDefaults.standard.removeObject(forKey: "NSWindow Frame com_apple_SwiftUI_Settings_window") + // Migration: clear stale pairing override values when the toggle was OFF. + // M9 removed the isOverrideEnabled gate — any non-empty override now + // applies unconditionally. Users who had override values typed in but + // the toggle OFF would have those stale values silently activate. + migratePairingOverridesIfNeeded() + if let envPath = FeatureFlagManager.findRepoEnvFile() { FeatureFlagManager.shared.loadFromFile(at: envPath) } @@ -587,6 +593,32 @@ public final class AppDelegate: NSObject, NSApplicationDelegate { log.info("Configured HTTP transport for remote assistant \(assistant.assistantId) at \(runtimeUrl, privacy: .public)") } + // MARK: - Pairing Override Migration + + /// Key that tracks whether the pairing override migration has run. + private static let pairingOverrideMigrationKey = "pairing_override_migration_done" + + /// Clears stale gateway/token override values when the old toggle was OFF + /// (or absent). Runs once; the flag persists across future launches. + private func migratePairingOverridesIfNeeded() { + let defaults = UserDefaults.standard + guard !defaults.bool(forKey: Self.pairingOverrideMigrationKey) else { return } + + // If the legacy toggle was off (false or absent), the user did not + // intend for overrides to be active. Clear them so they don't + // silently take effect now that the gate is removed. + let overrideWasEnabled = defaults.bool(forKey: "iosPairingUseOverride") + if !overrideWasEnabled { + defaults.removeObject(forKey: PairingConfiguration.gatewayOverrideKey) + defaults.removeObject(forKey: PairingConfiguration.tokenOverrideKey) + } + + // Clean up the legacy toggle key itself — no longer used. + defaults.removeObject(forKey: "iosPairingUseOverride") + + defaults.set(true, forKey: Self.pairingOverrideMigrationKey) + } + func setupDaemonClient() { guard !hasSetupDaemon else { return } hasSetupDaemon = true diff --git a/clients/macos/vellum-assistantTests/SettingsStoreOverrideResolutionTests.swift b/clients/macos/vellum-assistantTests/SettingsStoreOverrideResolutionTests.swift index eb42b844b91..8008e58b414 100644 --- a/clients/macos/vellum-assistantTests/SettingsStoreOverrideResolutionTests.swift +++ b/clients/macos/vellum-assistantTests/SettingsStoreOverrideResolutionTests.swift @@ -7,7 +7,6 @@ final class SettingsStoreOverrideResolutionTests: XCTestCase { // Each test manipulates UserDefaults keys that the override resolution // reads. Clean up after each test to avoid cross-contamination. override func tearDown() { - UserDefaults.standard.removeObject(forKey: "iosPairingUseOverride") UserDefaults.standard.removeObject(forKey: "iosPairingGatewayOverride") UserDefaults.standard.removeObject(forKey: "iosPairingTokenOverride") super.tearDown() @@ -15,30 +14,26 @@ final class SettingsStoreOverrideResolutionTests: XCTestCase { // MARK: - iOS Gateway URL - func testIosGatewayReturnsGlobalWhenOverrideOff() { - UserDefaults.standard.set(false, forKey: "iosPairingUseOverride") + func testIosGatewayReturnsOverrideWhenNonEmpty() { UserDefaults.standard.set("https://custom.example.com", forKey: "iosPairingGatewayOverride") let store = SettingsStore() - // Simulate global URL being set via IPC response store.ingressPublicBaseUrl = "https://global.example.com" - XCTAssertEqual(store.resolvedIosGatewayUrl, "https://global.example.com") + XCTAssertEqual(store.resolvedIosGatewayUrl, "https://custom.example.com") } - func testIosGatewayReturnsOverrideWhenOverrideOn() { - UserDefaults.standard.set(true, forKey: "iosPairingUseOverride") - UserDefaults.standard.set("https://custom.example.com", forKey: "iosPairingGatewayOverride") + func testIosGatewayFallsBackToGlobalWhenOverrideEmpty() { + UserDefaults.standard.set("", forKey: "iosPairingGatewayOverride") let store = SettingsStore() store.ingressPublicBaseUrl = "https://global.example.com" - XCTAssertEqual(store.resolvedIosGatewayUrl, "https://custom.example.com") + XCTAssertEqual(store.resolvedIosGatewayUrl, "https://global.example.com") } - func testIosGatewayFallsBackToGlobalWhenOverrideOnButEmpty() { - UserDefaults.standard.set(true, forKey: "iosPairingUseOverride") - UserDefaults.standard.set("", forKey: "iosPairingGatewayOverride") + func testIosGatewayFallsBackToGlobalWhenOverrideWhitespaceOnly() { + UserDefaults.standard.set(" ", forKey: "iosPairingGatewayOverride") let store = SettingsStore() store.ingressPublicBaseUrl = "https://global.example.com" @@ -46,10 +41,8 @@ final class SettingsStoreOverrideResolutionTests: XCTestCase { XCTAssertEqual(store.resolvedIosGatewayUrl, "https://global.example.com") } - func testIosGatewayFallsBackToGlobalWhenOverrideOnAndWhitespaceOnly() { - UserDefaults.standard.set(true, forKey: "iosPairingUseOverride") - UserDefaults.standard.set(" ", forKey: "iosPairingGatewayOverride") - + func testIosGatewayFallsBackToGlobalWhenOverrideAbsent() { + // No override key set at all let store = SettingsStore() store.ingressPublicBaseUrl = "https://global.example.com"