mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* 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
144 lines
4.7 KiB
TypeScript
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)
|
|
})
|
|
})
|