codeburn/tests/codex-cache-wsl-flush.test.ts
Resham Joshi 4cf1885593
windows: discover provider roots inside WSL distros (#1062)
* feat(windows): discover provider roots inside WSL distros

Agents run inside a WSL distro write their history to the distro's Linux
home, exposed to Windows as \\wsl$\<distro>\... and never under the user
profile, so a Windows-only scan reported zero sessions for them.

src/wsl.ts lists distros through %SystemRoot%\System32\wsl.exe (absolute,
so nothing dropped next to the CLI can impersonate it) with a 3s timeout,
decodes its UTF-16LE output, drops container-runtime distros, and
enumerates each distro's home/* and root. Running distros only by
default: touching \\wsl$ boots a stopped distro. CODEBURN_WSL=all|off
opts in or out. Inert off win32 and memoized per process.

claude and codex append <wslHome>/.claude and <wslHome>/.codex to their
existing root lists, so multi-root discovery, probeRoots() and doctor
cover them unchanged. Roots are additive, never an override.

CODEBURN_WSL stays out of PROVIDER_ENV_VARS: cache entries are keyed by
source path, so it can only add or drop paths, never make a kept entry
stale, while declaring it would force a re-parse on every toggle.

Fingerprints drop dev/ino for \\wsl$ paths in both caches. The 9P share
synthesizes them per mount, so keying on them would re-parse every WSL
session on every run; mtime+size still catches edits and appends.

Refs #1059

* test(windows): cover WSL root discovery and the 9P carve-outs

UTF-16 output parsing, utility-distro filtering, CODEBURN_WSL modes, UNC
classification, claude/codex root injection and discovery, probeRoots,
fingerprint reconciliation with zeroed dev/ino, and the win32 guard that
keeps a Linux cwd from being walked on Windows.

Refs #1059

* docs(windows): document WSL discovery and CODEBURN_WSL

Refs #1059

* fix(windows): harden WSL discovery from adversarial review

Probe \\wsl$\ before \\wsl.localhost\. The older spelling works on every
WSL build; probing the newer one first sends builds without it through
MUP, SMB and DNS resolution for a host named "wsl.localhost" — seconds
of stall per distro, every process.

Only accept lines shaped like a distro name. With nothing installed,
wsl.exe prints prose on stdout whose lines otherwise read as distro
names and get probed over UNC. A name must be a single token with none
of the characters Windows bans from a path component and no trailing
period, which discards that message in any UI language.

Do not evict a \\wsl$ cache entry for being undiscovered, and serve it
in write mode too. A stopped distro drops its whole root out of
discovery; treating that as a deletion re-parsed everything over 9P on
the next start and made totals drop while the read-only serve path still
showed them. The exemption is scoped to isWslUncPath — an ordinary path
whose file is gone is still evicted.

Also: getCachedCodexProject was the one fingerprint site still keying on
dev/ino for WSL paths; doctor now prints why it probed no WSL roots; and
a distro contributing several homes labels them per user rather than
letting makeUniqueLabels number them.

Refs #1059

* fix(windows): re-validate running WSL distros on a TTL before UNC access

wslHomes() memoized the running-distro homes for the process lifetime. In
long-lived serve/menubar use a distro cached as running could be shut down
and the next refresh would still touch its \\wsl$ UNC path — which can hang
win32 fs calls — while a distro started after an empty discovery stayed
invisible until restart.

Discovery now caches for a 60s TTL only; on expiry the wsl.exe -l --running
probe (timeout-bounded) re-validates the running list before any UNC path
is touched, and a failed probe fails closed to native-only discovery rather
than serving stale homes. Covers both shutdown and later-start transitions.

Addresses the blocking review finding on PR #1062.

* fix(windows): harden WSL cache reconciliation

* fix(windows): bound WSL orphan refresh to the discovery TTL

refreshWslHomes re-spawned wsl.exe on every full parse for a stopped-but-
cached distro, ignoring the 60s TTL — up to ~3s twice per parse in the
resident serve/menubar. Reuse the TTL-bounded wslHomes() probe instead; the
same TTL already reflects the only transition the orphan check must catch,
offline->running.

* test(windows): skip on-disk POSIX-worktree walk case on real win32

The fixture builds a worktree under tmpdir() and treats it as a POSIX cwd the
win32 guard should reject. On a real Windows runner tmpdir() is a C:\ path, so
the guard rightly walks it and canonicalizes to the main repo, failing the
assertion. The case is only constructible on a POSIX host (mocked win32); the
literal /home/... test already covers the guard on Windows.

---------

Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
2026-09-19 12:33:29 -07:00

70 lines
2.4 KiB
TypeScript

import { mkdtemp, readFile, rm } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const statCalls = vi.hoisted(() => [] as string[])
// Keep real fs/promises behavior for the cache file while recording source
// probes. The regression must prove that flush never asks the OS about a WSL
// UNC path, because a stopped distro can make that call hang.
vi.mock('fs/promises', async () => {
const actual = await vi.importActual<typeof import('fs/promises')>('fs/promises')
return {
...actual,
stat: (...args: Parameters<typeof actual.stat>) => {
const source = String(args[0])
statCalls.push(source)
// Never touch a real share from the regression itself. If the flush
// loop regresses, fail fast instead of inheriting the Windows UNC
// timeout this test is intended to prevent.
if (/^\\\\wsl(\$|\.localhost)\\/i.test(source)) {
return Promise.reject(new Error('stubbed WSL share'))
}
return actual.stat(...args)
},
}
})
const {
clearCodexMemCaches,
codexCacheFileName,
flushCodexCache,
withCodexCacheDirectory,
writeCachedCodexResults,
} = await import('../src/codex-cache.js')
let root: string
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'codeburn-codex-wsl-flush-'))
statCalls.length = 0
clearCodexMemCaches()
})
afterEach(async () => {
clearCodexMemCaches()
await rm(root, { recursive: true, force: true })
})
describe('Codex cache flush with WSL sources', () => {
it('retains WSL UNC entries without statting them, while evicting missing paths', async () => {
const cacheDir = join(root, 'cache')
const missingPath = join(root, 'missing.jsonl')
const wslPath = String.raw`\\wsl$\Ubuntu\home\alice\.codex\sessions\rollout.jsonl`
const fingerprint = { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }
await withCodexCacheDirectory(cacheDir, async () => {
await writeCachedCodexResults(wslPath, 'wsl-project', [], fingerprint)
await writeCachedCodexResults(missingPath, 'native-project', [], fingerprint)
await flushCodexCache()
})
const disk = JSON.parse(await readFile(join(cacheDir, codexCacheFileName()), 'utf8'))
expect(disk.files[wslPath]).toMatchObject({ project: 'wsl-project' })
expect(disk.files[missingPath]).toBeUndefined()
expect(statCalls).toContain(missingPath)
expect(statCalls).not.toContain(wslPath)
})
})