Files
orca/src/main/wsl.test.ts
T
OrcaWinandOrcaWin 6e8da1df8d fix(wsl): pass guest argv verbatim through --exec (#15039)
* fix(wsl): pass guest argv verbatim through --exec

`wsl.exe <...> -- <argv>` expands `$name` in every argument against the
guest environment before the guest ever runs. It does this even when no
shell is involved, so `-- /usr/bin/printf %s '$HOME'` prints /home/you.

Every WSL invocation went through that preprocessor, so scripts arrived
already rewritten: `awk '{print $2}'` lost its field reference, and a
POSIX script asking for the literal `$HOME` got the expanded path.

`escapeWslShCommandForWindows` tried to compensate by escaping `$`, but
it skipped any `$` preceded by a backslash, so a script containing `\$`
was still corrupted -- and half the call sites never applied it at all.

Route every invocation through `--exec`, which passes argv through
untouched, and delete the escaper. The direct-git path already used
`--exec`, so this is not a new compatibility dependency.

A guard test fails if the `--` form reappears anywhere in the tree.

Net -50 lines of production code.

* test(wsl): drop remaining escaped-dollar assertions

* fix(wsl): cover the --exec migration's blind spots

An audit of every wsl.exe invocation found sites the first pass missed,
including two it actively broke:

- config/scripts/wsl-git-shell-benchmark.mjs imported
  escapeWslShCommandForWindows, which no longer exists, so the script
  threw on startup. Its wslShellArgs helper also still used `--`; the
  file already had an --exec helper, so route both call sites there.
- classifySubprocessCommand unwrapped `wsl.exe <...> -- <binary>` by
  breaking on `--` alone. With every Orca spawn now on --exec it never
  found the guest binary and bucketed all WSL subprocesses as plain
  "wsl", losing the git/gh/glab breakdown. Break on either separator,
  since foreign wsl.exe processes still use `--`.

CliSkillRuntimeSetup builds its setup command as a template literal
rather than an argv array, so no array-shaped search could see it. Its
decoder accepts both separators so commands persisted before this
change still decode.

The guard now scans config/ and tests/ as well as src/, and checks the
command-string spelling alongside the argv one — the two shapes that
have each shipped a regression. It skips comment lines so prose about
the old form stays allowed, and asserts it scanned a plausible file
count so a bad root cannot make it vacuous.

* fix(wsl): restore the guard's multi-line sensitivity

The guard matched line by line, so `'--',\s*'bash'` could not span a
newline -- and every argv array in this repo is formatted one element
per line, which is exactly the shape it exists to catch. Measured
against the pre-migration tree it caught 17 files before and 9 fewer
after. It now strips comment lines and matches the rejoined text, with
a case that pins the multi-line shape so this cannot silently return.

The program list is wider than shells now, which surfaced a false
positive: tmux takes a `--` separator followed by a program too
(`split-window ... -- cat`). Matching is scoped to files that mention
WSL rather than narrowing the list back.

Also:
- Replaced the `sed` regression case, which was vacuous. A backreference
  contains no `$`, so it returned `bac` under both separators and would
  have passed without the fix. The block claimed every case proved the
  bug. Swapped in a positional argument and a shell local, both measured
  to differ -- the positional is the shape `wslUncDirectoryExists` uses,
  where `--` blanked `$1` so every existing directory probed as missing.
- windows-shell-args.test.ts derived its expected argv from
  buildWslExecArgs, the helper under test, so six assertions would still
  pass if it regressed to `--`. Spelled the expectation out.
- Dropped two comments citing the removed `--` behavior as rationale.

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-08-18 02:12:21 -07:00

922 lines
32 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import type * as childProcess from 'node:child_process'
const { execFileMock, execFileSyncMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
execFileSyncMock: vi.fn()
}))
vi.mock('child_process', async (importOriginal) => {
const actual = await importOriginal<typeof childProcess>()
return {
...actual,
execFile: execFileMock,
execFileSync: execFileSyncMock
}
})
import {
_resetWslCachesForTests,
_setWslCachesForTests,
getCachedWslAvailability,
getCachedWslDistros,
hasCachedWslAvailability,
hasCachedWslDistros,
isWslAvailable,
isWslAvailableAsync,
listWslDistros,
listWslDistrosAsync,
parseWslPath,
toLinuxPath,
toWindowsWslPath,
wslUncDirectoryExists,
wslUncDirectoryExistsAsync
} from './wsl'
function withPlatform<T>(value: NodeJS.Platform, fn: () => T): T {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value })
try {
return fn()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: original })
}
}
async function withPlatformAsync<T>(value: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value })
try {
return await fn()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: original })
}
}
describe('WSL distro discovery cache', () => {
afterEach(() => {
execFileMock.mockReset()
execFileSyncMock.mockReset()
_resetWslCachesForTests()
})
it('retries asynchronous discovery after a transient wsl.exe failure', async () => {
vi.useFakeTimers()
execFileMock
.mockImplementationOnce((_command, _args, _options, callback) => {
callback(new Error('transient failure'), '')
})
.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, 'Ubuntu\n')
})
try {
await withPlatformAsync('win32', async () => {
await expect(listWslDistrosAsync()).resolves.toEqual([])
expect(getCachedWslDistros()).toBeNull()
// Brief negative caching bounds the wsl.exe spawn rate between retries.
await expect(listWslDistrosAsync()).resolves.toEqual([])
expect(execFileMock).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(15_000)
await expect(listWslDistrosAsync()).resolves.toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
it('retries synchronous discovery after a transient wsl.exe failure', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw new Error('transient failure')
})
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
expect(getCachedWslDistros()).toBeNull()
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
// Why: `wsl --install` succeeds and reports zero distros while the distro is
// still being provisioned (or until the required reboot). Caching that empty
// success for the process lifetime is why users saw WSL offered during setup
// and then permanently absent from the terminal picker.
it('retries asynchronous discovery after wsl.exe reports no distros yet', async () => {
vi.useFakeTimers()
execFileMock
.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, '')
})
.mockImplementationOnce((_command, _args, _options, callback) => {
callback(null, 'Ubuntu\n')
})
try {
await withPlatformAsync('win32', async () => {
await expect(listWslDistrosAsync()).resolves.toEqual([])
vi.advanceTimersByTime(15_000)
await expect(listWslDistrosAsync()).resolves.toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
it('retries synchronous discovery after wsl.exe reports no distros yet', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValueOnce('')
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
// Why: an empty result must still not let every caller re-spawn a blocking
// wsl.exe; it reuses the same brief negative-cache window as a hard failure.
it('bounds the wsl.exe spawn rate while no distros are installed', () => {
execFileSyncMock.mockReturnValue('')
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
})
it('bounds the asynchronous spawn rate while no distros are installed', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(null, '')
})
await withPlatformAsync('win32', async () => {
await expect(listWslDistrosAsync()).resolves.toEqual([])
await expect(listWslDistrosAsync()).resolves.toEqual([])
expect(execFileMock).toHaveBeenCalledTimes(1)
})
})
it('still caches a non-empty distro list for the process lifetime', () => {
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
withPlatform('win32', () => {
expect(listWslDistros()).toEqual(['Ubuntu'])
expect(listWslDistros()).toEqual(['Ubuntu'])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
})
// Why: docker-desktop entries filter to zero user distros, which is the same
// "nothing installed yet" state as an empty list — a distro installed later
// must still appear. The answer stays readable as [] so a missing distro is
// still visible to `isKnownMissingDistro`.
it('re-probes a docker-desktop-only machine once a user distro can appear', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValueOnce('docker-desktop\ndocker-desktop-data\n')
execFileSyncMock.mockReturnValueOnce('docker-desktop\nUbuntu\n')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
expect(getCachedWslDistros()).toEqual([])
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
// Why: a docker-desktop-only machine filters to empty on every probe, so a flat
// window would re-spawn wsl.exe every 15s for the whole session.
it('backs off while the list keeps coming back empty', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValue('')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
// Second empty result doubles the window, so 15s more is not enough.
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(3)
})
} finally {
vi.useRealTimers()
}
})
it('backs off after repeated distro-list failures', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementation(() => {
throw new Error('transient failure')
})
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
})
} finally {
vi.useRealTimers()
}
})
// Why: listWslDistrosAsync has no in-flight dedupe, so two probes can resolve out of order.
// The late empty answer must not erase the list the newer probe already found.
it('does not let a late empty probe erase a list a newer probe found', async () => {
const callbacks: ((error: Error | null, stdout: string) => void)[] = []
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callbacks.push(callback)
})
await withPlatformAsync('win32', async () => {
const stale = listWslDistrosAsync()
const fresh = listWslDistrosAsync()
expect(callbacks).toHaveLength(2)
callbacks[1](null, 'Ubuntu\n')
callbacks[0](null, '')
await expect(fresh).resolves.toEqual(['Ubuntu'])
await expect(stale).resolves.toEqual(['Ubuntu'])
expect(getCachedWslDistros()).toEqual(['Ubuntu'])
})
})
it('does not let an older non-empty probe overwrite a newer list', async () => {
const callbacks: ((error: Error | null, stdout: string) => void)[] = []
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callbacks.push(callback)
})
await withPlatformAsync('win32', async () => {
const stale = listWslDistrosAsync()
const fresh = listWslDistrosAsync()
callbacks[1](null, 'Ubuntu\n')
callbacks[0](null, 'Debian\n')
await expect(fresh).resolves.toEqual(['Ubuntu'])
await expect(stale).resolves.toEqual(['Ubuntu'])
expect(getCachedWslDistros()).toEqual(['Ubuntu'])
})
})
// Why: N startup callers each land with the same empty answer; counting each one
// would arm the 5min cap on the first round and hide a distro for that whole time.
it('counts one empty result per window when probes overlap', async () => {
vi.useFakeTimers()
const callbacks: ((error: Error | null, stdout: string) => void)[] = []
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callbacks.push(callback)
})
try {
await withPlatformAsync('win32', async () => {
const pending = [listWslDistrosAsync(), listWslDistrosAsync(), listWslDistrosAsync()]
expect(callbacks).toHaveLength(3)
for (const callback of callbacks) {
callback(null, '')
}
await Promise.all(pending)
// One window elapsed, not three doublings, so the next probe runs at 15s.
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
} finally {
vi.useRealTimers()
}
})
// Why: an overlapping failure and empty result must count as one retry window.
it('holds the base window when a failure and an empty result overlap', async () => {
vi.useFakeTimers()
const callbacks: ((error: Error | null, stdout: string) => void)[] = []
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callbacks.push(callback)
})
try {
await withPlatformAsync('win32', async () => {
const failing = listWslDistrosAsync()
const empty = listWslDistrosAsync()
expect(callbacks).toHaveLength(2)
callbacks[0](new Error('transient failure'), '')
callbacks[1](null, '')
await Promise.all([failing, empty])
vi.advanceTimersByTime(7_500)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).not.toHaveBeenCalled()
vi.advanceTimersByTime(7_500)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
} finally {
vi.useRealTimers()
}
})
it('does not shorten an empty-list backoff when an overlapping probe fails', async () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValueOnce('')
const callbacks: ((error: Error | null, stdout: string) => void)[] = []
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callbacks.push(callback)
})
try {
await withPlatformAsync('win32', async () => {
expect(listWslDistros()).toEqual([])
vi.advanceTimersByTime(15_000)
const empty = listWslDistrosAsync()
const failing = listWslDistrosAsync()
callbacks[0](null, '')
callbacks[1](new Error('transient failure'), '')
await Promise.all([empty, failing])
vi.advanceTimersByTime(15_000)
expect(listWslDistros()).toEqual([])
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
} finally {
vi.useRealTimers()
}
})
// Why: the cap is the only bound on how long a distro installed mid-session stays
// invisible, so pin it rather than letting the doubling run away.
it('caps the empty-list backoff at five minutes', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValue('')
try {
withPlatform('win32', () => {
// Windows double to 15/30/60/120/240s, so the 6th would be 480s uncapped.
for (const delayMs of [0, 15_000, 30_000, 60_000, 120_000, 240_000]) {
vi.advanceTimersByTime(delayMs)
listWslDistros()
}
expect(execFileSyncMock).toHaveBeenCalledTimes(6)
vi.advanceTimersByTime(300_000)
listWslDistros()
expect(execFileSyncMock).toHaveBeenCalledTimes(7)
})
} finally {
vi.useRealTimers()
}
})
// Why: an empty list is a real probe result and must keep driving the
// `wsl-distro-missing` repair prompt even once stale. Going null instead fails
// open and silently spawns `wsl.exe -d <distro>` for a distro Orca saw was absent.
it('keeps reporting an empty result after it goes stale', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValue('')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual([])
expect(getCachedWslDistros()).toEqual([])
vi.advanceTimersByTime(15_000)
expect(getCachedWslDistros()).toEqual([])
expect(hasCachedWslDistros()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: a genuinely missing distro yields a non-empty list, which never expires,
// so repair-required still fires for the case that warrants it.
it('keeps reporting a known distro list indefinitely', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
try {
withPlatform('win32', () => {
expect(listWslDistros()).toEqual(['Ubuntu'])
vi.advanceTimersByTime(600_000)
expect(getCachedWslDistros()).toEqual(['Ubuntu'])
})
} finally {
vi.useRealTimers()
}
})
})
describe('WSL availability cache', () => {
afterEach(() => {
execFileMock.mockReset()
execFileSyncMock.mockReset()
_resetWslCachesForTests()
})
// Why: a cold WSL2 utility-VM boot on a just-installed or just-rebooted
// machine routinely exceeds the 5s probe timeout. Latching false for the
// process lifetime is what makes WSL vanish from the picker after setup.
it('retries availability after a probe timeout instead of latching false', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
// Real execFileSync timeout shape on Windows: status null, signal SIGTERM.
throw Object.assign(new Error('spawnSync ETIMEDOUT'), {
code: 'ETIMEDOUT',
status: null,
signal: 'SIGTERM'
})
})
execFileSyncMock.mockReturnValueOnce('')
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: the probe blocks the main process for up to 5s, so a timeout must not
// let every caller re-spawn it immediately.
it('bounds the probe rate while the failure window is open', () => {
execFileSyncMock.mockImplementation(() => {
// Real execFileSync timeout shape on Windows: status null, signal SIGTERM.
throw Object.assign(new Error('spawnSync ETIMEDOUT'), {
code: 'ETIMEDOUT',
status: null,
signal: 'SIGTERM'
})
})
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
})
it('caches a successful probe for the process lifetime', () => {
execFileSyncMock.mockReturnValueOnce('')
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(true)
expect(isWslAvailable()).toBe(true)
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
})
})
// Why: the renderer's capability read reaches this over IPC; a blocking spawn there
// stalls every PTY message and window IPC for as long as wsl.exe takes to answer.
it('probes availability for IPC callers without blocking the main thread', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(null, '', '')
})
await withPlatformAsync('win32', async () => {
await expect(isWslAvailableAsync()).resolves.toBe(true)
expect(execFileMock).toHaveBeenCalledWith(
'wsl.exe',
['--status'],
expect.objectContaining({ timeout: 5000, windowsHide: true }),
expect.any(Function)
)
expect(execFileSyncMock).not.toHaveBeenCalled()
})
})
it('shares one wsl.exe spawn between concurrent async probes', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
setTimeout(() => callback(null, '', ''), 0)
})
await withPlatformAsync('win32', async () => {
const results = await Promise.all([isWslAvailableAsync(), isWslAvailableAsync()])
expect(results).toEqual([true, true])
expect(execFileMock).toHaveBeenCalledTimes(1)
})
})
it('does not let an older async failure overwrite a newer sync success', async () => {
let finishAsyncProbe: ((error: Error | null) => void) | null = null
execFileMock.mockImplementation((_command, _args, _options, callback) => {
finishAsyncProbe = (error) => callback(error, '', '')
})
execFileSyncMock.mockReturnValue('')
await withPlatformAsync('win32', async () => {
const staleProbe = isWslAvailableAsync()
expect(isWslAvailable()).toBe(true)
finishAsyncProbe?.(Object.assign(new Error('older failure'), { code: 1 }))
await expect(staleProbe).resolves.toBe(true)
expect(getCachedWslAvailability()).toBe(true)
})
})
it('does not restore a failure after distro discovery disproves it mid-probe', async () => {
const callbacks = new Map<string, (error: Error | null, stdout: string) => void>()
execFileMock.mockImplementation((_command, args, _options, callback) => {
callbacks.set(args.join(' '), callback)
})
await withPlatformAsync('win32', async () => {
const staleAvailability = isWslAvailableAsync()
const distroProbe = listWslDistrosAsync()
callbacks.get('--list --quiet')?.(null, 'Ubuntu\n')
await expect(distroProbe).resolves.toEqual(['Ubuntu'])
callbacks.get('--status')?.(Object.assign(new Error('older failure'), { code: 1 }), '')
await expect(staleAvailability).resolves.toBe(false)
expect(getCachedWslAvailability()).toBeNull()
const retry = isWslAvailableAsync()
callbacks.get('--status')?.(null, '')
await expect(retry).resolves.toBe(true)
})
})
it('shares the failure backoff between the async and sync probes', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(Object.assign(new Error('not installed'), { code: 'ENOENT' }), '', '')
})
await withPlatformAsync('win32', async () => {
await expect(isWslAvailableAsync()).resolves.toBe(false)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).not.toHaveBeenCalled()
})
})
// Why: wsl.exe ships in System32 on every modern Windows, so a host without WSL answers
// with a non-zero exit, not ENOENT — and execFile reports that as a numeric `code`, not the
// `status` execFileSync uses. Misreading it as retryable would shrink the shared cache window
// to 45s and make the sync callers pay their blocking spawn ~13x more often.
it('treats a non-zero async exit as definitive, so the sync probe keeps the long window', async () => {
vi.useFakeTimers()
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(Object.assign(new Error('wsl.exe exited 1'), { code: 1 }), '', '')
})
execFileSyncMock.mockReturnValue('')
try {
await withPlatformAsync('win32', async () => {
await expect(isWslAvailableAsync()).resolves.toBe(false)
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).not.toHaveBeenCalled()
vi.advanceTimersByTime(10 * 60_000)
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: the resolver reads the cached getter, not the probe. Reporting the last
// observed answer keeps the `wsl-unavailable` repair prompt reachable; going null
// on staleness would let git and PTY silently resolve to a WSL that just failed.
it('keeps reporting the last observed answer after it goes stale', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementation(() => {
// Real execFileSync timeout shape on Windows: status null, signal SIGTERM.
throw Object.assign(new Error('spawnSync ETIMEDOUT'), {
code: 'ETIMEDOUT',
status: null,
signal: 'SIGTERM'
})
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
vi.advanceTimersByTime(45_000)
expect(getCachedWslAvailability()).toBe(false)
expect(hasCachedWslAvailability()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: an answer-shaped failure earns a long window, not a session-long latch —
// wsl.exe also exits non-zero while the WSL package is servicing or LxssManager is
// still starting, which is transient.
it.each([
['wsl.exe reports WSL unusable', { status: 1 }],
['wsl.exe is not installed', { code: 'ENOENT' }]
])('holds a definitive failure far longer than a timeout when %s', (_label, errorShape) => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('definitive failure'), errorShape)
})
execFileSyncMock.mockReturnValueOnce('')
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(false)
expect(getCachedWslAvailability()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(1)
vi.advanceTimersByTime(10 * 60_000)
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: the probe blocks the main process for up to 5s, so a wedged wsl.exe must not
// be re-probed on every window boundary for the rest of the session.
it('backs off after repeated probe failures', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementation(() => {
throw Object.assign(new Error('spawnSync ETIMEDOUT'), {
code: 'ETIMEDOUT',
status: null,
signal: 'SIGTERM'
})
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
// Second failure doubles the window, so the next boundary is not enough.
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
})
} finally {
vi.useRealTimers()
}
})
// Why: a seeded retryable failure must re-probe once its window lapses, the same
// as one observed live — otherwise test setup can hide the latch this fixes.
it('re-probes a seeded retryable failure once its window lapses', () => {
vi.useFakeTimers()
execFileSyncMock.mockReturnValueOnce('')
try {
withPlatform('win32', () => {
_setWslCachesForTests({ available: false, availabilityRetryable: true })
expect(isWslAvailable()).toBe(false)
vi.advanceTimersByTime(45_000)
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: the caches expire independently and `getWslRepairReason` checks availability
// first, so a definitive failure held for 10min would report `wsl-unavailable` over a
// WSL that just listed a distro. Finding a distro must drop the stale failure.
it.each([
['a definitive failure', { status: 1 }],
['a timeout', { code: 'ETIMEDOUT', status: null, signal: 'SIGTERM' }]
])('re-probes availability once a distro list succeeds after %s', (_label, errorShape) => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('probe failed'), errorShape)
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
// A distro turns up (WSL finished provisioning / was repaired mid-session).
execFileSyncMock.mockReturnValueOnce('Ubuntu\n')
expect(listWslDistros()).toEqual(['Ubuntu'])
// Without dropping the stale failure this would stay false for 10min.
execFileSyncMock.mockReturnValueOnce('')
expect(isWslAvailable()).toBe(true)
})
} finally {
vi.useRealTimers()
}
})
// Why: an empty list re-probes on a 15s-to-5min schedule, so clearing the availability
// failure on every empty probe would re-spawn the blocking 5s probe far too often.
it('does not drop an availability failure for an empty distro list', () => {
vi.useFakeTimers()
execFileSyncMock.mockImplementationOnce(() => {
throw Object.assign(new Error('probe failed'), { status: 1 })
})
try {
withPlatform('win32', () => {
expect(isWslAvailable()).toBe(false)
execFileSyncMock.mockReturnValueOnce('')
expect(listWslDistros()).toEqual([])
expect(getCachedWslAvailability()).toBe(false)
// Still inside the definitive window, so no re-probe was paid.
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
expect(isWslAvailable()).toBe(false)
expect(execFileSyncMock).toHaveBeenCalledTimes(2)
})
} finally {
vi.useRealTimers()
}
})
it('keeps reporting unavailable off Windows without probing', () => {
withPlatform('darwin', () => {
expect(isWslAvailable()).toBe(false)
expect(getCachedWslAvailability()).toBe(false)
expect(execFileSyncMock).not.toHaveBeenCalled()
})
})
})
describe('wsl path helpers', () => {
it('parses WSL UNC paths on Windows', () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', {
configurable: true,
value: 'win32'
})
try {
expect(parseWslPath('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')).toEqual({
distro: 'Ubuntu',
linuxPath: '/home/jin/repo'
})
} finally {
Object.defineProperty(process, 'platform', {
configurable: true,
value: originalPlatform
})
}
})
it('converts Windows drive paths to /mnt paths for WSL commands', () => {
expect(toLinuxPath('C:\\Users\\jinwo\\git\\orca')).toBe('/mnt/c/Users/jinwo/git/orca')
})
it('converts /mnt drive paths back to native Windows form', () => {
expect(toWindowsWslPath('/mnt/c/Users/jinwo/git/orca', 'Ubuntu')).toBe(
'C:\\Users\\jinwo\\git\\orca'
)
})
})
describe('wslUncDirectoryExists', () => {
afterEach(() => {
execFileSyncMock.mockReset()
})
it('returns true when the distro reports the directory exists', () => {
execFileSyncMock.mockReturnValue('__ORCA_DIRECTORY_EXISTS__')
const result = withPlatform('win32', () =>
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
)
expect(result).toBe(true)
expect(execFileSyncMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--exec',
'sh',
'-c',
expect.stringContaining('__ORCA_DIRECTORY_EXISTS__'),
'sh',
'/home/jin/repo'
],
expect.objectContaining({ timeout: 5000 })
)
})
it('returns false when the guest reports the directory missing', () => {
execFileSyncMock.mockReturnValue('__ORCA_DIRECTORY_MISSING__')
const result = withPlatform('win32', () =>
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing')
)
expect(result).toBe(false)
})
it('returns null when wsl.exe or the distro is unavailable', () => {
execFileSyncMock.mockImplementation(() => {
const error = new Error('distro unavailable') as Error & { status: number }
error.status = 4294967295
throw error
})
const result = withPlatform('win32', () =>
wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
)
expect(result).toBeNull()
})
it('returns null for non-WSL paths and off Windows', () => {
expect(withPlatform('win32', () => wslUncDirectoryExists('C:\\Users\\jin\\repo'))).toBeNull()
expect(
withPlatform('linux', () => wslUncDirectoryExists('\\\\wsl.localhost\\Ubuntu\\home\\jin'))
).toBeNull()
expect(execFileSyncMock).not.toHaveBeenCalled()
})
})
describe('wslUncDirectoryExistsAsync', () => {
afterEach(() => {
execFileMock.mockReset()
})
it('returns true when the distro reports the directory exists', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) =>
callback(null, '__ORCA_DIRECTORY_EXISTS__')
)
await expect(
withPlatformAsync('win32', () =>
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
)
).resolves.toBe(true)
expect(execFileMock).toHaveBeenCalledWith(
'wsl.exe',
[
'-d',
'Ubuntu',
'--exec',
'sh',
'-c',
expect.stringContaining('__ORCA_DIRECTORY_EXISTS__'),
'sh',
'/home/jin/repo'
],
expect.objectContaining({ timeout: 5000 }),
expect.any(Function)
)
})
it('distinguishes a missing directory from an inconclusive probe', async () => {
execFileMock
.mockImplementationOnce((_command, _args, _options, callback) =>
callback(null, '__ORCA_DIRECTORY_MISSING__')
)
.mockImplementationOnce((_command, _args, _options, callback) =>
callback(Object.assign(new Error('distro unavailable'), { code: 4294967295 }), '')
)
await withPlatformAsync('win32', async () => {
await expect(
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\missing')
).resolves.toBe(false)
await expect(
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo')
).resolves.toBeNull()
})
})
it('returns null without spawning for paths outside WSL or off Windows', async () => {
await expect(
withPlatformAsync('win32', () => wslUncDirectoryExistsAsync('C:\\Users\\jin\\repo'))
).resolves.toBeNull()
await expect(
withPlatformAsync('linux', () =>
wslUncDirectoryExistsAsync('\\\\wsl.localhost\\Ubuntu\\home\\jin')
)
).resolves.toBeNull()
expect(execFileMock).not.toHaveBeenCalled()
})
})