Files
orca/src/main/codex/wsl-codex-session-bridge.test.ts
T
Brennan BensonandMerge Sim 5ff1aa540e fix(codex): re-land WSL direct-home cutover with counsel findings fixed (#16854)
* fix(codex): safely re-land WSL direct homes

* fix(codex): finish WSL direct-home cutover

* fix(codex): coalesce WSL launch hook installs

* perf(codex): avoid duplicate retired WSL session scan

* fix(codex): retain canonical WSL retired-home path

* fix(codex): fail closed before retiring WSL auth

* fix(codex): reopen WSL drain after rollback

* fix(codex): preserve WSL source on unknown panes

* fix(codex): harden repeated WSL runtime drains

* perf(codex): bound pending WSL session scans

* fix(codex): recover invalid WSL session watermarks

* fix(codex): validate retained WSL scan state

* fix(codex): accept durable WSL scan state

* test(codex): cover the drain's inode-identity guard against destination replacement

Removing the four `target_auth -ef temporary_destination_auth` assertions left
all 33 apply-script tests passing, so a regression deleting them would have
shipped silently. Reproduced before writing this.

A hash check cannot catch the case. The pinned hard link keeps the original
inode, so it still hashes correctly after another writer atomically renames a
different file over the destination path; only inode identity sees it. Without
the guard the script exits 0 and retires the source, leaving the user holding
bytes nothing validated. The new case asserts the source survives.

The harness is split by responsibility so no file exceeds its max-lines budget:
fixtures, the coreutils interference shims, the run types, the apply runner, and
the recovery/absent runners. The atomic-rename hook is deliberately separate
from the in-place rewrite shim because different guards catch them.

* fix(codex): keep the split drain harness inside the child-process boundaries

Extracting the harness into non-test modules moved it out of the exemptions the
single test file had: three new files import child_process, and two spawned
without windowsHide.

Adds the three to the import allowlist, and sets windowsHide on the spawns
rather than exempting them - the flag is correct for these calls regardless of
the ratchet, and they are skipped on win32 anyway.

---------

Co-authored-by: Merge Sim <sim@local>
2026-08-31 11:20:22 -07:00

291 lines
11 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { execFileSync } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const { runWslProcessMock } = vi.hoisted(() => ({
runWslProcessMock: vi.fn()
}))
vi.mock('../wsl/wsl-runner', () => ({
runWslProcess: runWslProcessMock
}))
import {
buildWslCodexSessionBridgeShellCommand,
resolveWslCodexSessionBridgeLinuxPaths,
startWslCodexSessionBridgeInBackground,
syncWslCodexSessionsIntoManagedHome
} from './wsl-codex-session-bridge'
function mockRunWslProcessSuccess(stdout = '{"scannedFiles":2,"linkedFiles":1}\n'): void {
runWslProcessMock.mockResolvedValue({
environmentResolved: true,
code: 0,
stdout,
stderr: '',
timedOut: false
})
}
beforeEach(() => {
runWslProcessMock.mockReset()
})
describe('syncWslCodexSessionsIntoManagedHome', () => {
it('runs a WSL hardlink bridge from the WSL system sessions into the managed home', async () => {
mockRunWslProcessSuccess()
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 2, linkedFiles: 1 })
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
const spec = runWslProcessMock.mock.calls[0]?.[0] as {
distro: string
loginPath: string
shell?: string
script: string
timeoutMs: number
}
expect(spec.distro).toBe('Ubuntu')
expect(spec.loginPath).toBe('none')
expect(spec.shell).toBe('bash')
expect(spec.timeoutMs).toBe(30_000)
const shellCommand = spec.script
expect(shellCommand).toContain("source_sessions_root='/home/alice/.codex/sessions'")
expect(shellCommand).toContain(
"managed_sessions_root='/home/alice/.local/share/orca/codex-runtime-home/home/sessions'"
)
expect(shellCommand).toContain(`find "$source_sessions_root" -type f -name '*.jsonl' -print0`)
expect(shellCommand).toContain('ln -- "$link_source" "$target_file"')
expect(shellCommand).toContain('if [ -e "$target_file" ] || [ -L "$target_file" ]; then')
expect(shellCommand).not.toContain('ln -s')
expect(shellCommand).toContain('cp --')
expect(shellCommand).not.toContain('sqlite')
})
it('does not invoke WSL when paths are not resolvable inside the distro', async () => {
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: 'C:\\Users\\alice\\.codex',
managedCodexHomePath: 'C:\\Users\\alice\\AppData\\Roaming\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 0, linkedFiles: 0 })
expect(runWslProcessMock).not.toHaveBeenCalled()
})
it('parses the summary after leading stdout noise', async () => {
mockRunWslProcessSuccess(
'Welcome to Ubuntu\nprofile output\n{"scannedFiles":4,"linkedFiles":3}\n'
)
const summary = await syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
expect(summary).toEqual({ scannedFiles: 4, linkedFiles: 3 })
})
it('throws on a non-zero exit so the background wrapper logs and swallows it', async () => {
runWslProcessMock.mockResolvedValue({
environmentResolved: true,
code: 1,
stdout: '',
stderr: 'boom',
timedOut: false
})
await expect(
syncWslCodexSessionsIntoManagedHome({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
).rejects.toThrow('WSL codex session bridge failed')
})
it('coalesces duplicate background bridges for the same WSL target', async () => {
mockRunWslProcessSuccess()
const target = {
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
}
const firstTask = startWslCodexSessionBridgeInBackground(target)
const secondTask = startWslCodexSessionBridgeInBackground(target)
expect(firstTask).toBe(secondTask)
await firstTask
expect(runWslProcessMock).toHaveBeenCalledTimes(1)
})
})
describe('resolveWslCodexSessionBridgeLinuxPaths', () => {
it('requires both homes to belong to the requested distro', () => {
expect(
resolveWslCodexSessionBridgeLinuxPaths({
distro: 'Ubuntu',
systemCodexHomePath: '\\\\wsl.localhost\\Ubuntu\\home\\alice\\.codex',
managedCodexHomePath:
'\\\\wsl.localhost\\Debian\\home\\alice\\.local\\share\\orca\\codex-runtime-home\\home'
})
).toBeNull()
})
it('accepts Linux paths for direct script construction tests', () => {
expect(
resolveWslCodexSessionBridgeLinuxPaths({
distro: 'Ubuntu',
systemCodexHomePath: '/home/alice/.codex',
managedCodexHomePath: '/home/alice/.local/share/orca/codex-runtime-home/home'
})
).toEqual({
systemSessionsRoot: '/home/alice/.codex/sessions',
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
})
})
describe('buildWslCodexSessionBridgeShellCommand', () => {
it('only targets JSONL session files under sessions', () => {
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: "/home/alice/.codex/sessions with 'quote'",
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
expect(shellCommand).toContain(
`source_sessions_root='/home/alice/.codex/sessions with '\\''quote'\\'''`
)
expect(shellCommand).toContain(`-name '*.jsonl'`)
expect(shellCommand).not.toContain('.sqlite')
})
it('keeps Linux-side shell variable expansion intact for the guest shell', () => {
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: '/home/alice/.codex/sessions',
managedSessionsRoot: '/home/alice/.local/share/orca/codex-runtime-home/home/sessions'
})
expect(shellCommand).toContain('$source_sessions_root')
expect(shellCommand).toContain('$source_file')
expect(shellCommand).toContain('$((scanned_files + 1))')
})
it.skipIf(process.platform === 'win32')(
'publishes a verified guest-side copy across filesystems',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-cross-fs-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const binDir = join(root, 'bin')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const targetPath = join(managedSessionsRoot, relativePath)
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(binDir)
writeFileSync(sourcePath, '{"session":"retired"}\n', 'utf-8')
const lnShimPath = join(binDir, 'ln')
writeFileSync(
lnShimPath,
`#!/bin/sh
if [ "$2" = "$BRIDGE_SOURCE" ] && [ "$3" = "$BRIDGE_TARGET" ]; then
exit 1
fi
exec /bin/ln "$@"
`
)
chmodSync(lnShimPath, 0o755)
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
execFileSync('/bin/bash', ['-c', shellCommand], {
env: {
...process.env,
BRIDGE_SOURCE: sourcePath,
BRIDGE_TARGET: targetPath,
PATH: `${binDir}:${process.env.PATH ?? ''}`
}
})
expect(readFileSync(targetPath, 'utf-8')).toBe('{"session":"retired"}\n')
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'leaves an existing copied session to its current writer',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-existing-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const targetPath = join(managedSessionsRoot, relativePath)
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(join(targetPath, '..'), { recursive: true })
writeFileSync(sourcePath, '{"session":"retired"}\n{"event":"legacy"}\n', 'utf-8')
writeFileSync(targetPath, '{"session":"retired"}\n', 'utf-8')
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
execFileSync('/bin/bash', ['-c', shellCommand])
expect(readFileSync(targetPath, 'utf-8')).toBe('{"session":"retired"}\n')
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
it.skipIf(process.platform === 'win32')(
'fails in the guest shell when a missing session cannot be linked, then retries cleanly',
() => {
const root = mkdtempSync(join(tmpdir(), 'orca-wsl-session-bridge-'))
const sourceSessionsRoot = join(root, 'legacy', 'sessions')
const managedSessionsRoot = join(root, 'managed', 'sessions')
const relativePath = join('2026', '08', '26', 'retired.jsonl')
const sourcePath = join(sourceSessionsRoot, relativePath)
const blockingPath = join(managedSessionsRoot, '2026')
mkdirSync(join(sourcePath, '..'), { recursive: true })
mkdirSync(managedSessionsRoot, { recursive: true })
writeFileSync(sourcePath, '{"session":"retired"}\n', 'utf-8')
writeFileSync(blockingPath, 'not-a-directory\n', 'utf-8')
const shellCommand = buildWslCodexSessionBridgeShellCommand({
systemSessionsRoot: sourceSessionsRoot,
managedSessionsRoot
})
try {
expect(() => execFileSync('/bin/bash', ['-c', shellCommand])).toThrow()
rmSync(blockingPath)
execFileSync('/bin/bash', ['-c', shellCommand])
expect(readFileSync(join(managedSessionsRoot, relativePath), 'utf-8')).toBe(
'{"session":"retired"}\n'
)
} finally {
rmSync(root, { recursive: true, force: true })
}
}
)
})