From 9658fb25ff26d2c0af61fc369f6235d59addeae3 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:23:48 -0700 Subject: [PATCH] fix(accounts): align auto runtime consumers --- .../runtime-auth-service.test.ts | 66 +++++++++++++++++++ .../claude-accounts/runtime-auth-service.ts | 6 +- src/main/persistence.test.ts | 2 + src/main/persistence.ts | 4 +- .../claude-rate-limit-target.test.ts | 19 +++++- .../rate-limits/claude-rate-limit-target.ts | 10 ++- .../codex-rate-limit-target.test.ts | 17 +++++ .../rate-limits/codex-rate-limit-target.ts | 10 ++- .../src/components/settings/AccountsPane.tsx | 8 +-- .../src/components/status-bar/StatusBar.tsx | 19 +++--- .../status-bar-runtime-groups.test.ts | 22 ++++++- src/shared/local-account-runtime.ts | 8 +-- src/shared/types.ts | 4 +- 13 files changed, 158 insertions(+), 37 deletions(-) diff --git a/src/main/claude-accounts/runtime-auth-service.test.ts b/src/main/claude-accounts/runtime-auth-service.test.ts index c6c2043deab..a78bc99ec1c 100644 --- a/src/main/claude-accounts/runtime-auth-service.test.ts +++ b/src/main/claude-accounts/runtime-auth-service.test.ts @@ -3537,6 +3537,72 @@ describe('ClaudeRuntimeAuthService', () => { }) }) + it('uses the global WSL runtime for untargeted Claude preparation under auto', async () => { + setPlatform('win32') + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => null, + toWindowsWslPath: (value: string) => value + })) + const ubuntuAuthPath = createManagedClaudeAuth( + testState.userDataDir, + 'ubuntu-account', + createClaudeCredentialsJson('ubuntu@example.com', 'ubuntu-token') + ) + const settings = createSettings({ + localAccountRuntime: 'auto', + localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' }, + claudeManagedAccounts: [ + createClaudeAccount('ubuntu-account', ubuntuAuthPath, { + managedAuthRuntime: 'wsl', + wslDistro: 'Ubuntu', + wslLinuxAuthPath: '/home/alice/.local/share/orca/claude-accounts/ubuntu/auth' + }) + ], + activeClaudeManagedAccountId: null, + activeClaudeManagedAccountIdsByRuntime: { + host: null, + wsl: { Ubuntu: 'ubuntu-account' } + } + }) + const store = createStore(settings) + + const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service') + const service = new ClaudeRuntimeAuthService(store as never) + const preparation = await service.prepareForClaudeLaunch() + + expect(preparation).toMatchObject({ + runtime: 'wsl', + wslDistro: 'Ubuntu', + provenance: 'managed:ubuntu-account:wsl:Ubuntu' + }) + }) + + it('ignores a persisted WSL account-runtime pin on non-Windows hosts', async () => { + setPlatform('darwin') + vi.doMock('../wsl', () => ({ + getDefaultWslDistro: () => 'Ubuntu', + getWslHome: () => null, + toWindowsWslPath: (value: string) => value + })) + const settings = createSettings({ + localAccountRuntime: 'wsl', + localAccountWslDistro: 'Ubuntu' + }) + const store = createStore(settings) + + const { ClaudeRuntimeAuthService } = await import('./runtime-auth-service') + const service = new ClaudeRuntimeAuthService(store as never) + const preparation = await service.prepareForClaudeLaunch() + + expect(preparation).toMatchObject({ + runtime: 'host', + wslDistro: null, + provenance: 'system', + stripAuthEnv: false + }) + }) + it('keeps untargeted Claude preparation on host when account runtime is host', async () => { setPlatform('win32') vi.doMock('../wsl', () => ({ diff --git a/src/main/claude-accounts/runtime-auth-service.ts b/src/main/claude-accounts/runtime-auth-service.ts index febc7d80807..58328222528 100644 --- a/src/main/claude-accounts/runtime-auth-service.ts +++ b/src/main/claude-accounts/runtime-auth-service.ts @@ -684,11 +684,9 @@ export class ClaudeRuntimeAuthService { private getDefaultAccountSelectionTarget( settings = this.store.getSettings() ): ClaudeAccountSelectionTarget { - // Why: auth defaults follow the resolved account runtime ('auto' tracks the - // global WSL project default), not legacy terminal WSL settings that can - // outlive the Terminal UI. + // Why: Windows auth follows the resolved account runtime; stale cross-platform WSL pins must stay local-host. const resolved = resolveLocalAccountRuntimeTarget(settings) - if (resolved.runtime === 'wsl') { + if (process.platform === 'win32' && resolved.runtime === 'wsl') { return { runtime: 'wsl', wslDistro: resolved.wslDistro } } return { runtime: 'host' } diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index ae18f25bb96..43f94be0942 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -618,6 +618,8 @@ describe('Store', () => { expect(settings.terminalScrollSensitivity).toBe(1.15) expect(settings.terminalFastScrollSensitivity).toBe(5) expect(settings.terminalTuiScrollSensitivity).toBe(1) + expect(settings.localAccountRuntime).toBe('auto') + expect(settings.localAccountRuntimeDefaultedToAutoForAllUsers).toBe(true) expect(settings.terminalTuiScrollSensitivityDefaultedToOne).toBe(true) expect(settings.terminalUseSeparateLightTheme).toBe(true) expect(settings.rightSidebarOpenByDefault).toBe(true) diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 0eb59dbec32..166d03a5347 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -2959,9 +2959,7 @@ export class Store { ) { this.loadNeedsSave = true } - // Why (#9537): the legacy hard 'host' default pinned account detection to - // Windows even for WSL projects. Flip the untouched default to 'auto' once - // so it follows localWindowsRuntimeDefault; an explicit 'wsl' pin survives. + // Why (#9537): migrate the indistinguishable legacy host default once so WSL-default users follow their runtime. const localAccountRuntimeAlreadyMigrated = parsed.settings?.localAccountRuntimeDefaultedToAutoForAllUsers === true const migratedLocalAccountRuntime: GlobalSettings['localAccountRuntime'] = diff --git a/src/main/rate-limits/claude-rate-limit-target.test.ts b/src/main/rate-limits/claude-rate-limit-target.test.ts index b051d84bda5..bc7f1861466 100644 --- a/src/main/rate-limits/claude-rate-limit-target.test.ts +++ b/src/main/rate-limits/claude-rate-limit-target.test.ts @@ -103,7 +103,7 @@ describe('getInitialClaudeRateLimitTarget', () => { ).toEqual({ runtime: 'wsl', wslDistro: 'Ubuntu' }) }) - it("auto follows the global WSL project runtime default", () => { + it('auto follows the global WSL project runtime default', () => { expect( getInitialClaudeRateLimitTarget( { @@ -129,6 +129,23 @@ describe('getInitialClaudeRateLimitTarget', () => { ).toEqual({ runtime: 'host' }) }) + it('does not let a stale WSL-only selection override an auto host target', () => { + expect( + getInitialClaudeRateLimitTarget( + { + ...getDefaultSettings('/tmp'), + localAccountRuntime: 'auto', + localWindowsRuntimeDefault: { kind: 'windows-host' }, + activeClaudeManagedAccountIdsByRuntime: { + host: null, + wsl: { Ubuntu: 'wsl-account-1' } + } + }, + 'win32' + ) + ).toEqual({ runtime: 'host' }) + }) + it('keeps explicit host runtime on host', () => { expect( getInitialClaudeRateLimitTarget( diff --git a/src/main/rate-limits/claude-rate-limit-target.ts b/src/main/rate-limits/claude-rate-limit-target.ts index 4af666d2817..28d4f60d8a6 100644 --- a/src/main/rate-limits/claude-rate-limit-target.ts +++ b/src/main/rate-limits/claude-rate-limit-target.ts @@ -1,4 +1,5 @@ import type { GlobalSettings } from '../../shared/types' +import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime' import { getClaudeWslSelectionKey, normalizeClaudeRuntimeSelection, @@ -36,9 +37,14 @@ export function getInitialClaudeRateLimitTarget( getSingleSelectedWslDistro(settings) } } + if (settings.localAccountRuntime === 'auto') { + const target = resolveLocalAccountRuntimeTarget(settings, platform) + return target.runtime === 'wsl' + ? { runtime: 'wsl', wslDistro: target.wslDistro } + : { runtime: 'host' } + } - // 'auto' (the default) and legacy-unset settings fall through here so account - // detection follows the global WSL project runtime instead of pinning host. + // Why: pre-setting profiles used account selection as their startup fallback. const projectRuntimeTarget = getProjectRuntimeRateLimitTarget(settings, platform) if (projectRuntimeTarget) { return projectRuntimeTarget diff --git a/src/main/rate-limits/codex-rate-limit-target.test.ts b/src/main/rate-limits/codex-rate-limit-target.test.ts index b790d79aa89..6ac328514c4 100644 --- a/src/main/rate-limits/codex-rate-limit-target.test.ts +++ b/src/main/rate-limits/codex-rate-limit-target.test.ts @@ -129,6 +129,23 @@ describe('getInitialCodexRateLimitTarget', () => { ).toEqual({ runtime: 'host' }) }) + it('does not let a stale WSL-only selection override an auto host target', () => { + expect( + getInitialCodexRateLimitTarget( + { + ...getDefaultSettings('/tmp'), + localAccountRuntime: 'auto', + localWindowsRuntimeDefault: { kind: 'windows-host' }, + activeCodexManagedAccountIdsByRuntime: { + host: null, + wsl: { Ubuntu: 'wsl-account-1' } + } + }, + 'win32' + ) + ).toEqual({ runtime: 'host' }) + }) + it('keeps explicit host runtime on host', () => { expect( getInitialCodexRateLimitTarget( diff --git a/src/main/rate-limits/codex-rate-limit-target.ts b/src/main/rate-limits/codex-rate-limit-target.ts index 9e23547fadb..4d7bfe4e360 100644 --- a/src/main/rate-limits/codex-rate-limit-target.ts +++ b/src/main/rate-limits/codex-rate-limit-target.ts @@ -1,4 +1,5 @@ import type { GlobalSettings } from '../../shared/types' +import { resolveLocalAccountRuntimeTarget } from '../../shared/local-account-runtime' import { getWslSelectionKey, normalizeCodexRuntimeSelection, @@ -36,9 +37,14 @@ export function getInitialCodexRateLimitTarget( getSingleSelectedWslDistro(settings) } } + if (settings.localAccountRuntime === 'auto') { + const target = resolveLocalAccountRuntimeTarget(settings, platform) + return target.runtime === 'wsl' + ? { runtime: 'wsl', wslDistro: target.wslDistro } + : { runtime: 'host' } + } - // 'auto' (the default) and legacy-unset settings fall through here so account - // detection follows the global WSL project runtime instead of pinning host. + // Why: pre-setting profiles used account selection as their startup fallback. const projectRuntimeTarget = getProjectRuntimeRateLimitTarget(settings, platform) if (projectRuntimeTarget) { return projectRuntimeTarget diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx index a532777536a..ed485caab80 100644 --- a/src/renderer/src/components/settings/AccountsPane.tsx +++ b/src/renderer/src/components/settings/AccountsPane.tsx @@ -10,6 +10,7 @@ import type { GlobalSettings } from '../../../../shared/types' import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime' +import { getRendererAppPlatform } from '../../lib/renderer-app-platform' import { Badge } from '../ui/badge' import { Button } from '../ui/button' import { Input } from '../ui/input' @@ -267,11 +268,8 @@ function getSelectedAccountRuntime( wslDistros: string[], wslCapabilitiesLoading: boolean ): LocalAccountRuntime { - // Why: 'auto' (the default) follows the global WSL project runtime, so resolve - // it to a concrete host/wsl position for the toggle instead of only honoring an - // explicit 'wsl' pin. The renderer is sandboxed, so derive platform from the UA. - const platform = navigator.userAgent.includes('Windows') ? 'win32' : 'linux' - const resolvedRuntime = resolveLocalAccountRuntimeTarget(settings, platform) + // Why: the two-option control displays the concrete target behind the persisted auto policy. + const resolvedRuntime = resolveLocalAccountRuntimeTarget(settings, getRendererAppPlatform()) if (wslSupportedPlatform && resolvedRuntime.runtime === 'wsl') { if (!wslAvailable && !wslCapabilitiesLoading) { return { diff --git a/src/renderer/src/components/status-bar/StatusBar.tsx b/src/renderer/src/components/status-bar/StatusBar.tsx index c9ed283968a..beb7d787a5e 100644 --- a/src/renderer/src/components/status-bar/StatusBar.tsx +++ b/src/renderer/src/components/status-bar/StatusBar.tsx @@ -46,6 +46,7 @@ import type { RateLimitWindow } from '../../../../shared/rate-limit-types' import { resolveLocalAccountRuntimeTarget } from '../../../../shared/local-account-runtime' +import { getRendererAppPlatform } from '../../lib/renderer-app-platform' import { ProviderIcon, ProviderPanel, @@ -196,24 +197,24 @@ function toCodexStatusRuntimeTarget( export function getStatusBarPreferredWslDistro( settings: GlobalSettings | null | undefined, - wslDistros: string[] + wslDistros: string[], + platform: NodeJS.Platform = getRendererAppPlatform() ): string | null { - const configuredDistro = settings?.localAccountWslDistro?.trim() || null - if (configuredDistro) { - return configuredDistro + if (settings) { + const target = resolveLocalAccountRuntimeTarget(settings, platform) + if (target.runtime === 'wsl' && target.wslDistro) { + return target.wslDistro + } } return wslDistros.length === 1 ? wslDistros[0] : null } function shouldIncludeSettingsWslRuntime(settings: GlobalSettings | null | undefined): boolean { - // Why: 'auto' (the default) surfaces the WSL group when the global project - // runtime is WSL, matching where account detection now reads from. The renderer - // is sandboxed without process.platform, so derive it from the user agent. if (!settings) { return false } - const platform = navigator.userAgent.includes('Windows') ? 'win32' : 'linux' - return resolveLocalAccountRuntimeTarget(settings, platform).runtime === 'wsl' + // Why: the fallback group must match the concrete runtime used for account polling. + return resolveLocalAccountRuntimeTarget(settings, getRendererAppPlatform()).runtime === 'wsl' } function getSingleConcreteCodexWslDistro(state: CodexRateLimitAccountsState): string | null { diff --git a/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts b/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts index db8b96abaa2..262cf1595f1 100644 --- a/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts +++ b/src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts @@ -116,10 +116,12 @@ describe('status bar runtime switch groups', () => { expect( getStatusBarPreferredWslDistro( { + localAccountRuntime: 'wsl', localAccountWslDistro: null, terminalWindowsWslDistro: 'Debian' } as GlobalSettings, - ['Ubuntu'] + ['Ubuntu'], + 'win32' ) ).toBe('Ubuntu') }) @@ -128,14 +130,30 @@ describe('status bar runtime switch groups', () => { expect( getStatusBarPreferredWslDistro( { + localAccountRuntime: 'wsl', localAccountWslDistro: 'Fedora', terminalWindowsWslDistro: 'Debian' } as GlobalSettings, - ['Ubuntu'] + ['Ubuntu'], + 'win32' ) ).toBe('Fedora') }) + it('uses the auto runtime distro instead of a stale account-runtime distro', () => { + expect( + getStatusBarPreferredWslDistro( + { + localAccountRuntime: 'auto', + localAccountWslDistro: 'Fedora', + localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } + } as GlobalSettings, + ['Fedora', 'Ubuntu'], + 'win32' + ) + ).toBe('Ubuntu') + }) + it('labels the host account group with the active remote server name', () => { const state: CodexRateLimitAccountsState = { accounts: [], diff --git a/src/shared/local-account-runtime.ts b/src/shared/local-account-runtime.ts index ab3fc59c926..2e2b8b000da 100644 --- a/src/shared/local-account-runtime.ts +++ b/src/shared/local-account-runtime.ts @@ -11,13 +11,7 @@ type LocalAccountRuntimeSettings = Pick< 'localAccountRuntime' | 'localAccountWslDistro' | 'localWindowsRuntimeDefault' > -/** - * Resolves where provider accounts are inspected (Windows host vs a WSL distro). - * - * `'host'`/`'wsl'` are explicit pins; `'auto'` (the default) follows the global - * Windows runtime default so a WSL project surfaces WSL accounts instead of the - * Windows-host ones. Non-Windows platforms have no WSL, so always resolve host. - */ +/** Resolves the persisted account policy to a concrete host or WSL target. */ export function resolveLocalAccountRuntimeTarget( settings: LocalAccountRuntimeSettings, platform: NodeJS.Platform = process.platform diff --git a/src/shared/types.ts b/src/shared/types.ts index 4fe88a4da29..0311ce3d0eb 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -2653,10 +2653,10 @@ export type GlobalSettings = { terminalWindowsShell: string /** Pins the WSL distro for terminals/agent scans instead of WSL's current global default. */ terminalWindowsWslDistro?: string | null - /** Account/auth location independent from the terminal shell (e.g. WSL terminals but Windows-scoped accounts). 'auto' follows the global Windows runtime default; 'host'/'wsl' pin it explicitly. */ + /** Account/auth location; auto follows the global Windows runtime while host/wsl pin it. */ localAccountRuntime: 'auto' | 'host' | 'wsl' localAccountWslDistro?: string | null - /** One-shot guard: flips the legacy hard 'host' default to 'auto' once so account detection follows the WSL project runtime; preserves explicit 'wsl'. */ + /** One-shot guard for migrating the legacy host default to auto. */ localAccountRuntimeDefaultedToAutoForAllUsers?: boolean /** Independent from the terminal shell so users can inspect Windows vs WSL agent PATH state without changing it. */ localAgentRuntime?: 'host' | 'wsl'