Files
orca/src/shared/setup-agent-sequencing-codex-no-daemon.test.ts
T
Jinwoo Hong 26bb7c23f1 fix(terminal): run Orca's cmd.exe, path-named and setup-gated Codex launches without the shared server (#23933)
* fix(terminal): give plain shells and cmd.exe Codex launches --no-daemon

Plain bash, zsh and fish tabs were never wrapped, so a typed codex skipped the
shell function that adds --no-daemon. Wrap them (bash keeps its prompt and
DEBUG trap untouched unless Orca asked for command markers), add --no-daemon
host-side where no function can run (cmd.exe, path-named binaries), and move
new tabs to a v38 terminal daemon so they get the new wrappers.

* fix(terminal): keep plain bash a login shell and give the setup gate the codex function

Plain bash and Git Bash tabs launch exactly as before again: the rcfile
wrapper would have made every one a non-login shell. Plain tabs on the
user's configured shell args stay unwrapped on both transports. The
wait-for-setup gate's bash -lc now defines the codex function, so a
sequenced Codex launch gets --no-daemon from the binary it actually runs.

* fix(terminal): define the setup gate's codex function after setup finishes

Setup can be what puts codex on PATH, so defining the function before the
marker wait found no binary and skipped --no-daemon.

* refactor(terminal): fold the SSH/WSL guard into the Codex launch planner and bound the gate test

* fix(terminal): honour the pane's env deletions in the Codex opt-out check

Also pin the setup-gate test's fake codex ahead of path_helper's PATH.

* revert(terminal): launch plain zsh and fish tabs exactly as on main

Drops the always-wrap for plain zsh and fish, the configured-args guard
that only served it, and the v38 daemon bump: the daemon's launch configs
and generated wrappers are byte-identical to main again. Keeps the
host-side --no-daemon for cmd.exe and path-named launches and the setup
gate's codex function.
2026-09-29 21:40:53 -04:00

91 lines
3.6 KiB
TypeScript

import { spawn, spawnSync, type ChildProcess } from 'node:child_process'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { createSequencedSetupAgentCommands } from './setup-agent-sequencing'
// Why: the POSIX gate evals the agent in a fresh `bash -lc`, which never reads
// Orca's shell wrapper, so it must carry the codex --no-daemon rule itself.
const roots: string[] = []
const gates: ChildProcess[] = []
afterEach(() => {
for (const gate of gates.splice(0)) {
gate.kill()
}
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function prepareGate(startupCommand: string) {
const root = mkdtempSync(join(tmpdir(), 'orca-gate-codex-'))
roots.push(root)
const bin = join(root, 'bin')
mkdirSync(bin)
// Why: `bash -l` reads /etc/profile, whose macOS path_helper moves the fixture bin
// behind /usr/local/bin and /opt/homebrew/bin; re-prepend it so a real codex never runs.
writeFileSync(join(root, '.bash_profile'), `export PATH=${JSON.stringify(bin)}:"$PATH"\n`)
const runner = join(root, 'setup-runner.sh')
const sequenced = createSequencedSetupAgentCommands({
runnerScriptPath: runner,
startupCommand,
platform: 'posix',
nonce: 'n1',
// Why: a failed assertion must not leave a gate polling for the default two hours.
waitTimeoutSeconds: 5
})
return {
sequenced,
// Why HOME and CODEX_HOME: `bash -l` must read no real profile, and nothing may touch ~/.codex.
env: { HOME: root, CODEX_HOME: root, PATH: `${bin}:/usr/bin:/bin`, ...sequenced.startupEnv },
finishSetup: (help: string) => {
const codex = join(bin, 'codex')
writeFileSync(
codex,
`#!/bin/sh\n[ "$1" = --help ] && { printf '%s\\n' '${help}'; exit 0; }\nprintf 'ARGV:%s\\n' "$*"\n`
)
chmodSync(codex, 0o755)
writeFileSync(`${runner}.n1.done`, 'n1:0\n')
}
}
}
function runGate(startupCommand: string, help: string, env: Record<string, string> = {}): string {
const gate = prepareGate(startupCommand)
gate.finishSetup(help)
return spawnSync('bash', ['-c', gate.sequenced.startupCommand], {
encoding: 'utf8',
env: { ...gate.env, ...env }
}).stdout
}
describe.skipIf(process.platform === 'win32')('sequenced setup gate runs codex', () => {
it('with --no-daemon when the binary supports it', () => {
expect(runGate('codex --yolo', '--no-daemon')).toBe('ARGV:--no-daemon --yolo\n')
})
it.each([
['an old binary', 'codex --yolo', '--no-alt-screen', {}],
['a subcommand that needs the shared server', 'codex agents', '--no-daemon', {}],
['the opt-out', 'codex --yolo', '--no-daemon', { ORCA_CODEX_ISOLATE: '0' }]
])('unchanged for %s', (_case, command, help, env) => {
expect(runGate(command, help, env)).toBe(`ARGV:${command.slice('codex '.length)}\n`)
})
it('with --no-daemon when setup is what installs codex', async () => {
const gate = prepareGate('codex --yolo')
const child = spawn('bash', ['-c', gate.sequenced.startupCommand], { env: gate.env })
gates.push(child)
let stdout = ''
child.stdout.on('data', (chunk: Buffer) => (stdout += chunk.toString()))
const exited = new Promise((resolve) => child.on('close', resolve))
// Why after spawn: the gate is already waiting when setup puts codex on PATH.
await new Promise((resolve) => setTimeout(resolve, 300))
gate.finishSetup('--no-daemon')
await exited
expect(stdout).toBe('ARGV:--no-daemon --yolo\n')
})
})