fix: WSL agent detection broken for zsh login shells (#5325) (#5337)

This commit is contained in:
Jinwoo Hong 2026-06-13 18:17:17 -07:00 committed by GitHub
parent 6312f067cd
commit ac2cf1dbd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 93 additions and 1 deletions

View File

@ -0,0 +1,88 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, execFileAsyncMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
execFileAsyncMock: vi.fn()
}))
vi.mock('child_process', () => {
const execFileWithPromisify = Object.assign(execFileMock, {
[Symbol.for('nodejs.util.promisify.custom')]: execFileAsyncMock
})
return {
execFile: execFileWithPromisify,
spawn: vi.fn()
}
})
import { detectWslCommandsOnPath } from './preflight-wsl-agent-detection'
function lastShCommandPayload(): string {
const call = execFileAsyncMock.mock.calls.at(-1)
expect(call).toBeDefined()
const [file, args] = call as [string, string[]]
expect(file).toBe('wsl.exe')
// args: [...distroArgs, '--', 'sh', '-c', <payload>]
return args.at(-1) as string
}
describe('detectWslCommandsOnPath', () => {
beforeEach(() => {
execFileAsyncMock.mockReset()
})
afterEach(() => {
vi.restoreAllMocks()
})
it('builds a probe script with no `fi done` (zsh parse error) sequence', async () => {
execFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
await detectWslCommandsOnPath({ distro: 'Ubuntu' }, ['claude'])
const payload = lastShCommandPayload()
// Why: zsh aborts on `fi done` — the loop body and `done` must be separated
// by a newline. Regression guard for issue #5325.
expect(payload).not.toContain('fi done')
expect(payload).toContain('fi\ndone')
})
it('parses detected commands from prefixed stdout', async () => {
execFileAsyncMock.mockResolvedValue({
stdout:
'__ORCA_AGENT_PATH__claude\t/usr/bin/claude\n' +
'__ORCA_AGENT_PATH__codex\t/home/user/.local/bin/codex\n',
stderr: ''
})
const found = await detectWslCommandsOnPath({ distro: 'Ubuntu' }, ['claude', 'codex'])
expect(found).toEqual(new Set(['claude', 'codex']))
})
it('ignores commands whose resolved path is not absolute', async () => {
execFileAsyncMock.mockResolvedValue({
stdout: '__ORCA_AGENT_PATH__claude\tclaude\n',
stderr: ''
})
const found = await detectWslCommandsOnPath({ distro: 'Ubuntu' }, ['claude'])
expect(found).toEqual(new Set())
})
it('returns an empty set when the probe fails (e.g. shell parse error)', async () => {
execFileAsyncMock.mockRejectedValue(new Error("zsh:1: parse error near `done'"))
const found = await detectWslCommandsOnPath({ distro: 'Ubuntu' }, ['claude'])
expect(found).toEqual(new Set())
})
it('skips the probe entirely when no commands are requested', async () => {
const found = await detectWslCommandsOnPath({ distro: 'Ubuntu' }, [])
expect(found).toEqual(new Set())
expect(execFileAsyncMock).not.toHaveBeenCalled()
})
})

View File

@ -24,13 +24,17 @@ export async function detectWslCommandsOnPath(
}
const commandList = uniqueCommands.map(shellQuote).join(' ')
// Why: join with newlines, not spaces. zsh treats `fi done` as a parse error
// (it needs a separator before `done`); the login shell may be zsh, so a
// space-joined script silently fails for every agent. Newlines are valid
// statement separators in every POSIX shell and zsh.
const script = [
`for cmd in ${commandList}; do`,
'if resolved=$(command -v "$cmd" 2>/dev/null); then',
`printf '${WSL_AGENT_DETECTION_PREFIX}%s\\t%s\\n' "$cmd" "$resolved";`,
'fi',
'done'
].join(' ')
].join('\n')
try {
// Why: WSL cold-start plus many parallel wsl.exe probes can timeout and