From a32fe5d82397edf2750d235cf4090154ae0858ce Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 15 Jun 2026 18:35:28 -0700 Subject: [PATCH] Refresh Windows PATH for local preflight probes (#5442) Co-authored-by: Neil --- src/main/ipc/preflight-local-env.ts | 12 +++++++++ src/main/ipc/preflight.test.ts | 39 +++++++++++++++++++++++++++-- src/main/ipc/preflight.ts | 5 +++- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/main/ipc/preflight-local-env.ts diff --git a/src/main/ipc/preflight-local-env.ts b/src/main/ipc/preflight-local-env.ts new file mode 100644 index 000000000..30aaf9feb --- /dev/null +++ b/src/main/ipc/preflight-local-env.ts @@ -0,0 +1,12 @@ +import { mergePersistedWindowsPath } from '../pty/windows-environment-path' + +export function buildLocalPreflightEnv(): NodeJS.ProcessEnv | undefined { + if (process.platform !== 'win32') { + return undefined + } + const env = { ...process.env } + // Why: newly installed CLIs update persisted Windows Path, but the running + // Electron process keeps its old environment until we merge it explicitly. + mergePersistedWindowsPath(env) + return env +} diff --git a/src/main/ipc/preflight.test.ts b/src/main/ipc/preflight.test.ts index 311affa59..462df8936 100644 --- a/src/main/ipc/preflight.test.ts +++ b/src/main/ipc/preflight.test.ts @@ -12,7 +12,8 @@ const { getBitbucketAuthStatusMock, getAzureDevOpsAuthStatusMock, getGiteaAuthStatusMock, - resolveCliCommandsMock + resolveCliCommandsMock, + mergePersistedWindowsPathMock } = vi.hoisted(() => ({ handleMock: vi.fn(), execFileMock: vi.fn(), @@ -23,7 +24,8 @@ const { getBitbucketAuthStatusMock: vi.fn(), getAzureDevOpsAuthStatusMock: vi.fn(), getGiteaAuthStatusMock: vi.fn(), - resolveCliCommandsMock: vi.fn() + resolveCliCommandsMock: vi.fn(), + mergePersistedWindowsPathMock: vi.fn() })) vi.mock('electron', () => ({ @@ -51,6 +53,10 @@ vi.mock('../codex-cli/command', () => ({ resolveCliCommands: resolveCliCommandsMock })) +vi.mock('../pty/windows-environment-path', () => ({ + mergePersistedWindowsPath: mergePersistedWindowsPathMock +})) + vi.mock('./ssh', () => ({ getActiveMultiplexer: getActiveMultiplexerMock })) @@ -104,6 +110,7 @@ describe('preflight', () => { getBitbucketAuthStatusMock.mockReset() getAzureDevOpsAuthStatusMock.mockReset() getGiteaAuthStatusMock.mockReset() + mergePersistedWindowsPathMock.mockReset() // Why: existing tests should keep treating `which` as the only source // unless a case explicitly exercises the install-dir fallback. resolveCliCommandsMock.mockReset() @@ -304,6 +311,34 @@ describe('preflight', () => { ) }) + it('uses the persisted Windows Path when probing host CLIs', async () => { + Object.defineProperty(process, 'platform', { + configurable: true, + value: 'win32' + }) + mergePersistedWindowsPathMock.mockImplementation((env: Record) => { + env.Path = 'C:\\Windows\\System32;C:\\Program Files\\GitHub CLI' + }) + execFileAsyncMock + .mockResolvedValueOnce({ stdout: 'git version 2.0.0\n' }) + .mockResolvedValueOnce({ stdout: 'gh version 2.0.0\n' }) + .mockResolvedValueOnce({ stdout: 'glab version 1.92.1\n' }) + .mockResolvedValueOnce({ stdout: 'github.com\n - Active account: true\n' }) + .mockResolvedValueOnce({ stdout: 'Logged in to gitlab.com\n' }) + + const status = await runPreflightCheck() + + expect(status.gh).toEqual({ installed: true, authenticated: true }) + expect(mergePersistedWindowsPathMock).toHaveBeenCalled() + expect(execFileAsyncMock).toHaveBeenNthCalledWith(2, 'gh', ['--version'], { + encoding: 'utf-8', + timeout: 5000, + env: expect.objectContaining({ + Path: 'C:\\Windows\\System32;C:\\Program Files\\GitHub CLI' + }) + }) + }) + it('times out hung WSL preflight probes', async () => { vi.useFakeTimers() try { diff --git a/src/main/ipc/preflight.ts b/src/main/ipc/preflight.ts index 82981552c..a2e7130b7 100644 --- a/src/main/ipc/preflight.ts +++ b/src/main/ipc/preflight.ts @@ -13,6 +13,7 @@ import { getActiveMultiplexer } from './ssh' import { detectWslCommandsOnPath, type WslPreflightTarget } from './preflight-wsl-agent-detection' import { runPreflightCommandInWsl } from './preflight-wsl-command' import { detectCommandsInInstallDirs } from './local-agent-install-dir-detection' +import { buildLocalPreflightEnv } from './preflight-local-env' const execFileAsync = promisify(execFile) const PREFLIGHT_COMMAND_TIMEOUT_MS = 5000 @@ -91,9 +92,11 @@ async function execLocalPreflightCommand( command: string, args: string[] ): Promise { + const env = buildLocalPreflightEnv() const commandPromise = execFileAsync(command, args, { encoding: 'utf-8', - timeout: PREFLIGHT_COMMAND_TIMEOUT_MS + timeout: PREFLIGHT_COMMAND_TIMEOUT_MS, + ...(env ? { env } : {}) }) as Promise return withPreflightTimeout(command, commandPromise)