From b1570ad44ce5ead89edbb698fa5d954b532bf798 Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 10 Sep 2026 21:26:21 -0700 Subject: [PATCH] test(wsl): give the login-shell contrast its own HOME so a sibling suite cannot stall it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `local-worktree-filesystem-wsl-banner.wsl.test.ts` read its contrast through the distro user's real `~/.profile`. `wsl-runner.wsl.test.ts` appends `sleep 60` to that same file for the length of its describe — it has to, because the stall it reproduces (#14288) happens inside `getWslGuestEnvironment`'s probe, which takes no HOME from its caller. Run in one Vitest invocation the two overlap, and the contrast read inherits the 60s stall and dies on its own 30s timeout. Measured on a Windows 11 host with real WSL2 Ubuntu-24.04, at pinned origin/main cdf41df37c so it is not this stack: all four suites together 1 failed / 15 passed; the banner suite alone 5 passed / 5; the interfering pair failed 2/2 at 30,013ms and 30,017ms. Each suite is correct; only the pair is broken, which is why nobody had seen it. Serialising the files would have hidden the coupling. The contrast now runs under `env HOME=/home` with an rc file the suite writes, so the sibling's mutation is unobservable rather than merely unlikely to overlap — and a developer's own slow `.profile` stops failing this suite too. It also stops the contrast being vacuous. It asserted only that the raw read ended with the file contents, which holds just as well on a login shell that sourced nothing at all. The fixture rc prints a marker and the test now requires it, so "the login shell really does add chatter" is proven rather than assumed. `wsl-runner.wsl.test.ts` gains no behaviour change, only a header that states the mutation is distro-global and what that costs a sibling. --- ...worktree-filesystem-wsl-banner.wsl.test.ts | 37 ++++++++++++++++--- src/main/wsl/wsl-runner.wsl.test.ts | 9 +++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts b/src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts index a4e7e4a40f9..859898c05bb 100644 --- a/src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts +++ b/src/main/local-worktree-filesystem-wsl-banner.wsl.test.ts @@ -10,6 +10,9 @@ const runRealWsl = process.platform === 'win32' && process.env.ORCA_REAL_WSL_BAN const FILE_CONTENTS = 'line one\nline two\n' +/** Printed by the fixture HOME's own `.profile`, so the contrast below proves rc really ran. */ +const RC_CHATTER = 'ORCA_RC_CHATTER' + function unc(linuxPath: string): string { return `\\\\wsl.localhost\\${DISTRO}${linuxPath.replaceAll('/', '\\')}` } @@ -23,11 +26,25 @@ async function wsl(command: string, ...args: string[]): Promise { return stdout.trim() } -/** What an unfenced login-shell read returns — the shape this suite exists to rule out. */ -async function readThroughRawLoginShell(linuxPath: string): Promise { +/** What an unfenced login-shell read returns — the shape this suite exists to rule out. + * + * `HOME` is the fixture's, not the distro user's. One distro is shared by every suite in the + * run and by the developer running it, and `wsl-runner.wsl.test.ts` appends `sleep 60` to the + * real `~/.profile` for the length of its describe (#14288, reproduced not simulated). Reading + * the real HOME here made that sibling's state observable: run in the same Vitest invocation, + * this call inherited the 60s stall and died on its own 30s timeout, while each suite passed + * alone. Owning the rc file the contrast reads is what makes the overlap impossible rather + * than unlikely — and it also stops a developer's own slow `.profile` from failing this. */ +async function readThroughRawLoginShell(linuxPath: string, home: string): Promise { const { stdout } = await execFileAsync( 'wsl.exe', - buildWslExecArgs(DISTRO, ['sh', '-lc', buildWslLoginShellCommand(`cat -- '${linuxPath}'`)]), + buildWslExecArgs(DISTRO, [ + 'env', + `HOME=${home}`, + 'sh', + '-lc', + buildWslLoginShellCommand(`cat -- '${linuxPath}'`) + ]), { encoding: 'utf-8', timeout: 30000 } ) return stdout @@ -39,6 +56,11 @@ describe.skipIf(!runRealWsl)('WSL worktree reads carry no shell chatter', () => beforeAll(async () => { fixtureRoot = await wsl("mktemp -d -p /tmp 'orca-wsl-banner.XXXXXX'") await wsl('mkdir -p "$1/dir" && printf \'%s\' "$2" > "$1/file.txt"', fixtureRoot, FILE_CONTENTS) + await wsl( + 'mkdir -p "$1/home" && printf \'echo %s\\n\' "$2" > "$1/home/.profile"', + fixtureRoot, + RC_CHATTER + ) }, 120_000) afterAll(async () => { @@ -55,12 +77,15 @@ describe.skipIf(!runRealWsl)('WSL worktree reads carry no shell chatter', () => it('is unaffected by what a login shell would have printed', async () => { const { readPath } = getLocalWorktreePathAccess({ wslDistro: DISTRO }) - const raw = await readThroughRawLoginShell(`${fixtureRoot}/file.txt`) + const raw = await readThroughRawLoginShell(`${fixtureRoot}/file.txt`, `${fixtureRoot}/home`) // Contrast: routed through a login shell the read carries whatever the rc - // files printed (stock Ubuntu ships a sudo hint). These reads use a plain - // `sh -c`, which runs no rc at all, so they are exactly the file. + // files printed. Asserting the chatter is present first is what keeps this + // honest — without it the contrast passes on a login shell that sourced + // nothing, and the comparison below would prove nothing. + expect(raw).toContain(RC_CHATTER) expect(raw.endsWith(FILE_CONTENTS)).toBe(true) + // These reads use a plain `sh -c`, which runs no rc at all, so they are exactly the file. expect(await readPath(unc(`${fixtureRoot}/file.txt`))).toBe(FILE_CONTENTS) }, 60_000) diff --git a/src/main/wsl/wsl-runner.wsl.test.ts b/src/main/wsl/wsl-runner.wsl.test.ts index c31ac669140..53fb732853b 100644 --- a/src/main/wsl/wsl-runner.wsl.test.ts +++ b/src/main/wsl/wsl-runner.wsl.test.ts @@ -11,6 +11,15 @@ import { resolveWslExecutablePath } from './wsl-executable-path' * Gated behind an env var and win32 because it mutates the distro's `~/.profile` * to reproduce #14288. Run with: * ORCA_REAL_WSL_RUNNER_TEST=1 pnpm vitest run src/main/wsl/wsl-runner.wsl.test.ts + * + * The mutation has to be distro-global: the stall it reproduces happens inside + * `getWslGuestEnvironment`'s probe, which takes no HOME from the caller. So for + * the length of this describe, every login shell in the distro blocks for 60s — + * including any run by a sibling suite in the same Vitest invocation. That is + * not hypothetical: it silently timed out the login-shell contrast read in + * `local-worktree-filesystem-wsl-banner.wsl.test.ts`, which passed alone and + * failed in the pair. Any new WSL suite that needs a predictable `~/.profile` + * must own the HOME it reads rather than the distro user's, as that one now does. */ const DISTRO = process.env.ORCA_WSL_TEST_DISTRO ?? 'Ubuntu-24.04' const enabled = process.platform === 'win32' && process.env.ORCA_REAL_WSL_RUNNER_TEST === '1'