Files
orca/src/main/codex/codex-trust-config-mutation-queue.test.ts
T
Brennan Benson 3558cf943f fix(codex): heal WSL hooks before typed launches (#16535)
* fix(codex): heal WSL hooks before typed launches

* test(codex): keep launcher fixture type-safe on Windows

* fix(build): list codex-home-wsl-env in the CLI typecheck project

`managed-home-shell-preflight.ts` is already in the CLI project's include list and now imports
`wslCodexRuntimeHomeForGuestHome` from `src/main/pty/codex-home-wsl-env.ts`, which the list did not
cover — TS6307, so the CLI typecheck failed on every push.

Added the single module rather than a `src/main/pty/**` glob: it is a 31-line leaf with no imports
of its own, so it does not widen what the CLI bundle can reach.

* fix(codex): converge the two WSL hook install lanes onto one writer

Two independent readiness reviews agreed the Orca-terminal boundary holds, but Codex Sol found a
P1 the other rated P2: the new just-in-time repair raced the existing relay installer and the two
produced DIFFERENT hook and trust representations for the same managed home. Two unserialized
writers emitting different formats is worse than the bug this PR fixes, because it fails
intermittently rather than cleanly — a pane works or does not depending on which lane won.

- Relay Codex installs now delegate to the runtime-home writer, so there is one canonical
  representation instead of two. Redirected scripts use the runtime path, the readable wrapper,
  and the prepended group.
- `installForRuntimeHomeSerialized` puts every asynchronous WSL caller for a given home on one
  queue (`wslInstallQueues`), so concurrent panes cannot interleave writes.

Also rewrites the stale pin test the new `-x` guard broke. It asserted the defect —
"would run the impostor if the preflight carried an unqualified command name", expecting the
hijack marker to exist. The guard is a security improvement, so the test now asserts the contract:
an unqualified preflight is skipped and the marker is never written. Rewritten to the new
behavior, not loosened or deleted.

818 tests pass across the affected suites; typecheck clean. The changed-file quality gate could
not run locally — its pnpm engine-warning JSON parser fails under Node 26 — so CI covers it.

The boundary both reviews verified is untouched: paired/relay/mobile clients stay hard-blocked
from the RPC, params remain shape-locked to the managed home suffix with traversal rejection,
nothing is written outside the managed home, and macOS/Linux stay inert.

* fix(codex): serialize resolved WSL hook homes

* fix(codex): recover managed WSL homes after restart

* fix(wsl): translate Codex preflight through WSLENV

* fix(cli): cover bounded WSL Codex repair

* fix(codex): coalesce duplicate WSL hook repairs

* fix(codex): verify reconstructed WSL homes
2026-08-27 13:05:12 -07:00

144 lines
4.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { runExclusivelyForCodexTrustConfig } from './codex-trust-config-mutation-queue'
function deferred(): { promise: Promise<void>; resolve: () => void; reject: (e: unknown) => void } {
let resolve!: () => void
let reject!: (e: unknown) => void
const promise = new Promise<void>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
describe('runExclusivelyForCodexTrustConfig', () => {
// Why: the grant lane runs inside the installer that already owns the file;
// a non-reentrant lane would queue it behind itself and never settle.
it('passes through a nested acquire of a lane the caller already holds', async () => {
const nested = await runExclusivelyForCodexTrustConfig('/a/config.toml', () =>
runExclusivelyForCodexTrustConfig('/a/config.toml', () => Promise.resolve('inner'))
)
expect(nested).toBe('inner')
})
it('still queues an unrelated lane acquired from inside another lane', async () => {
const gate = deferred()
let innerRan = false
const blocking = runExclusivelyForCodexTrustConfig('/b/config.toml', () => gate.promise)
const nested = runExclusivelyForCodexTrustConfig('/a/config.toml', () =>
runExclusivelyForCodexTrustConfig('/b/config.toml', () => {
innerRan = true
return Promise.resolve()
})
)
await Promise.resolve()
expect(innerRan).toBe(false)
gate.resolve()
await blocking
await nested
expect(innerRan).toBe(true)
})
it('runs one mutation at a time per config.toml', async () => {
const order: string[] = []
const first = deferred()
const second = deferred()
const a = runExclusivelyForCodexTrustConfig('/home/.codex/config.toml', async () => {
order.push('a:start')
await first.promise
order.push('a:end')
return 'a'
})
const b = runExclusivelyForCodexTrustConfig('/home/.codex/config.toml', async () => {
order.push('b:start')
await second.promise
order.push('b:end')
return 'b'
})
await Promise.resolve()
expect(order).toEqual(['a:start'])
first.resolve()
await a
second.resolve()
await b
expect(order).toEqual(['a:start', 'a:end', 'b:start', 'b:end'])
})
it('keeps distinct config.toml paths independent', async () => {
const gate = deferred()
let secondRan = false
const blocked = runExclusivelyForCodexTrustConfig('/a/config.toml', () => gate.promise)
await runExclusivelyForCodexTrustConfig('/b/config.toml', async () => {
secondRan = true
})
expect(secondRan).toBe(true)
gate.resolve()
await blocked
})
it('keeps the queue alive after a rejected mutation', async () => {
const failing = runExclusivelyForCodexTrustConfig('/a/config.toml', () =>
Promise.reject(new Error('grant blew up'))
)
await expect(failing).rejects.toThrow('grant blew up')
await expect(
runExclusivelyForCodexTrustConfig('/a/config.toml', () => Promise.resolve('next'))
).resolves.toBe('next')
})
// Why: normalized keys, so a Windows caller passing the other separator or
// case must still land in the same lane as the run it has to wait for.
it('serializes equivalent paths that differ only in normalization', async () => {
const gate = deferred()
let secondStarted = false
const blocked = runExclusivelyForCodexTrustConfig(
String.raw`C:\Users\Alice\.codex\config.toml`,
() => gate.promise
)
const queued = runExclusivelyForCodexTrustConfig('C:/Users/Alice/.codex/config.toml', () => {
secondStarted = true
return Promise.resolve()
})
await Promise.resolve()
expect(secondStarted).toBe(false)
gate.resolve()
await blocked
await queued
expect(secondStarted).toBe(true)
})
it('coalesces WSL UNC aliases without folding the case-sensitive Linux path', async () => {
const aliasGate = deferred()
let aliasStarted = false
const blockedAlias = runExclusivelyForCodexTrustConfig(
String.raw`\\wsl.localhost\Ubuntu\home\Alice\.codex\config.toml`,
() => aliasGate.promise
)
const queuedAlias = runExclusivelyForCodexTrustConfig(
String.raw`\\wsl$\ubuntu\home\Alice\.codex\config.toml`,
() => {
aliasStarted = true
return Promise.resolve()
}
)
await Promise.resolve()
expect(aliasStarted).toBe(false)
let distinctStarted = false
await runExclusivelyForCodexTrustConfig(
String.raw`\\wsl.localhost\Ubuntu\home\alice\.codex\config.toml`,
async () => {
distinctStarted = true
}
)
expect(distinctStarted).toBe(true)
aliasGate.resolve()
await blockedAlias
await queuedAlias
expect(aliasStarted).toBe(true)
})
})