fix(wsl): run wsl.exe probes from the system folder, never the profile (#29853)

* fix(wsl): run wsl.exe probes from the system root, never the profile

WSL holds wsl.exe's working directory busy for about its idle timeout
after the probe exits. Since #29209 the orcad startup probe ran in the
profile, so a temporary orca serve profile could not be deleted for about
a minute (diagnosed in #29845).

* test(orcad): drop the WSL profile-hold wait from serve parity

Reverts #29845's extended EBUSY retry: wsl.exe no longer runs in the
profile, so nothing outside the test's process tree holds it.

* build(cli): list the WSL spawn-directory module in the CLI typecheck project
This commit is contained in:
Neil
2026-10-16 17:12:44 -07:00
committed by GitHub
parent 74863c2a88
commit 0ed0caa663
18 changed files with 124 additions and 138 deletions
+1
View File
@@ -199,6 +199,7 @@
"../src/main/codex/codex-trust-grant-ledger.ts",
"../src/main/codex/codex-user-hook-trust-moves.ts",
"../src/main/codex/codex-wsl-hook-install-plan.ts",
"../src/main/wsl-interop-spawn-directory.ts",
"../src/main/codex/config-settings-baseline.ts",
"../src/main/codex/config-settings-conflict-resolution.ts",
"../src/main/codex/config-plugin-registration-promotion.ts",
+2 -1
View File
@@ -12,6 +12,7 @@ import type { CodexRuntimeHomeService } from './runtime-home-service'
import type { Store } from '../persistence'
import type { RateLimitService } from '../rate-limits/service'
import { buildEncodedWslBashCommand } from '../wsl-bash-command'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
import { admitSelfInitiatedTreeKill } from '../own-chromium-tree-kill-guard'
import type { CodexAccountSelectionTarget } from './runtime-selection'
import { CodexAccountIdentity, type ResolvedCodexIdentity } from './codex-account-identity'
@@ -102,7 +103,7 @@ export class CodexAccountService {
execFileSync(
'wsl.exe',
['-d', distro, '--exec', 'bash', '-lc', buildEncodedWslBashCommand(script)],
{ windowsHide: true, encoding: 'utf-8', timeout: 5000 }
{ windowsHide: true, encoding: 'utf-8', timeout: 5000, cwd: resolveWslInteropSpawnCwd() }
)
)
this.managedHomes = new CodexManagedHomeLifecycle(this.managedHomePaths)
@@ -10,6 +10,7 @@ import {
} from '../provider-process/provider-process-supervisor'
import type { CodexAppServerSpawn } from './codex-app-server-process-tree-kill'
import { stopSupervisedChildProcess } from '../provider-process/supervised-child-process-stop'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
const BACKFILL_RECOVERY_KILL_SITE = 'codex-state-db-backfill-recovery'
// Codex drains on its stdin end, as the Codex connection's root-only close asks; one value
@@ -36,7 +37,8 @@ export function spawnCodexBackfillRecoveryProcess(
{
stdio: ['pipe', 'ignore', 'ignore'],
windowsHide: true,
env: process.env
env: process.env,
cwd: resolveWslInteropSpawnCwd()
}
)
return { child, supervised: false }
+2
View File
@@ -11,6 +11,7 @@ import {
type CodexTrustGrantBinaryStamp,
type CodexTrustGrantLedgerHome
} from './codex-trust-grant-ledger'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
// Why: WSL pays cold-distro and login-shell startup, but stays hard-bounded on launch prep.
const WSL_GRANT_TIMEOUT_MS = 30_000
@@ -65,6 +66,7 @@ async function buildWslCodexBinaryStamp(
const result = await runProcess({
program: 'wsl.exe',
args: probe.args,
cwd: resolveWslInteropSpawnCwd(),
timeoutMs: WSL_CODEX_AVAILABILITY_TIMEOUT_MS
})
if (result.code !== 0 || result.timedOut) {
@@ -1,6 +1,7 @@
import { execFile } from 'node:child_process'
import { posix as pathPosix, win32 as pathWin32 } from 'node:path'
import { parseWslUncPath } from '../../shared/wsl-paths'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
export type CodexWslRuntimeHookTarget = {
runtime?: 'host' | 'wsl'
@@ -118,7 +119,12 @@ function scheduleWslLinuxPathCanonicalization(
execFile(
'wsl.exe',
args,
{ encoding: 'utf-8', timeout: WSL_CANONICALIZE_TIMEOUT_MS, windowsHide: true },
{
encoding: 'utf-8',
timeout: WSL_CANONICALIZE_TIMEOUT_MS,
windowsHide: true,
cwd: resolveWslInteropSpawnCwd()
},
(error, stdout) => {
const canonicalPath = stdout.trim()
const resolvedPath = !error && canonicalPath.startsWith('/') ? canonicalPath : null
+3 -1
View File
@@ -3,6 +3,7 @@ import {
buildWslCapturedLoginShellCommand,
buildWslExecArgs
} from '../../shared/wsl-login-shell-command'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
export type WslGitReadEnvironment = { gitPath: string; home: string; path: string }
@@ -92,7 +93,8 @@ function probeWslGitReadEnvironment(distro: string): Promise<ProbeOutcome> {
encoding: 'utf8',
maxBuffer: PROBE_MAX_BUFFER,
timeout: PROBE_TIMEOUT_MS,
windowsHide: true
windowsHide: true,
cwd: resolveWslInteropSpawnCwd()
},
(error, stdout) => {
if (error) {
@@ -249,28 +249,16 @@ export async function killProfileDaemons(userData: string): Promise<void> {
}
}
/**
* A full orcad on Windows probes WSL with `wsl.exe` run in its profile, and the WSL
* service, outside the test's process tree, holds that directory busy for ~60s after.
*/
export const WINDOWS_WSL_PROFILE_HOLD_MS = 75_000
/**
* Removes a test root, retrying a late writer's ENOTEMPTY/EBUSY on every platform.
* `windowsBusyHoldMs` keeps retrying a Windows EBUSY that long once attempts run out.
*/
export async function removeTestRoot(root: string, windowsBusyHoldMs = 0): Promise<void> {
const holdDeadline = Date.now() + (process.platform === 'win32' ? windowsBusyHoldMs : 0)
/** Removes a test root, retrying a late writer's ENOTEMPTY/EBUSY on every platform. */
export async function removeTestRoot(root: string): Promise<void> {
for (let attempt = 1; ; attempt += 1) {
try {
await removeTree(root)
return
} catch (error) {
const code = errorCode(error) ?? ''
const held = code === 'EBUSY' && Date.now() < holdDeadline
if (
(attempt >= REMOVE_ATTEMPTS && !held) ||
!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(code)
attempt >= REMOVE_ATTEMPTS ||
!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(errorCode(error) ?? '')
) {
throw error
}
@@ -1,69 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { removeTree } from '../../shared/windows-transient-lock-removal'
import { removeTestRoot } from './orcad-daemon-teardown-fixture'
vi.mock('../../shared/windows-transient-lock-removal', () => ({ removeTree: vi.fn() }))
const originalPlatform = process.platform
function setPlatform(platform: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
}
function failTimes(times: number, code: string): void {
let calls = 0
vi.mocked(removeTree).mockImplementation(async () => {
calls += 1
if (calls <= times) {
throw Object.assign(new Error(code), { code })
}
})
}
async function settle(removal: Promise<void>): Promise<unknown> {
const outcome = removal.then(
() => 'removed',
(error: unknown) => error
)
await vi.runAllTimersAsync()
return outcome
}
describe('removeTestRoot Windows busy hold', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.mocked(removeTree).mockReset()
})
afterEach(() => {
vi.useRealTimers()
setPlatform(originalPlatform)
})
it('outlasts an EBUSY hold longer than its attempts within the hold window', async () => {
setPlatform('win32')
failTimes(40, 'EBUSY')
expect(await settle(removeTestRoot('root', 10_000))).toBe('removed')
expect(removeTree).toHaveBeenCalledTimes(41)
})
it('still fails an EBUSY that outlives the hold window', async () => {
setPlatform('win32')
failTimes(Number.POSITIVE_INFINITY, 'EBUSY')
expect(await settle(removeTestRoot('root', 1_000))).toMatchObject({ code: 'EBUSY' })
})
it('keeps the attempt bound for ENOTEMPTY, a late writer rather than a held directory', async () => {
setPlatform('win32')
failTimes(Number.POSITIVE_INFINITY, 'ENOTEMPTY')
expect(await settle(removeTestRoot('root', 10_000))).toMatchObject({ code: 'ENOTEMPTY' })
expect(removeTree).toHaveBeenCalledTimes(8)
})
it('ignores the hold off Windows', async () => {
setPlatform('linux')
failTimes(Number.POSITIVE_INFINITY, 'EBUSY')
expect(await settle(removeTestRoot('root', 10_000))).toMatchObject({ code: 'EBUSY' })
expect(removeTree).toHaveBeenCalledTimes(8)
})
})
@@ -24,8 +24,7 @@ import {
killProfileDaemons,
recordProfileDaemons,
removeTestRoot,
waitForProcessExit,
WINDOWS_WSL_PROFILE_HOLD_MS
waitForProcessExit
} from './orcad-daemon-teardown-fixture'
const slotDir = resolve('out/orcad')
@@ -36,7 +35,6 @@ const skip = skipForMissingInputs('artifact', runtime ? [] : ['a Node orcad slot
const roots: string[] = []
const cleanupDiagnostics = new Set<ServeCleanupDiagnostics>()
// Past the 60s hook default: daemon cleanup plus the Windows WSL profile hold.
afterEach(async () => {
const failures: unknown[] = []
// Stop every profile before removing any project root it may still use.
@@ -61,7 +59,7 @@ afterEach(async () => {
const diagnostics = [...cleanupDiagnostics].find((entry) => entry.owns(root))
const startedAt = Date.now()
try {
await removeTestRoot(root, WINDOWS_WSL_PROFILE_HOLD_MS)
await removeTestRoot(root)
roots.splice(roots.indexOf(root), 1)
if (diagnostics?.rootRemoved(root)) {
cleanupDiagnostics.delete(diagnostics)
@@ -76,7 +74,7 @@ afterEach(async () => {
if (failures.length > 0) {
throw new AggregateError(failures, 'Test root removal incomplete')
}
}, 150_000)
})
/** Without Vitest's markers: daemon-entry.js does not start its server under VITEST. */
function serveEnv(userData: string): NodeJS.ProcessEnv {
+4 -1
View File
@@ -44,6 +44,9 @@ vi.mock('../codex-cli/command', () => ({
resolveCodexCommand: resolveCodexCommandMock
}))
vi.mock('../wsl-interop-spawn-directory', () => ({
resolveWslInteropSpawnCwd: () => 'C:\\Windows'
}))
vi.mock('node-pty', () => ({
spawn: ptySpawnMock
}))
@@ -678,7 +681,7 @@ describe('fetchCodexRateLimits', () => {
expect(shellCommand).not.toContain('wsl-codex-path')
expect(spawnOptions).toEqual(
expect.objectContaining({
cwd: expect.stringContaining('rate-limit-pty-cwd'),
cwd: 'C:\\Windows',
env: expect.not.objectContaining({ CODEX_HOME: expect.anything() })
})
)
+3 -1
View File
@@ -34,6 +34,7 @@ import {
resolveHiddenRateLimitPtyCwd
} from './hidden-rate-limit-pty-cwd'
import { quoteHiddenRateLimitShellValue } from './hidden-rate-limit-shell'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
const RPC_TIMEOUT_MS = 10_000
const WSL_RPC_TIMEOUT_MS = 25_000
@@ -96,7 +97,8 @@ async function fetchViaRpc(options?: CodexRateLimitFetchOptions): Promise<Provid
const child = spawnProcessOnWorkerThread({
program: wslCodex ? wslCodex.command : codexCommand,
args: wslCodex ? wslCodex.args : codexArgs,
cwd: resolveHiddenRateLimitPtyCwd(),
// Why: WSL holds wsl.exe's cwd busy ~60s, so never hand it a folder inside the profile (#29845).
cwd: wslCodex ? resolveWslInteropSpawnCwd() : resolveHiddenRateLimitPtyCwd(),
env: withCliRuntimeOnPath(codexCommand, {
...(wslCodex ? processEnvWithoutCodexHome() : process.env),
// WSL probes keep the guest's own proxy env; a Windows proxy address may not resolve there.
+32 -27
View File
@@ -8,12 +8,8 @@ import {
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.
// #16463: an inherited cwd Linux had deleted broke every later `wsl.exe` spawn.
// #29845: naming the profile instead let WSL hold it busy ~60s after Orca exited.
const createdRoots: string[] = []
@@ -23,7 +19,15 @@ function makeExistingDirectory(): string {
return dir
}
const ENV_KEYS = ['ORCA_USER_DATA_PATH', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH'] as const
const ENV_KEYS = [
'ORCA_USER_DATA_PATH',
'USERPROFILE',
'HOMEDRIVE',
'HOMEPATH',
'SystemRoot',
'windir',
'SystemDrive'
] as const
const savedEnv = new Map<string, string | undefined>()
beforeEach(() => {
@@ -50,40 +54,41 @@ afterEach(() => {
})
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
it('names the system root, never the profile or the user folder', () => {
const systemRoot = makeExistingDirectory()
process.env.SystemRoot = systemRoot
process.env.ORCA_USER_DATA_PATH = makeExistingDirectory()
process.env.USERPROFILE = makeExistingDirectory()
expect(resolveWslInteropSpawnCwd()).toBe(userData)
expect(resolveWslInteropSpawnCwd()).toBe(systemRoot)
})
it('never falls back to the profile or user folder when no system directory resolves', () => {
const profile = makeExistingDirectory()
const userFolder = makeExistingDirectory()
process.env.ORCA_USER_DATA_PATH = profile
process.env.USERPROFILE = userFolder
expect([profile, userFolder]).not.toContain(resolveWslInteropSpawnCwd())
})
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
process.env.SystemRoot = join(tmpdir(), 'orca-wsl-spawn-cwd-never-created')
const windir = makeExistingDirectory()
process.env.windir = windir
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))
expect(resolveWslInteropSpawnCwd()).toBe(windir)
})
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.
// A memo that is never re-validated reproduces #16463 one layer up.
const doomed = makeExistingDirectory()
process.env.ORCA_USER_DATA_PATH = doomed
process.env.SystemRoot = doomed
const survivor = makeExistingDirectory()
expect(resolveWslInteropSpawnCwd()).toBe(doomed)
rmSync(doomed, { recursive: true, force: true })
process.env.ORCA_USER_DATA_PATH = survivor
process.env.SystemRoot = survivor
expect(resolveWslInteropSpawnCwd()).toBe(survivor)
})
+12 -12
View File
@@ -1,5 +1,5 @@
import { statSync } from 'node:fs'
import { homedir } from 'node:os'
import { tmpdir } from 'node:os'
/**
* A Windows directory that is safe to hand `wsl.exe` as its working directory.
@@ -19,6 +19,10 @@ import { homedir } from 'node:os'
* git works. It is never the cwd the command runs in: WSL invocations carry
* their Linux directory in `git -C`, a `cd` inside `bash -c`, or the `sh -c`
* wrapper `withGuestCwd` builds.
*
* Why a system directory and never the profile or user folder (#29845): WSL
* holds this directory busy for about its ~60s idle timeout after `wsl.exe`
* exits, so a profile named here cannot be deleted for a minute after Orca exits.
*/
let cachedSpawnCwd: string | null = null
@@ -40,22 +44,18 @@ export function resetWslInteropSpawnDirectoryCache(): void {
}
export function resolveWslInteropSpawnCwd(): string | undefined {
// Why re-validate: the answer is only useful while it still resolves, and the
// user's profile directory can go away on a roaming/mapped-drive host.
// Why re-validate: the answer is only useful while it still resolves.
if (isExistingDirectory(cachedSpawnCwd)) {
return cachedSpawnCwd
}
const env = process.env
// Why this order: an app-owned directory first (it outlives every worktree),
// then the user's profile, then the system root as a floor that always exists.
// A root is fine here — nothing scans this directory, it is only the value
// `CreateProcessW` receives for `lpCurrentDirectory`.
const candidates: (string | undefined)[] = [
env.ORCA_USER_DATA_PATH,
env.USERPROFILE,
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : undefined,
homedir(),
env.SystemDrive ? `${env.SystemDrive}\\` : 'C:\\'
env.SystemRoot,
env.windir,
env.SystemDrive ? `${env.SystemDrive}\\` : undefined,
'C:\\',
// Why last: unreachable on a real Windows host; it keeps POSIX test hosts naming a directory.
tmpdir()
]
for (const candidate of candidates) {
if (isExistingDirectory(candidate)) {
+34
View File
@@ -1,3 +1,6 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ProcessResult } from '@orca/process-host/process-spec'
@@ -17,11 +20,20 @@ import {
listRunningWslHomeDirsAsync
} from './wsl'
import { filterPathsToRunningWslDistrosAsync } from './wsl-running-path-filter'
import { resetWslInteropSpawnDirectoryCache } from './wsl-interop-spawn-directory'
import {
observeWslTranscriptRunningState,
resetWslTranscriptRunningObserverForTests
} from './native-chat/wsl-transcript-running-observer'
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
async function withPlatform<T>(value: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value })
@@ -56,6 +68,28 @@ describe('running WSL distro discovery', () => {
})
})
it('runs the startup probe from the system root, never the profile (#29845)', async () => {
runProcessMock.mockResolvedValue(exited('Ubuntu\n'))
const saved = { SystemRoot: process.env.SystemRoot, profile: process.env.ORCA_USER_DATA_PATH }
const systemRoot = mkdtempSync(join(tmpdir(), 'orca-wsl-probe-system-root-'))
const profile = mkdtempSync(join(tmpdir(), 'orca-wsl-probe-profile-'))
process.env.SystemRoot = systemRoot
process.env.ORCA_USER_DATA_PATH = profile
resetWslInteropSpawnDirectoryCache()
try {
await withPlatform('win32', () => listRunningWslDistrosAsync({ requireConfirmed: true }))
expect(runProcessMock).toHaveBeenCalledWith(
expect.objectContaining({ args: ['--list', '--running', '--quiet'], cwd: systemRoot })
)
} finally {
restoreEnv('SystemRoot', saved.SystemRoot)
restoreEnv('ORCA_USER_DATA_PATH', saved.profile)
resetWslInteropSpawnDirectoryCache()
rmSync(systemRoot, { recursive: true, force: true })
rmSync(profile, { recursive: true, force: true })
}
})
it('confirms an empty list when no distro runs', async () => {
// Captured on WSL 2.7: with nothing running, `--quiet` exits 0 with empty stdout in both
// encodings; only the non-quiet form prints the localized message and exits -1.
+4 -1
View File
@@ -4,6 +4,9 @@ import type { ProcessResult } from '@orca/process-host/process-spec'
const { runProcessMock } = vi.hoisted(() => ({ runProcessMock: vi.fn() }))
vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock }))
vi.mock('./wsl-interop-spawn-directory', () => ({
resolveWslInteropSpawnCwd: () => 'C:\\Windows'
}))
function exited(code: number, stderr = ''): ProcessResult {
return { code, signal: null, stdout: '', stderr, timedOut: false }
@@ -58,7 +61,7 @@ describe('tryDeleteWslUncPath', () => {
expect(timeoutMs).toBe(30000)
// Why a concrete directory (#16463): this deletes worktrees, so the cwd it
// would otherwise inherit is the very directory about to disappear.
expect(cwd).toEqual(expect.any(String))
expect(cwd).toEqual('C:\\Windows')
expect(spawnArgs).toEqual([
'-d',
'Ubuntu',
+4 -1
View File
@@ -10,6 +10,9 @@ vi.mock('@orca/process-host', () => ({
runProcess: runProcessMock,
runProcessSync: runProcessSyncMock
}))
vi.mock('./wsl-interop-spawn-directory', () => ({
resolveWslInteropSpawnCwd: () => 'C:\\Windows'
}))
function exited(stdout: string, code: number | null = 0): ProcessResult {
return { code, signal: null, stdout, stderr: '', timedOut: false }
@@ -391,7 +394,7 @@ describe('WSL availability cache', () => {
program: 'wsl.exe',
args: ['--status'],
timeoutMs: 5000,
cwd: expect.any(String)
cwd: 'C:\\Windows'
})
)
})
+2
View File
@@ -4,6 +4,7 @@ import {
buildWslExecArgs
} from '../../shared/wsl-login-shell-command'
import { resolveWslExecutablePath } from './wsl-executable-path'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
/**
* A distro's login-shell environment, probed once and cached.
@@ -91,6 +92,7 @@ async function probeGuestEnvironment(
// spawns wsl.exe itself, so without it wsl.exe's own errors arrive UTF-16LE
// and the NUL-separated payload below is read through NUL-riddled text.
env: { ...process.env, WSL_UTF8: '1' },
cwd: resolveWslInteropSpawnCwd(),
timeoutMs: Math.min(PROBE_TIMEOUT_MS, budgetMs),
maxOutputBytes: PROBE_MAX_OUTPUT_BYTES
})
+4 -1
View File
@@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const runProcessMock = vi.hoisted(() => vi.fn())
vi.mock('@orca/process-host', () => ({ runProcess: runProcessMock }))
vi.mock('../wsl-interop-spawn-directory', () => ({
resolveWslInteropSpawnCwd: () => 'C:\\Windows'
}))
vi.mock('./wsl-executable-path', () => ({
resolveWslExecutablePath: () => 'C:\\Windows\\System32\\wsl.exe'
}))
@@ -244,7 +247,7 @@ describe('guest cwd', () => {
// The contract is that the GUEST path never becomes wsl.exe's Windows cwd,
// not that there is no cwd: inheriting one is its own bug (#16463).
expect(runProcessMock.mock.calls.at(-1)?.[0].cwd).not.toBe('/home/u/repo')
expect(runProcessMock.mock.calls.at(-1)?.[0].cwd).toEqual(expect.any(String))
expect(runProcessMock.mock.calls.at(-1)?.[0].cwd).toEqual('C:\\Windows')
expect(lastArgv()).toContain('/home/u/repo')
expect(lastArgv()).toContain('sh')
})