Files
orca/src/main/wsl-interop-spawn-directory.test.ts
Neil 4bc20cb842 fix(wsl): name an explicit Windows cwd for wsl.exe spawns (#17834)
* fix(wsl): name an explicit Windows cwd for wsl.exe spawns

Removing the worktree Orca was launched from broke every wsl.exe spawn for
the rest of the session. The WSL command builders passed `cwd: undefined`
meaning "the directory is inside the command" -- but CreateProcessW reads
NULL as "inherit the parent's", and the parent's was a \\wsl.localhost path
Linux had just deleted.

Fixes #16463

* fix(wsl): name the spawn directory at the six remaining wsl.exe sites

The first commit fixed the WSL command builders. Six spawn sites were left
inheriting the process cwd, which is the same deletable `\\wsl.localhost`
worktree: `wsl-availability` (both probes), the WSL filesystem watcher, the
agent-hook relay launch, the UNC delete, and the local worktree filesystem.

`wsl-availability` is the one that matters most, and it turns the bug into a
latching false negative. `isRetryableWslProbeFailure` returns false for ENOENT,
so a spawn that failed only because the inherited cwd was gone is cached as
"WSL is not installed" on the 10-minute definitive TTL with exponential
backoff up to 30 minutes. Git keeps working and Orca reports WSL unavailable --
worse than the bug being fixed.

ENOENT stays non-retryable. It is answer-shaped for the reason it is meant to
be -- wsl.exe is not on PATH -- and naming the directory is what removes the
one cause that was not. Making it retryable would instead re-probe every
non-WSL Windows machine on the short window, and would leave the false ENOENT
in place for the other five sites, which have no cache to correct.

Three of these are also on the `runWslProcess` W3 migration allowlist; this is
the interim until they move, and matches what #17837 does inside the runner.
2026-09-02 01:39:48 -07:00

91 lines
3.2 KiB
TypeScript

import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
resetWslInteropSpawnDirectoryCache,
resolveWslInteropSpawnCwd
} from './wsl-interop-spawn-directory'
// Regression coverage for #16463 ("Removing the worktree Orca was launched from
// breaks every wsl.exe spawn for the rest of the session"). The WSL command
// builders passed `cwd: undefined` meaning "the directory is inside the
// command", but CreateProcessW reads NULL as "inherit the parent's" — and the
// parent's was a `\\wsl.localhost\...` worktree Linux had just deleted. 1805 of
// 1806 git calls then failed `spawn wsl.exe ENOENT` until the app restarted.
const createdRoots: string[] = []
function makeExistingDirectory(): string {
const dir = mkdtempSync(join(tmpdir(), 'orca-wsl-spawn-cwd-'))
createdRoots.push(dir)
return dir
}
const ENV_KEYS = ['ORCA_USER_DATA_PATH', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH'] as const
const savedEnv = new Map<string, string | undefined>()
beforeEach(() => {
for (const key of ENV_KEYS) {
savedEnv.set(key, process.env[key])
delete process.env[key]
}
resetWslInteropSpawnDirectoryCache()
})
afterEach(() => {
for (const key of ENV_KEYS) {
const saved = savedEnv.get(key)
if (saved === undefined) {
delete process.env[key]
} else {
process.env[key] = saved
}
}
resetWslInteropSpawnDirectoryCache()
while (createdRoots.length > 0) {
rmSync(createdRoots.pop()!, { recursive: true, force: true })
}
})
describe('resolveWslInteropSpawnCwd', () => {
it('names the app-owned directory first, so no worktree can be the answer', () => {
const userData = makeExistingDirectory()
process.env.ORCA_USER_DATA_PATH = userData
process.env.USERPROFILE = makeExistingDirectory()
expect(resolveWslInteropSpawnCwd()).toBe(userData)
})
it('skips a candidate that does not resolve instead of naming it', () => {
process.env.ORCA_USER_DATA_PATH = join(tmpdir(), 'orca-wsl-spawn-cwd-never-created')
const profile = makeExistingDirectory()
process.env.USERPROFILE = profile
expect(resolveWslInteropSpawnCwd()).toBe(profile)
})
it('always names some directory rather than letting the spawn inherit one', () => {
// Why: inheriting is the failure mode. With no configured candidate at all
// the home directory and system root still stand between a spawn and the
// parent's cwd.
expect(resolveWslInteropSpawnCwd()).toEqual(expect.any(String))
})
it('re-answers after the directory it memoized goes away mid-session', () => {
// This is the incident: the chosen directory was valid when the process
// started and was deleted underneath it hours later. A memo that is never
// re-validated reproduces the original bug one layer up.
const doomed = makeExistingDirectory()
process.env.ORCA_USER_DATA_PATH = doomed
const survivor = makeExistingDirectory()
expect(resolveWslInteropSpawnCwd()).toBe(doomed)
rmSync(doomed, { recursive: true, force: true })
process.env.ORCA_USER_DATA_PATH = survivor
expect(resolveWslInteropSpawnCwd()).toBe(survivor)
})
})