Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
b6dddf934This file is pinned to the selected release. Download the original to open it in your design tool.
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
AGENT_NAME_RE,
BYPASS_PERMISSION_FLAGS,
FIRST_PROMPT_ARGS,
FirstPromptUnsupportedError,
LAUNCH_RESUME_FLAG,
NAMED_AGENT_ARGS,
NamedAgentUnsupportedError,
HARNESS_WORDMARK_LINES,
PERMISSION_MODES,
terminalHintLines,
buildEngineCommandArgv,
buildEngineLaunchArgv,
commandAvailableInInteractiveShell,
commandSupportsFlagInInteractiveShell,
dropPermissionFlagIfUnsupported,
permissionFlagToVerify,
permissionModeFlags,
refusePermissionFlagIfUnsupported,
resetCommandFlagSupportCache,
engineFallbackPrelude,
firstPromptArgs,
gridPanePrelude,
harnessNodePrelude,
namedAgentArgs,
supportsFirstPrompt,
supportsNamedAgent,
unreadableCwdGuard,
} from './engineLaunch.js'
import { ENGINES, type AgentEngine } from '../engines/types.js'
import { engineBin } from './engineBin.js'
import type { EngineInstallRecipe } from './engineInstall.js'
import { MIN_OPEN_FILES, RAISE_OPEN_FILES_SH } from './openFiles.js'
import { DSH_SESSION_ENV, harnessEnvToClear } from '../dsh/launch.js'
// The launch script names the `grid` the daemon resolved, and a developer's own HARNESS_GRID_BIN
// would resolve to THEIR grid. The suite's runtime dir is already a throwaway (vitest.setup.ts), so
// with the override gone every case below resolves to the bare name.
const developersOwnGridBin = process.env.HARNESS_GRID_BIN
beforeEach(() => { delete process.env.HARNESS_GRID_BIN })
afterAll(() => { if (developersOwnGridBin !== undefined) process.env.HARNESS_GRID_BIN = developersOwnGridBin })
/** The prelude every case below gets by default: no managed grid on this machine, so PATH is left
* alone and only grid's update check is turned off. */
const GRID_PRELUDE = gridPanePrelude('grid')
/** The engine-in-a-shell wrapper every launch carries, for the shell each case names — see
* `engineFallbackPrelude`. `null` tmux: the suite must not depend on what this machine has. */
const FALLBACK = (engine: AgentEngine, shell: string) => engineFallbackPrelude(engine, shell, null)
const NO_TMUX = null
describe('buildEngineLaunchArgv', () => {
it.each(['claude', 'terminal'] as const)('clears inherited harness context before a plain %s session runs', (engine) => {
const argv = buildEngineLaunchArgv(engine, { clearEnv: harnessEnvToClear() }, '/bin/sh', undefined, undefined, NO_TMUX)
const probe = 'for name in HARNESS_DSH HARNESS_DSH_DIR HARNESS_WORKSPACE HARNESS_CONTEXT_FILE HARNESS_SKILLS_DIR HARNESS_PRIVATE_GRID; do printenv "$name" && exit 9; done; printf "%s" "$KEEP_ME"'
const args = engine === 'terminal' ? ['harness-terminal', ''] : ['harness-engine']
const out = execFileSync(argv[0], [argv[1], argv[2], ...args, '/bin/sh', '-c', probe], {
encoding: 'utf8',
env: { ...process.env, ...Object.fromEntries(DSH_SESSION_ENV.map((name) => [name, 'stale-harness'])), KEEP_ME: 'retained' },
stdio: ['ignore', 'pipe', 'pipe'],
})
expect(out).toBe('retained')
})
it('wraps zsh in its interactive login form and execs the resolved binary', () => {
expect(buildEngineLaunchArgv('claude', {}, '/bin/zsh', undefined, undefined, NO_TMUX)).toEqual([
'/bin/zsh', '-lic', `${RAISE_OPEN_FILES_SH}${FALLBACK('claude', '/bin/zsh')}${GRID_PRELUDE}harness_engine "$@"`, 'harness-engine', engineBin('claude'),
])
})
it('uses Ubuntu bash interactive startup files without making it a login shell', () => {
expect(buildEngineLaunchArgv('claude', {}, '/bin/bash', undefined, undefined, NO_TMUX)).toEqual([
'/bin/bash', '-ic', `${RAISE_OPEN_FILES_SH}${FALLBACK('claude', '/bin/bash')}${GRID_PRELUDE}harness_engine "$@"`, 'harness-engine', engineBin('claude'),
])
})
it('enters the workspace only after interactive startup has completed', () => {
const argv = buildEngineLaunchArgv('claude', { cwd: '/work/project' }, '/bin/zsh', undefined, undefined, NO_TMUX)
expect(argv).toEqual([
'/bin/zsh', '-lic',
`${RAISE_OPEN_FILES_SH}${FALLBACK('claude', '/bin/zsh')}${GRID_PRELUDE}if ! cd -- "$1"; then printf '%s\\n' 'harness: the selected working directory is unavailable.' >&2; exit 1; fi\n${unreadableCwdGuard(process.platform)}shift\nharness_engine "$@"`,
'harness-engine', '/work/project', engineBin('claude'),
])
})
describe('the engine runs INSIDE the pane shell (`harness_engine`), never exec\'d over it', () => {
const run = (argv: string[]) => {
try {
return { status: 0, out: execFileSync(argv[0], argv.slice(1), { stdio: ['ignore', 'pipe', 'pipe'] }).toString() }
} catch (error) {
const failed = error as { status: number | null; stdout: Buffer; stderr: Buffer }
return { status: failed.status, out: `${failed.stdout}${failed.stderr}` }
}
}
it('hands the engine its argv untouched, and without a terminal on stdin reports its exit status', () => {
const argv = buildEngineLaunchArgv('claude', {}, '/bin/sh', undefined, undefined, NO_TMUX)
// The launcher's own argv, then the "engine": `sh -c 'echo …; exit 3'` stands in for one.
const script = [argv[0], argv[1], argv[2], 'harness-engine', '/bin/sh', '-c', 'printf "%s\\n" "args:$*"; exit 3', 'engine', 'a b', '--flag']
const result = run(script)
expect(result.out).toContain('args:a b --flag')
expect(result.status).toBe(3)
// No shell was handed over and no "this pane is a shell now" line was printed: no tty.
expect(result.out).not.toContain('This pane is a shell now')
})
it('a take-over that waits says so, and starts the engine only once the other process is gone', () => {
// Not a child of this process, as the terminal's is not: an unreaped child never looks gone.
const pid = Number(execFileSync('/bin/sh', ['-c', 'sleep 1 >/dev/null 2>&1 & echo $!']).toString().trim())
try {
const argv = buildEngineLaunchArgv('codex', { waitForPid: { pid, name: 'Codex' } }, '/bin/sh', undefined, undefined, NO_TMUX)
const started = Date.now()
const result = run([argv[0], argv[1], argv[2], 'harness-engine', '/bin/sh', '-c', 'printf "%s\\n" "engine ran"'])
expect(Date.now() - started).toBeGreaterThanOrEqual(800)
expect(result.out).toContain('Waiting for the Codex in your terminal to finish its turn.')
expect(result.out.indexOf('Waiting for')).toBeLessThan(result.out.indexOf('engine ran'))
expect(result.status).toBe(0)
} finally {
try { process.kill(pid) } catch { /* gone */ }
}
// Nothing to wait for: it starts at once.
const argv = buildEngineLaunchArgv('codex', { waitForPid: { pid: 2 ** 22 + 7, name: 'Codex' } }, '/bin/sh', undefined, undefined, NO_TMUX)
const started = Date.now()
expect(run([argv[0], argv[1], argv[2], 'harness-engine', '/bin/sh', '-c', 'printf "%s\\n" "engine ran"']).out).toContain('engine ran')
expect(Date.now() - started).toBeLessThan(800)
})
it('a command that does not exist still ends the pane with 127, so "not installed" stays a launch failure', () => {
const argv = buildEngineLaunchArgv('claude', {}, '/bin/sh', undefined, undefined, NO_TMUX)
expect(run([argv[0], argv[1], argv[2], 'harness-engine', '/nowhere/claude-that-is-not-here']).status).toBe(127)
})
it('marks the pane with the exit status through the daemon\'s own tmux, and only then hands over a shell', () => {
const prelude = engineFallbackPrelude('codex', '/bin/bash', '/opt/homebrew/bin/tmux')
expect(prelude).toContain(`[ -n "\${TMUX_PANE:-}" ] && '/opt/homebrew/bin/tmux' set-option -p -t "$TMUX_PANE" @harness_engine_exit "$harness_status"`)
expect(prelude.indexOf('set-option')).toBeLessThan(prelude.indexOf("exec '/bin/bash'"))
expect(prelude).toContain('if [ "$harness_status" -eq 127 ]; then exit 127; fi')
// zsh is a login shell; bash keeps its interactive rc (same rule as a terminal).
expect(engineFallbackPrelude('codex', '/bin/zsh', null)).toContain("exec '/bin/zsh' -l\n")
expect(engineFallbackPrelude('codex', '/bin/bash', null)).toContain("exec '/bin/bash'\n")
expect(engineFallbackPrelude('codex', '/bin/bash', null)).not.toContain('set-option')
})
})
describe('a terminal (engine `terminal`)', () => {
it('is the login shell itself behind a non-interactive wrapper that only raises the limit and enters the folder', () => {
expect(buildEngineLaunchArgv('terminal', { cwd: '/work/project' }, '/bin/zsh')).toEqual([
'/bin/zsh', '-c',
`${RAISE_OPEN_FILES_SH}if ! cd -- "$1"; then printf '%s\\n' 'harness: the selected working directory is unavailable.' >&2; fi\nshift\nexec "$@"`,
'harness-terminal', '/work/project', '/bin/zsh', '-l',
])
})
it('gives bash no login flag (its rc file is where Ubuntu keeps PATH edits), and no folder means no cd', () => {
expect(buildEngineLaunchArgv('terminal', {}, '/bin/bash')).toEqual([
'/bin/bash', '-c', `${RAISE_OPEN_FILES_SH}shift\nexec "$@"`, 'harness-terminal', '', '/bin/bash',
])
})
it('falls back to /bin/sh rather than to an engine command when no shell resolves', () => {
expect(buildEngineLaunchArgv('terminal', {}, 'relative-shell')).toEqual([
'/bin/sh', '-c', `${RAISE_OPEN_FILES_SH}shift\nexec "$@"`, 'harness-terminal', '', '/bin/sh',
])
})
it('ignores every engine option: nothing to bypass, resume, prompt or install', () => {
const argv = buildEngineLaunchArgv('terminal', {
cwd: '/w', bypassPermission: true, resumeSessionId: 'abc', firstPrompt: 'hi', extraArgs: ['--x'],
}, '/bin/zsh')
expect(argv.slice(-2)).toEqual(['/bin/zsh', '-l'])
expect(argv.join(' ')).not.toContain('abc')
expect(argv.join(' ')).not.toContain('--x')
})
it('is a script a real shell accepts and that keeps a cd failure from ending the terminal', () => {
const script = buildEngineLaunchArgv('terminal', { cwd: '/nowhere/at/all' }, '/bin/sh')[2]
expect(() => execFileSync('/bin/sh', ['-n', '-c', script])).not.toThrow()
// The wrapper's `exec "$@"` runs the shell handed in argv; `true` in its place proves the
// wrapper reaches the exec even when the cd failed (the terminal still opens, at $HOME).
const out = execFileSync('/bin/sh', ['-c', script, 'harness-terminal', '/nowhere/at/all', '/bin/sh', '-c', 'echo reached'], { stdio: ['ignore', 'pipe', 'pipe'] })
expect(out.toString()).toContain('reached')
})
it('greets every new tile with the banner — wordmark, where it is, agents, `harness remote` — before its prompt', () => {
const argv = buildEngineLaunchArgv('terminal', { terminalHint: { machineName: 'MacbookPro.local' } }, '/bin/sh')
expect(argv.slice(3)).toEqual(['harness-terminal', '', '/bin/sh'])
expect(() => execFileSync('/bin/sh', ['-n', '-c', argv[2]])).not.toThrow()
const run = (): string => execFileSync('/bin/sh', ['-c', argv[2], 'harness-terminal', '', '/bin/sh', '-c', 'echo prompt'], { stdio: ['ignore', 'pipe', 'pipe'] }).toString()
// Every time, not once per machine: a new tile is a new person at a new prompt.
for (const out of [run(), run()]) {
// Every line as drawn — the wordmark's backslashes and backtick included.
expect(out).toContain(terminalHintLines('MacbookPro.local').join('\n'))
expect(out).toContain(' Terminal on MacbookPro.local')
expect(out).toContain('harness remote')
expect(out.trim().endsWith('prompt')).toBe(true)
}
})
it('the banner names the machine, falls back to "this machine", and fits an 80-column pane unwrapped', () => {
expect(terminalHintLines(' ')).toContain(' Terminal on this machine')
for (const line of terminalHintLines('office-imac')) expect(line.length).toBeLessThanOrEqual(78)
// A hostname longer than the line has room for is cut, never wrapped.
const long = terminalHintLines('a'.repeat(120))
for (const line of long) expect(line.length).toBeLessThanOrEqual(78)
expect(long.find((line) => line.startsWith(' Terminal on'))).toMatch(/^ Terminal on a{61}…$/)
// A blank line first and last: the wordmark does not sit on the pane's top edge, nor the prompt on the guide.
const lines = terminalHintLines('office-imac')
expect([lines[0], lines[lines.length - 1]]).toEqual(['', ''])
})
it('the wordmark is the one the installer prints, line for line', () => {
// install.sh's print_logo, run for real: what a person sees at the end of an install is what a
// tile shows them next — two copies, held to one drawing.
const installer = readFileSync(join(process.cwd(), 'scripts', 'install.sh'), 'utf8')
const printLogo = installer.slice(installer.indexOf('print_logo() {'), installer.indexOf('\n}\n', installer.indexOf('print_logo() {')) + 3)
const printed = execFileSync('/bin/sh', ['-c', `${printLogo}\nprint_logo`], { encoding: 'utf8' }).split('\n').filter((line) => line.trim() !== '')
expect(printed).toEqual([...HARNESS_WORDMARK_LINES])
})
it('with no guide asked for (restore, restart, a test) the pane says nothing before its prompt', () => {
const script = buildEngineLaunchArgv('terminal', {}, '/bin/sh')[2]
expect(script).not.toContain('harness remote')
expect(script).not.toContain('Terminal on')
})
})
it('hands the engine a soft open-files limit fit for it, however low the pane started', async () => {
// A tmux server started by the desktop app passes launchd's 256 to every pane; Claude Code will
// not start under that. The pane's own shell lifts it before exec, so the server never has to.
const [shell, flag, paneScript] = buildEngineLaunchArgv('claude', {}, '/bin/sh')
const { execFile } = await import('node:child_process')
const seenByEngine = await new Promise<string>((resolve, reject) => {
execFile(
'/bin/sh',
['-c', 'ulimit -S -n 256 || exit 99; exec "$@"', 'pane', shell, flag, paneScript, 'harness-engine', '/bin/sh', '-c', 'ulimit -S -n'],
{ timeout: 10_000 },
(error, stdout, stderr) => (error ? reject(new Error(`${error.message}\n${stderr}`)) : resolve(stdout.trim())),
)
})
expect(seenByEngine).not.toBe('256')
expect(seenByEngine === 'unlimited' || Number(seenByEngine) >= Math.min(MIN_OPEN_FILES, 4096)).toBe(true)
})
describe('an unreadable workspace', () => {
// Under /bin/sh, with the engine replaced by printf so its run leaves a marker.
async function launchInto(dir: string): Promise<{ code: number; stdout: string; stderr: string }> {
const [, , paneScript] = buildEngineLaunchArgv('claude', { cwd: dir }, '/bin/sh')
const { execFile } = await import('node:child_process')
return await new Promise((resolve) => {
execFile(
'/bin/sh',
['-c', paneScript, 'harness-engine', dir, '/usr/bin/printf', 'ENGINE_RAN\n'],
{ timeout: 10_000 },
(error, stdout, stderr) => {
const code = error && typeof (error as { code?: unknown }).code === 'number'
? (error as unknown as { code: number }).code
: 0
resolve({ code, stdout, stderr })
},
)
})
}
// root reads everything, so the guard has nothing to catch there.
const notRoot = process.getuid?.() !== 0
it.skipIf(!notRoot)('says why and stops instead of letting the engine fail on its first read', async () => {
// Enterable but not readable: what macOS privacy settings (TCC) produce for a folder the
// responsible app was not granted — `cd` works, the first readdir does not.
const dir = mkdtempSync(join(tmpdir(), 'harness-unreadable-'))
try {
chmodSync(dir, 0o300)
const result = await launchInto(dir)
expect(result.code).toBe(1)
expect(result.stdout).not.toContain('ENGINE_RAN')
expect(result.stderr).toContain(`harness: cannot read ${dir}`)
} finally {
chmodSync(dir, 0o700)
rmSync(dir, { recursive: true, force: true })
}
})
it('launches the engine when the workspace is readable', async () => {
const dir = mkdtempSync(join(tmpdir(), 'harness-readable-'))
try {
const result = await launchInto(dir)
expect(result.code).toBe(0)
expect(result.stdout).toContain('ENGINE_RAN')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
// Both hints are pushed through a real /bin/sh, whatever platform runs the suite: the quoting
// of the macOS text (a backtick-free command, `›`, `—`) is what a Linux runner would otherwise
// never exercise.
async function guardOutput(platform: NodeJS.Platform, dir: string): Promise<string> {
const { execFile } = await import('node:child_process')
return await new Promise((resolve) => {
execFile(
'/bin/sh',
['-c', `cd -- "$1" || exit 9\n${unreadableCwdGuard(platform)}echo ENGINE_RAN`, 'guard', dir],
{ timeout: 10_000 },
(_error, stdout, stderr) => resolve(stdout + stderr),
)
})
}
it.skipIf(!notRoot)('points at the privacy settings on macOS and at permissions elsewhere', async () => {
const dir = mkdtempSync(join(tmpdir(), 'harness-unreadable-hint-'))
try {
chmodSync(dir, 0o300)
const darwin = await guardOutput('darwin', dir)
expect(darwin).toContain('Full Disk Access')
expect(darwin).toContain('tmux kill-server')
expect(darwin).toContain('Privacy & Security')
expect(darwin).not.toContain('ENGINE_RAN')
const linux = await guardOutput('linux', dir)
expect(linux).toContain('permissions')
expect(linux).not.toContain('Privacy & Security')
expect(linux).not.toContain('ENGINE_RAN')
} finally {
chmodSync(dir, 0o700)
rmSync(dir, { recursive: true, force: true })
}
})
it('keeps the action inside what a relayed pane summary can show', () => {
// First hint line: what to do. It has to survive next to a folder path in a ~180-char summary.
const firstHint = unreadableCwdGuard('darwin').match(/'(harness: on macOS[^']*)'/)?.[1] ?? ''
expect(firstHint.length).toBeGreaterThan(0)
expect(firstHint.length).toBeLessThanOrEqual(120)
})
})
it('a DSH agent gets Harness\'s Node at the end of a PATH that has none; a plain launch is unchanged', () => {
const argv = buildEngineLaunchArgv('claude', { harnessNode: true }, '/bin/zsh', '/opt/harness runtime/bin/node', undefined, NO_TMUX)
// On this branch every pane script opens with the open-files raise, the engine-in-a-shell
// wrapper and the grid prelude; the Node line lands after them, and a launch without
// `harnessNode` is exactly the baseline above.
expect(argv[2]).toBe(`${RAISE_OPEN_FILES_SH}${FALLBACK('claude', '/bin/zsh')}${GRID_PRELUDE}${harnessNodePrelude('/opt/harness runtime/bin/node')}harness_engine "$@"`)
expect(harnessNodePrelude('/opt/harness runtime/bin/node')).toBe(
'if ! command -v node >/dev/null 2>&1; then PATH="${PATH:+$PATH:}"\'/opt/harness runtime/bin\'; export PATH; fi\n')
expect(buildEngineLaunchArgv('claude', { harnessNode: false }, '/bin/zsh', undefined, undefined, NO_TMUX)[2]).toBe(`${RAISE_OPEN_FILES_SH}${FALLBACK('claude', '/bin/zsh')}${GRID_PRELUDE}harness_engine "$@"`)
})
it('the DSH prelude, run by a real shell, reaches the engine\'s PATH only when node is missing', () => {
const home = mkdtempSync(join(tmpdir(), 'harness-node-prelude-'))
try {
const runtimeBin = join(home, 'runtime', 'bin')
mkdirSync(runtimeBin, { recursive: true })
writeFileSync(join(runtimeBin, 'node'), '#!/bin/sh\n', { mode: 0o755 })
const run = (path: string) => {
const argv = buildEngineLaunchArgv('claude', { harnessNode: true }, '/bin/sh', join(runtimeBin, 'node'))
const script = argv[2]
return execFileSync('/bin/sh', ['-c', script, 'harness-engine', '/bin/sh', '-c', 'echo "$PATH"; command -v node'], {
env: { HOME: home, PATH: path }, encoding: 'utf8',
}).trim().split('\n')
}
expect(run('/usr/bin:/bin')).toEqual([`/usr/bin:/bin:${runtimeBin}`, join(runtimeBin, 'node')])
const own = join(home, 'own')
mkdirSync(own)
writeFileSync(join(own, 'node'), '#!/bin/sh\n', { mode: 0o755 })
expect(run(`${own}:/usr/bin:/bin`)).toEqual([`${own}:/usr/bin:/bin`, join(own, 'node')])
} finally {
rmSync(home, { recursive: true, force: true })
}
})
it('falls back to direct execution when no absolute shell is available', () => {
expect(buildEngineLaunchArgv('claude', {}, '')).toEqual([engineBin('claude')])
expect(buildEngineLaunchArgv('claude', {}, 'zsh')).toEqual([engineBin('claude')])
})
it('appends the confirmed flag for engines with a known bypass flag', () => {
expect(buildEngineCommandArgv('claude', { bypassPermission: true }))
.toEqual([engineBin('claude'), '--permission-mode', 'auto'])
expect(buildEngineCommandArgv('codex', { bypassPermission: true }))
.toEqual([engineBin('codex'), '--approve-for-me'])
expect(buildEngineCommandArgv('cursor', { bypassPermission: true }))
.toEqual([engineBin('cursor'), '--force'])
expect(buildEngineCommandArgv('opencode', { bypassPermission: true }))
.toEqual([engineBin('opencode'), '--auto'])
})
it('launches each permission mode with its own flags, and a mode outranks bypassPermission', () => {
expect(buildEngineCommandArgv('claude', { permissionMode: 'plan' }))
.toEqual([engineBin('claude'), '--permission-mode', 'plan'])
expect(buildEngineCommandArgv('claude', { permissionMode: 'acceptEdits', bypassPermission: true }))
.toEqual([engineBin('claude'), '--permission-mode', 'acceptEdits'])
expect(buildEngineCommandArgv('claude', { permissionMode: 'ask', bypassPermission: true }))
.toEqual([engineBin('claude')])
expect(buildEngineCommandArgv('claude', { permissionMode: 'full' }))
.toEqual([engineBin('claude'), '--dangerously-skip-permissions'])
expect(buildEngineCommandArgv('codex', { permissionMode: 'readOnly' }))
.toEqual([engineBin('codex'), '--sandbox', 'read-only'])
expect(buildEngineCommandArgv('codex', { permissionMode: 'full' }))
.toEqual([engineBin('codex'), '--dangerously-bypass-approvals-and-sandbox'])
// A mode the engine lacks falls back to the yes/no.
expect(buildEngineCommandArgv('opencode', { permissionMode: 'plan', bypassPermission: true }))
.toEqual([engineBin('opencode'), '--auto'])
expect(buildEngineCommandArgv('pi', { permissionMode: 'auto' })).toEqual([engineBin('pi')])
})
it('makes auto the same flags as bypassPermission, for every engine with modes', () => {
for (const [engine, modes] of Object.entries(PERMISSION_MODES)) {
expect(modes?.auto, engine).toEqual(BYPASS_PERMISSION_FLAGS[engine as keyof typeof BYPASS_PERMISSION_FLAGS])
expect(modes?.ask, engine).toEqual([])
}
})
it('is a no-op for engines with no confirmed flag, even when bypass is requested', () => {
expect(buildEngineCommandArgv('pi', { bypassPermission: true })).toEqual([engineBin('pi')])
expect(buildEngineCommandArgv('hermes', { bypassPermission: true })).toEqual([engineBin('hermes')])
})
it('has an entry (possibly null) for every known engine — no engine silently falls through', () => {
for (const engine of ENGINES) {
expect(Object.prototype.hasOwnProperty.call(BYPASS_PERMISSION_FLAGS, engine)).toBe(true)
}
})
it('appends a flag-style resume argument after the binary', () => {
expect(buildEngineCommandArgv('claude', { resumeSessionId: 'abc-123' }))
.toEqual([engineBin('claude'), '--resume', 'abc-123'])
expect(buildEngineCommandArgv('opencode', { resumeSessionId: 'ses_1' }))
.toEqual([engineBin('opencode'), '--session', 'ses_1'])
})
it('puts a subcommand-style resume FIRST, ahead of any other flag', () => {
expect(buildEngineCommandArgv('codex', { resumeSessionId: 'abc-123' }))
.toEqual([engineBin('codex'), 'resume', 'abc-123'])
expect(buildEngineCommandArgv('codex', { resumeSessionId: 'abc-123', bypassPermission: true }))
.toEqual([engineBin('codex'), 'resume', 'abc-123', '--approve-for-me'])
expect(buildEngineCommandArgv('amp', { resumeSessionId: 'T-1' }))
.toEqual([engineBin('amp'), 'threads', 'continue', 'T-1'])
})
it('still applies bypassPermission with no resume requested (regression)', () => {
expect(buildEngineCommandArgv('claude', { bypassPermission: true }))
.toEqual([engineBin('claude'), '--permission-mode', 'auto'])
})
it('is a no-op when resumeSessionId is set but the engine has no known launch resume flag', () => {
expect(buildEngineCommandArgv('devin', { resumeSessionId: 'abc-123' })).toEqual([engineBin('devin')])
})
it('has a launch resume flag for every engine RESUME_ARGS also covers, plus claude/codex', () => {
for (const engine of ['claude', 'codex', 'cursor', 'opencode', 'kilo', 'pi', 'hermes', 'commandcode',
'muse', 'amp', 'grok', 'agy', 'copilot'] as const) {
expect(LAUNCH_RESUME_FLAG[engine]?.length).toBeGreaterThan(0)
}
expect(LAUNCH_RESUME_FLAG.devin).toBeUndefined()
})
})
describe('a first prompt on launch', () => {
const PROMPT = 'Start a local model on this machine'
it('hands opencode the text through its TUI flag', () => {
expect(buildEngineCommandArgv('opencode', { firstPrompt: PROMPT }))
.toEqual([engineBin('opencode'), '--prompt', PROMPT])
})
it('hands claude and codex the text positionally', () => {
expect(buildEngineCommandArgv('claude', { firstPrompt: PROMPT })).toEqual([engineBin('claude'), PROMPT])
expect(buildEngineCommandArgv('codex', { firstPrompt: PROMPT })).toEqual([engineBin('codex'), PROMPT])
})
it('puts the text LAST, after every flag, so a positional is never read as an option value', () => {
expect(buildEngineCommandArgv('claude', { firstPrompt: PROMPT, bypassPermission: true, extraArgs: ['--allowedTools=WebSearch'] }))
.toEqual([engineBin('claude'), '--permission-mode', 'auto', '--allowedTools=WebSearch', PROMPT])
expect(buildEngineCommandArgv('opencode', { firstPrompt: PROMPT, bypassPermission: true, extraArgs: ['-m', 'local/qwen'] }))
.toEqual([engineBin('opencode'), '--auto', '-m', 'local/qwen', '--prompt', PROMPT])
})
it('refuses an engine with no documented mechanism, naming the engine, rather than dropping the text', () => {
expect(supportsFirstPrompt('cursor')).toBe(false)
expect(() => firstPromptArgs('cursor', PROMPT)).toThrow(FirstPromptUnsupportedError)
let refusal: unknown
try { buildEngineCommandArgv('cursor', { firstPrompt: PROMPT }) } catch (error) { refusal = error }
expect(refusal).toBeInstanceOf(FirstPromptUnsupportedError)
expect((refusal as FirstPromptUnsupportedError).code).toBe('PROMPT_UNSUPPORTED')
expect((refusal as FirstPromptUnsupportedError).message).toContain('cursor')
})
it('leaves the argv unchanged with no prompt, or an empty one', () => {
expect(buildEngineCommandArgv('opencode', {})).toEqual([engineBin('opencode')])
expect(buildEngineCommandArgv('opencode', { firstPrompt: '' })).toEqual([engineBin('opencode')])
expect(buildEngineCommandArgv('cursor', { firstPrompt: '' })).toEqual([engineBin('cursor')])
})
it('reaches the engine as ONE positional argument through the pane shell, however it is spelled', async () => {
// The pane script execs "$@": the prompt must arrive as a single argv entry, spaces, quotes and
// all, rather than being re-split by the shell.
const spelled = `Start a local model on "this" machine; it's $HOME`
const [, , paneScript, marker, ...command] = buildEngineLaunchArgv('claude', { firstPrompt: spelled }, '/bin/sh')
expect(marker).toBe('harness-engine')
expect(command).toEqual([engineBin('claude'), spelled])
const { execFile } = await import('node:child_process')
const seen = await new Promise<string>((resolve, reject) => {
execFile('/bin/sh', ['-c', paneScript, 'harness-engine', '/bin/sh', '-c', 'printf "%s" "$1"', 'engine', spelled],
{ timeout: 10_000 }, (error, stdout) => (error ? reject(error) : resolve(stdout)))
})
expect(seen).toBe(spelled)
})
it('has an entry (possibly null) for every known engine — no engine silently falls through', () => {
for (const engine of ENGINES) {
expect(Object.prototype.hasOwnProperty.call(FIRST_PROMPT_ARGS, engine)).toBe(true)
}
expect(supportsFirstPrompt('opencode')).toBe(true)
expect(supportsFirstPrompt('claude')).toBe(true)
expect(supportsFirstPrompt('codex')).toBe(true)
})
})
describe('opening as a named agent', () => {
it('hands opencode the name through --agent, in the extraArgs slot a relaunch also uses', () => {
expect(namedAgentArgs('opencode', 'harness-compute')).toEqual(['--agent', 'harness-compute'])
expect(buildEngineCommandArgv('opencode', { bypassPermission: true, extraArgs: ['-m', 'local/qwen', ...namedAgentArgs('opencode', 'harness-compute')] }))
.toEqual([engineBin('opencode'), '--auto', '-m', 'local/qwen', '--agent', 'harness-compute'])
// Resume keeps it too — the flag rides `extraArgs`, which every relaunch rebuilds from the row.
expect(buildEngineCommandArgv('opencode', { resumeSessionId: 'ses_1', extraArgs: namedAgentArgs('opencode', 'harness-compute') }))
.toEqual([engineBin('opencode'), '--session', 'ses_1', '--agent', 'harness-compute'])
})
it('refuses every other engine, naming it, rather than dropping the name', () => {
for (const engine of ENGINES) {
if (engine === 'opencode') continue
expect(supportsNamedAgent(engine)).toBe(false)
let refusal: unknown
try { namedAgentArgs(engine, 'harness-compute') } catch (error) { refusal = error }
expect(refusal).toBeInstanceOf(NamedAgentUnsupportedError)
expect((refusal as NamedAgentUnsupportedError).code).toBe('AGENT_UNSUPPORTED')
expect((refusal as NamedAgentUnsupportedError).message).toContain(engine)
}
})
it('has an entry (possibly null) for every known engine — no engine silently falls through', () => {
for (const engine of ENGINES) {
expect(Object.prototype.hasOwnProperty.call(NAMED_AGENT_ARGS, engine)).toBe(true)
}
expect(supportsNamedAgent('opencode')).toBe(true)
})
it('accepts an identifier and nothing that could be a path or prose', () => {
for (const ok of ['harness-compute', 'build', 'A_b-1', 'x'.repeat(64)]) expect(AGENT_NAME_RE.test(ok)).toBe(true)
for (const bad of ['', ' harness-compute', 'local model', '../etc', 'a/b', 'name.md', 'x'.repeat(65), 'nämn']) {
expect(AGENT_NAME_RE.test(bad)).toBe(false)
}
})
})
const dirs: string[] = []
const originalProbePath = process.env.HARNESS_ENGINE_TEST_PATH
afterEach(() => {
if (originalProbePath === undefined) delete process.env.HARNESS_ENGINE_TEST_PATH
else process.env.HARNESS_ENGINE_TEST_PATH = originalProbePath
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})
function executable(dir: string, name: string): string {
const path = join(dir, name)
writeFileSync(path, '#!/bin/sh\nexit 0\n')
chmodSync(path, 0o700)
return path
}
function bashProbeShell(): string {
const dir = mkdtempSync(join(tmpdir(), 'harness-engine-shell-'))
dirs.push(dir)
const shell = join(dir, 'bash')
writeFileSync(shell, `#!/bin/sh
[ "$1" = '-ic' ] || exit 97
shift
script="$1"
shift
PATH="$HARNESS_ENGINE_TEST_PATH"
export PATH
exec /bin/sh -c "$script" "$@"
`)
chmodSync(shell, 0o700)
return shell
}
/** The real zsh, if this machine has one. macOS always does; a Linux CI box may not. */
const ZSH = ['/bin/zsh', '/usr/bin/zsh'].find((path) => existsSync(path)) ?? null
/**
* A probe shell that hands the script to the REAL zsh. `bashProbeShell` runs everything through
* `/bin/sh` and so cannot see a zsh-only parse rule — and zsh is what macOS defaults $SHELL to,
* i.e. the shell the capability probe actually runs under for most users. Plain `-c` rather than
* zsh's interactive form keeps the case hermetic: no developer's rc file gets a say in the result.
*/
function zshProbeShell(): string {
const dir = mkdtempSync(join(tmpdir(), 'harness-engine-zsh-shell-'))
dirs.push(dir)
// Named `zsh` so interactiveEngineShell hands it zsh's own `-lic`, exactly as it would $SHELL.
const shell = join(dir, 'zsh')
writeFileSync(shell, `#!/bin/sh
[ "$1" = '-lic' ] || exit 97
shift
script="$1"
shift
PATH="$HARNESS_ENGINE_TEST_PATH"
export PATH
exec ${ZSH} -c "$script" "$@"
`)
chmodSync(shell, 0o700)
return shell
}
describe('commandAvailableInInteractiveShell', () => {
it('uses the same bash interactive PATH that launches a new engine', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-bin-'))
dirs.push(binDir)
executable(binDir, 'kilo')
process.env.HARNESS_ENGINE_TEST_PATH = binDir
await expect(commandAvailableInInteractiveShell('kilo', bashProbeShell())).resolves.toBe(true)
await expect(commandAvailableInInteractiveShell('missing-engine', bashProbeShell())).resolves.toBe(false)
})
it('recognizes an installed vendor path before the shell profile has it on PATH', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-vendor-bin-'))
dirs.push(binDir)
const installed = executable(binDir, 'cursor-agent')
process.env.HARNESS_ENGINE_TEST_PATH = '/usr/bin:/bin'
const recipe: EngineInstallRecipe = {
command: 'false',
source: 'test fixture',
executable: { names: ['cursor-agent'], absolutePaths: [installed] },
}
await expect(
commandAvailableInInteractiveShell('cursor-agent', bashProbeShell(), recipe),
).resolves.toBe(true)
})
})
/** A fake engine whose `--help` prints exactly `help`, and nothing else. */
function fakeEngine(dir: string, name: string, help: string): void {
writeFileSync(join(dir, name), `#!/bin/sh\nif [ "$1" = "--help" ]; then printf "%s\\n" '${help}'; exit 0; fi\nexit 2\n`)
chmodSync(join(dir, name), 0o700)
}
describe('the permission gates', () => {
/** A fake opencode on the probe shell's PATH, and the shell that will find it. */
function withFakeOpencode(help: string): string {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-gate-'))
dirs.push(binDir)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
fakeEngine(binDir, 'opencode', help)
return bashProbeShell()
}
it('refuses a create whose engine does not take the flag, naming the engine and the flag', async () => {
const shell = withFakeOpencode('--auto-update')
const refusal = await refusePermissionFlagIfUnsupported('opencode', { permissionMode: 'auto' }, shell)
expect(refusal?.error).toBe('CODEX_CLI_TOO_OLD')
expect(refusal?.detail).toContain('opencode')
expect(refusal?.detail).toContain('--auto')
})
it('lets a create through when the flag is there, and when there is no flag to check', async () => {
const shell = withFakeOpencode('--auto')
await expect(refusePermissionFlagIfUnsupported('opencode', { permissionMode: 'auto' }, shell)).resolves.toBeNull()
await expect(refusePermissionFlagIfUnsupported('opencode', { permissionMode: 'ask' }, shell)).resolves.toBeNull()
})
it('drops the flag for a relaunch instead of refusing it, leaving the launch in Ask', async () => {
const shell = withFakeOpencode('--auto-update')
const dropped = await dropPermissionFlagIfUnsupported('opencode', { permissionMode: 'auto', bypassPermission: true }, shell)
expect(dropped).toEqual({ choice: { permissionMode: 'ask', bypassPermission: false }, droppedFlag: '--auto' })
})
it('leaves a relaunch alone when the flag is supported', async () => {
const shell = withFakeOpencode('--auto')
const kept = await dropPermissionFlagIfUnsupported('opencode', { permissionMode: 'auto', bypassPermission: true }, shell)
expect(kept).toEqual({ choice: { permissionMode: 'auto', bypassPermission: true }, droppedFlag: null })
})
})
describe('permissionFlagToVerify', () => {
it('names the single token a mode adds', () => {
expect(permissionFlagToVerify('opencode', { permissionMode: 'auto' })).toBe('--auto')
expect(permissionFlagToVerify('cursor', { permissionMode: 'auto' })).toBe('--force')
expect(permissionFlagToVerify('codex', { permissionMode: 'auto' })).toBe('--approve-for-me')
})
it('reads the bypass table when no mode was recorded', () => {
expect(permissionFlagToVerify('opencode', { bypassPermission: true })).toBe('--auto')
expect(permissionFlagToVerify('opencode', { bypassPermission: false })).toBeNull()
})
// The documented limit: help never prints the pair, so only the token can be asked about, and a
// build that lists `--permission-mode` while having dropped the `auto` VALUE is not caught here.
it('takes the flag token of a pair, never its value', () => {
expect(permissionModeFlags('claude', 'auto')).toEqual(['--permission-mode', 'auto'])
expect(permissionFlagToVerify('claude', { permissionMode: 'auto' })).toBe('--permission-mode')
expect(permissionFlagToVerify('codex', { permissionMode: 'readOnly' })).toBe('--sandbox')
})
it('has nothing to verify for Ask, an engine with no table, or a terminal', () => {
expect(permissionFlagToVerify('claude', { permissionMode: 'ask' })).toBeNull()
expect(permissionFlagToVerify('muse', { bypassPermission: true })).toBeNull()
expect(permissionFlagToVerify('terminal', { bypassPermission: true })).toBeNull()
})
})
describe('commandSupportsFlagInInteractiveShell', () => {
it('distinguishes an older CLI help surface from a supported flag', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-capability-'))
dirs.push(binDir)
const codex = join(binDir, 'codex')
writeFileSync(
codex,
'#!/bin/sh\nif [ "$1" = "--help" ]; then printf "%s\\n" "--sandbox"; exit 0; fi\nexit 2\n',
)
chmodSync(codex, 0o700)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', bashProbeShell()),
).resolves.toBe('unsupported')
writeFileSync(
codex,
'#!/bin/sh\nif [ "$1" = "--help" ]; then printf "%s\\n" "--approve-for-me"; exit 0; fi\nexit 2\n',
)
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', bashProbeShell()),
).resolves.toBe('supported')
})
// openharness#285: opencode 1.14.51 has no `--auto`, prints its help and exits, and the wrapper
// hands the pane to a shell. A substring match reads `--auto-update` in a newer help as support
// for `--auto` and launches that very pane.
it('does not read a longer flag as the one it was asked about', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-boundary-'))
dirs.push(binDir)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
fakeEngine(binDir, 'opencode', '--auto-update keep opencode current')
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', bashProbeShell()))
.resolves.toBe('unsupported')
fakeEngine(binDir, 'opencode', '--auto approve automatically')
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', bashProbeShell()))
.resolves.toBe('supported')
})
it('answers a flag that ends the help text', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-tail-'))
dirs.push(binDir)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
fakeEngine(binDir, 'cursor-agent', '--force')
await expect(commandSupportsFlagInInteractiveShell('cursor-agent', '--force', bashProbeShell()))
.resolves.toBe('supported')
})
// Every relaunch asks, and a restore asks once per agent. Only the working answer is remembered:
// the cure for the other two is to change the engine on disk, and a remembered refusal would
// outlive the upgrade that fixed it — which is how openharness#285's reporter resolved theirs.
it('remembers that a flag IS supported, and keeps asking when it is not', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-cache-'))
dirs.push(binDir)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
const shell = bashProbeShell()
fakeEngine(binDir, 'opencode', '--auto')
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', shell)).resolves.toBe('supported')
// Downgraded underneath us; the cached yes stands until the cache is reset.
fakeEngine(binDir, 'opencode', '--auto-update')
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', shell)).resolves.toBe('supported')
resetCommandFlagSupportCache()
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', shell)).resolves.toBe('unsupported')
// A refusal is never remembered, so the upgrade is seen at once.
fakeEngine(binDir, 'opencode', '--auto')
await expect(commandSupportsFlagInInteractiveShell('opencode', '--auto', shell)).resolves.toBe('supported')
})
// A shell's own failure exits 1 — as zsh's read-only `status` did — and must not read as a missing
// flag: only the probe's own "not in the help" exit refuses an engine.
it('does not read a shell failure as a missing flag', async () => {
const shellDir = mkdtempSync(join(tmpdir(), 'harness-engine-shell-'))
dirs.push(shellDir)
const shell = join(shellDir, 'bash')
writeFileSync(shell, '#!/bin/sh\nexit 1\n')
chmodSync(shell, 0o700)
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', shell),
).resolves.toBe('unknown')
})
it('does not reject a CLI when its help command cannot be inspected', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-capability-'))
dirs.push(binDir)
const codex = join(binDir, 'codex')
writeFileSync(codex, '#!/bin/sh\nexit 2\n')
chmodSync(codex, 0o700)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', bashProbeShell()),
).resolves.toBe('unknown')
})
// zsh makes `status` a read-only alias of `$?`, so a bare `status=$?` in the probe script is a
// fatal error there: the shell dies with exit 1, which this function reads as `unsupported`.
// macOS defaults $SHELL to zsh, so that one name turned every Codex Auto launch on a Mac into
// "your installed Codex CLI does not support --approve-for-me" whatever version was installed.
it.skipIf(!ZSH)('answers from zsh too, where `status` is a read-only parameter', async () => {
const binDir = mkdtempSync(join(tmpdir(), 'harness-engine-capability-zsh-'))
dirs.push(binDir)
const codex = join(binDir, 'codex')
writeFileSync(
codex,
'#!/bin/sh\nif [ "$1" = "--help" ]; then printf "%s\\n" "--approve-for-me"; exit 0; fi\nexit 2\n',
)
chmodSync(codex, 0o700)
process.env.HARNESS_ENGINE_TEST_PATH = binDir
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', zshProbeShell()),
).resolves.toBe('supported')
writeFileSync(
codex,
'#!/bin/sh\nif [ "$1" = "--help" ]; then printf "%s\\n" "--sandbox"; exit 0; fi\nexit 2\n',
)
await expect(
commandSupportsFlagInInteractiveShell('codex', '--approve-for-me', zshProbeShell()),
).resolves.toBe('unsupported')
})
})
describe('buildEngineLaunchArgv — the grid the pane finds', () => {
/** A runnable `grid` at `dir/grid`. Which one the pane's shell resolves is the assertion. */
function gridAt(dir: string): string {
mkdirSync(dir, { recursive: true })
return executable(dir, 'grid')
}
it('puts the resolved grid first on PATH and turns its update check off, before the engine', () => {
const managed = '/opt/harness/runtime/grid-0.3.47-darwin-arm64/grid'
const script = buildEngineLaunchArgv('claude', {}, '/bin/zsh', undefined, managed)[2]
expect(script).toContain(`PATH='/opt/harness/runtime/grid-0.3.47-darwin-arm64'"\${PATH:+:$PATH}"\nexport PATH\n`)
expect(script).toContain('GRID_NO_UPDATE_CHECK=1\nexport GRID_NO_UPDATE_CHECK\n')
expect(script.indexOf('export GRID_NO_UPDATE_CHECK')).toBeLessThan(script.indexOf('harness_engine "$@"'))
})
it('leaves PATH alone when grid is only a name on it, but still turns the update check off', () => {
const script = buildEngineLaunchArgv('claude', {}, '/bin/zsh', undefined, 'grid')[2]
expect(script).not.toContain('export PATH')
expect(script).toContain('GRID_NO_UPDATE_CHECK=1')
})
/** What the engine's own `command -v grid` answers, and what it sees in GRID_NO_UPDATE_CHECK.
*
* The pane is the user's login shell, and a `.zshrc` that puts `~/.local/bin` first is ordinary —
* which is where grid's own installer (uv, on a Mac) leaves a `grid`. The prelude runs AFTER the
* startup files, so that one cannot get ahead of the grid the daemon resolved. `bashProbeShell()`
* plays the startup file: it resets PATH to HARNESS_ENGINE_TEST_PATH before the script runs. */
async function gridSeenByEngine(gridBinary: string): Promise<{ which: string; updateCheck: string }> {
const [shell, flag, script] = buildEngineLaunchArgv('claude', {}, bashProbeShell(), undefined, gridBinary)
const { execFile } = await import('node:child_process')
const out = await new Promise<string>((resolve, reject) => {
execFile(
shell,
[flag, script, 'harness-engine', '/bin/sh', '-c', 'command -v grid; printf "%s" "$GRID_NO_UPDATE_CHECK"'],
{ timeout: 10_000 },
(error, stdout, stderr) => (error ? reject(new Error(`${error.message}\n${stderr}`)) : resolve(stdout)),
)
})
const [which, updateCheck] = out.split('\n')
return { which, updateCheck }
}
it('wins over a grid the shell\'s own startup files put first on PATH', async () => {
const root = mkdtempSync(join(tmpdir(), 'harness-pane-grid-'))
dirs.push(root)
gridAt(join(root, 'users-own'))
const managed = gridAt(join(root, 'runtime', 'grid-0.3.47-darwin-arm64'))
process.env.HARNESS_ENGINE_TEST_PATH = `${join(root, 'users-own')}:/usr/bin:/bin`
await expect(gridSeenByEngine(managed)).resolves.toEqual({ which: managed, updateCheck: '1' })
})
it('leaves the user\'s own grid in charge when the daemon resolved nothing better', async () => {
const root = mkdtempSync(join(tmpdir(), 'harness-pane-grid-'))
dirs.push(root)
const own = gridAt(join(root, 'users-own'))
process.env.HARNESS_ENGINE_TEST_PATH = `${join(root, 'users-own')}:/usr/bin:/bin`
await expect(gridSeenByEngine('grid')).resolves.toEqual({ which: own, updateCheck: '1' })
})
})
describe('buildEngineLaunchArgv with installFirst', () => {
// Everything here is about ONE rule: the engine must not be exec'd after an install that failed.
// Doing so reproduces the `command not found` this feature exists to replace, with a screenful of
// installer output above it to bury the cause.
const script = (install: string): string =>
buildEngineLaunchArgv('opencode', { installFirst: install }, '/bin/zsh')[2]
it('leaves the plain launch alone when nothing has to be installed', () => {
expect(buildEngineLaunchArgv('opencode', {}, '/bin/zsh', undefined, undefined, NO_TMUX)[2]).toBe(`${RAISE_OPEN_FILES_SH}${FALLBACK('opencode', '/bin/zsh')}${GRID_PRELUDE}harness_engine "$@"`)
})
it('keeps the engine argv positional, so the shell never re-parses a path or a flag', () => {
const argv = buildEngineLaunchArgv('opencode', {
installFirst: 'npm install -g opencode-ai',
bypassPermission: true,
}, '/bin/zsh')
expect(argv.slice(0, 2)).toEqual(['/bin/zsh', '-lic'])
expect(argv.slice(3)).toEqual(['harness-engine', ...buildEngineCommandArgv('opencode', { bypassPermission: true })])
expect(argv[2]).toContain('harness_engine "$@"')
})
it('runs the engine only when the install succeeded', async () => {
await expect(runPaneScript(script('true'))).resolves.toMatchObject({ code: 0, ranEngine: true })
})
it('does not run the engine when the install returns a failure', async () => {
const result = await runPaneScript(script('false'))
expect(result.ranEngine).toBe(false)
expect(result.code).toBe(1)
expect(result.stdout).toContain('the install failed')
})
it('does not run the engine when the install command is not there at all', async () => {
const result = await runPaneScript(script('harness-no-such-installer --please'))
expect(result.ranEngine).toBe(false)
expect(result.code).toBe(1)
})
it('prints the command before running it, so a long install is not a hung pane', () => {
expect(script('npm install -g opencode-ai')).toContain('npm install -g opencode-ai')
})
it("survives an install line carrying a quote, rather than ending the shell's string", async () => {
// `curl … | bash` is already in the table; a quoted argument is the next shape to arrive, and an
// install line is a constant in our own source — so the failure mode is a broken pane, not an
// injection. It still must not break.
const result = await runPaneScript(script(`sh -c 'exit 3'`))
expect(result.ranEngine).toBe(false)
expect(result.code).toBe(1)
})
})
describe('buildEngineLaunchArgv with installIfMissing', () => {
const recipe = (
command: string,
executable: EngineInstallRecipe['executable'] = { names: ['harness-no-such-engine'] },
): EngineInstallRecipe => ({ command, source: 'test fixture', executable })
const script = (install: EngineInstallRecipe, runtimeNode?: string): string =>
buildEngineLaunchArgv('opencode', { installIfMissing: install }, '/bin/zsh', runtimeNode)[2]
it('execs an installed engine without running the installer', async () => {
await expect(runPaneScript(script(recipe('false')))).resolves.toMatchObject({ code: 0, ranEngine: true })
})
it('runs the installer inside the pane when the engine is absent', async () => {
const result = await runPaneScript(script(recipe('false')), 'harness-no-such-engine')
expect(result.ranEngine).toBe(false)
expect(result.code).toBe(1)
expect(result.stdout).toContain('engine is missing')
})
it('execs a vendor path after install even when the current PATH did not reload', async () => {
const dir = mkdtempSync(join(tmpdir(), 'harness-engine-installed-'))
dirs.push(dir)
const source = join(dir, 'source-engine')
const installed = join(dir, 'new-engine')
writeFileSync(source, '#!/bin/sh\n/usr/bin/printf "%s" "$1"\n')
chmodSync(source, 0o700)
const install = `cp ${JSON.stringify(source)} ${JSON.stringify(installed)}`
const result = await runPaneScript(
script(recipe(install, { names: ['harness-no-such-engine'], absolutePaths: [installed] })),
'harness-no-such-engine',
)
expect(result).toMatchObject({ code: 0, ranEngine: true })
})
it('reports a successful install that did not provide an executable', async () => {
const result = await runPaneScript(script(recipe('true')), 'harness-no-such-engine')
expect(result).toMatchObject({ code: 1, ranEngine: false })
expect(result.stdout).toContain('install completed, but its executable could not be found')
})
it('enables npm from the managed Node runtime when the pane PATH has no npm', async () => {
const runtimeBin = mkdtempSync(join(tmpdir(), 'harness-managed-node-bin-'))
const emptyPath = mkdtempSync(join(tmpdir(), 'harness-empty-path-'))
dirs.push(runtimeBin, emptyPath)
const runtimeNode = executable(runtimeBin, 'node')
const engineSource = join(runtimeBin, 'engine-source')
writeFileSync(engineSource, '#!/bin/sh\n/usr/bin/printf "HARNESS-TEST-ENGINE-RAN\\n"\n')
chmodSync(engineSource, 0o700)
const installed = join(runtimeBin, 'installed-engine')
const npm = join(runtimeBin, 'npm')
writeFileSync(npm, `#!/bin/sh
if [ "$1" = prefix ]; then exit 0; fi
/bin/cp ${JSON.stringify(engineSource)} ${JSON.stringify(installed)}
/bin/chmod 700 ${JSON.stringify(installed)}
`)
chmodSync(npm, 0o700)
const result = await runPaneScript(
script(recipe('npm install -g fixture', {
names: ['harness-no-such-engine'],
absolutePaths: [installed],
npmGlobal: true,
}), runtimeNode),
'harness-no-such-engine',
{ PATH: emptyPath },
)
expect(result).toMatchObject({ code: 0, ranEngine: true })
expect(result.stdout).toContain('enabling Harness managed Node.js/npm')
})
it('does not add the npm bootstrap to non-npm installers', () => {
expect(script(recipe('true'))).not.toContain('managed Node.js/npm')
})
})
/** Run one generated pane script under /bin/sh and report what it did. */
async function runPaneScript(
paneScript: string,
command = '/usr/bin/printf',
env: NodeJS.ProcessEnv = process.env,
): Promise<{ code: number; stdout: string; ranEngine: boolean }> {
const { execFile } = await import('node:child_process')
const marker = 'HARNESS-TEST-ENGINE-RAN'
return await new Promise((resolve) => {
execFile(
'/bin/sh',
['-c', paneScript, 'harness-engine', command, `${marker}\n`],
{ timeout: 10_000, env },
(error, stdout) => {
const code = error && typeof (error as { code?: unknown }).code === 'number'
? (error as unknown as { code: number }).code
: 0
resolve({ code, stdout, ranEngine: stdout.includes(marker) })
},
)
})
}