mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* 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.
91 lines
3.6 KiB
TypeScript
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')
|
|
})
|
|
})
|