mirror of
https://github.com/stablyai/orca.git
synced 2026-10-26 16:02:42 +00:00
255 lines
9.4 KiB
TypeScript
255 lines
9.4 KiB
TypeScript
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { getRelayShellLaunchConfig } from './pty-shell-launch'
|
|
|
|
const hasBash = process.platform !== 'win32' && spawnSync('bash', ['--version']).status === 0
|
|
const itWithBash = hasBash ? it : it.skip
|
|
|
|
function runInteractiveBashRcfile(rcfile: string, homeDir: string): string {
|
|
const result = spawnSync(
|
|
'bash',
|
|
['-lc', 'bash --noprofile --rcfile "$1" -i 2>&1', 'bash', rcfile],
|
|
{
|
|
input: 'true\nfalse\nexit 0\n',
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
HOME: homeDir,
|
|
TERM: process.env.TERM || 'xterm'
|
|
},
|
|
timeout: 5000
|
|
}
|
|
)
|
|
|
|
expect(result.error).toBeUndefined()
|
|
expect(result.status).toBe(0)
|
|
return result.stdout
|
|
}
|
|
|
|
function expectBashOsc133Lifecycle(output: string): void {
|
|
const oscA = '\x1b]133;A\x07'
|
|
const oscC = '\x1b]133;C\x07'
|
|
const oscD = '\x1b]133;D;'
|
|
const firstPromptMarker = output.indexOf(oscA)
|
|
|
|
expect(firstPromptMarker).toBeGreaterThanOrEqual(0)
|
|
expect(output.slice(0, firstPromptMarker)).not.toContain(oscC)
|
|
expect(output.slice(0, firstPromptMarker)).not.toContain(oscD)
|
|
expect(output).toContain(`${oscD}0\x07${oscA}`)
|
|
expect(output).toContain(`${oscD}1\x07${oscA}`)
|
|
expect(output.split(oscC)).toHaveLength(4)
|
|
expect(output.split(oscD)).toHaveLength(3)
|
|
}
|
|
|
|
function expectZdotdirSourceContext(content: string, fileName: '.zprofile' | '.zshrc' | '.zlogin') {
|
|
expect(content).toContain('export ZDOTDIR="$_orca_home"')
|
|
expect(content).toContain(`source "$_orca_home/${fileName}"`)
|
|
expect(content).toContain('export ZDOTDIR="$_orca_wrapper_zdotdir"')
|
|
}
|
|
|
|
function expectFinalZdotdirRestoreContext(content: string) {
|
|
expect(content).toContain("after Orca's last wrapper file has loaded")
|
|
expect(content).toContain('export ZDOTDIR="$_orca_home"')
|
|
}
|
|
|
|
describe('getRelayShellLaunchConfig', () => {
|
|
let homeDir: string
|
|
|
|
beforeEach(() => {
|
|
homeDir = mkdtempSync(join(tmpdir(), 'relay-shell-launch-'))
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(homeDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'preserves a user ZDOTDIR exported from .zshenv for later startup files',
|
|
() => {
|
|
const config = getRelayShellLaunchConfig('/bin/zsh', {
|
|
HOME: homeDir,
|
|
ORCA_OPENCODE_CONFIG_DIR: '/tmp/orca-opencode-overlay'
|
|
})
|
|
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
|
|
|
|
expect(config.args).toEqual(['-l'])
|
|
expect(config.env.ZDOTDIR).toBe(zshRoot)
|
|
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
|
|
'export ORCA_USER_ZDOTDIR="${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
|
|
)
|
|
expect(readFileSync(join(zshRoot, '.zprofile'), 'utf8')).toContain(
|
|
'_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
|
|
)
|
|
const zprofile = readFileSync(join(zshRoot, '.zprofile'), 'utf8')
|
|
const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
|
|
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
|
|
expect(zshrc).toContain('_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')
|
|
expect(zlogin).toContain('_orca_home="${ORCA_USER_ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"')
|
|
expectZdotdirSourceContext(zprofile, '.zprofile')
|
|
expectZdotdirSourceContext(zshrc, '.zshrc')
|
|
expectZdotdirSourceContext(zlogin, '.zlogin')
|
|
expectFinalZdotdirRestoreContext(zshrc)
|
|
expectFinalZdotdirRestoreContext(zlogin)
|
|
}
|
|
)
|
|
|
|
it('does not pass POSIX login flags to Windows shells', () => {
|
|
expect(
|
|
getRelayShellLaunchConfig('C:\\Windows\\System32\\cmd.exe', { HOME: homeDir }, 'win32')
|
|
).toEqual({
|
|
args: [],
|
|
env: {}
|
|
})
|
|
expect(
|
|
getRelayShellLaunchConfig(
|
|
'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe',
|
|
{ HOME: homeDir },
|
|
'win32'
|
|
)
|
|
).toEqual({
|
|
args: ['-NoLogo'],
|
|
env: {}
|
|
})
|
|
})
|
|
|
|
it('keeps PowerShell Core on POSIX remotes as a login shell', () => {
|
|
expect(getRelayShellLaunchConfig('/usr/bin/pwsh', { HOME: homeDir }, 'linux')).toEqual({
|
|
args: ['-l'],
|
|
env: {}
|
|
})
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')('rewrites stale persistent wrapper files', () => {
|
|
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
|
|
mkdirSync(zshRoot, { recursive: true })
|
|
writeFileSync(join(zshRoot, '.zshenv'), '# stale relay wrapper\n')
|
|
|
|
getRelayShellLaunchConfig('/bin/zsh', {
|
|
HOME: homeDir,
|
|
ORCA_OPENCODE_CONFIG_DIR: '/tmp/orca-opencode-overlay'
|
|
})
|
|
|
|
expect(readFileSync(join(zshRoot, '.zshenv'), 'utf8')).toContain(
|
|
'export ORCA_USER_ZDOTDIR="${ZDOTDIR:-${ORCA_ORIG_ZDOTDIR:-$HOME}}"'
|
|
)
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'wraps zsh when MiMo home must survive shell startup',
|
|
() => {
|
|
const config = getRelayShellLaunchConfig('/bin/zsh', {
|
|
HOME: homeDir,
|
|
ORCA_MIMOCODE_HOME: '/tmp/orca-mimocode-overlay'
|
|
})
|
|
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
|
|
const zshrc = readFileSync(join(zshRoot, '.zshrc'), 'utf8')
|
|
|
|
expect(config.args).toEqual(['-l'])
|
|
expect(config.env.ZDOTDIR).toBe(zshRoot)
|
|
expect(zshrc).toContain(
|
|
'[[ -n "${ORCA_MIMOCODE_HOME:-}" ]] && export MIMOCODE_HOME="${ORCA_MIMOCODE_HOME}"'
|
|
)
|
|
}
|
|
)
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'wraps bash even without overlay env for OSC 133 lifecycle markers',
|
|
() => {
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
|
|
const rcfile = join(homeDir, '.orca-relay', 'shell-ready', 'bash', 'rcfile')
|
|
const bashRc = readFileSync(rcfile, 'utf8')
|
|
|
|
expect(config.args).toEqual(['--rcfile', rcfile])
|
|
expect(config.env).toEqual({})
|
|
expect(bashRc).toContain('printf "\\033]133;D;%s\\007"')
|
|
expect(bashRc).toContain('printf "\\033]133;C\\007"')
|
|
}
|
|
)
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'enables the shell-ready marker for requested zsh startup delivery',
|
|
() => {
|
|
const config = getRelayShellLaunchConfig('/bin/zsh', { HOME: homeDir }, 'linux', {
|
|
emitReadyMarker: true
|
|
})
|
|
const zshRoot = join(homeDir, '.orca-relay', 'shell-ready', 'zsh')
|
|
const zlogin = readFileSync(join(zshRoot, '.zlogin'), 'utf8')
|
|
|
|
expect(config.args).toEqual(['-l'])
|
|
expect(config.env.ZDOTDIR).toBe(zshRoot)
|
|
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
|
|
expect(zlogin).toContain('zle -N zle-line-init __orca_prompt_mark')
|
|
expect(zlogin).toContain('printf "\\033]777;orca-shell-ready\\007"')
|
|
}
|
|
)
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'enables the shell-ready marker for requested bash startup delivery',
|
|
() => {
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir }, 'linux', {
|
|
emitReadyMarker: true
|
|
})
|
|
const bashRc = readFileSync(config.args[1] as string, 'utf8')
|
|
|
|
expect(config.env.ORCA_SHELL_READY_MARKER).toBe('1')
|
|
expect(bashRc).toContain('__orca_append_prompt_command "__orca_prompt_mark"')
|
|
expect(bashRc).toContain('printf "\\033]777;orca-shell-ready\\007"')
|
|
}
|
|
)
|
|
|
|
itWithBash('runs the relay bash wrapper without fake C/D markers before the first prompt', () => {
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
|
|
const output = runInteractiveBashRcfile(config.args[1] as string, homeDir)
|
|
|
|
expectBashOsc133Lifecycle(output)
|
|
})
|
|
|
|
itWithBash('preserves relay bash prompt hooks and DEBUG traps without fake markers', () => {
|
|
writeFileSync(
|
|
join(homeDir, '.bash_profile'),
|
|
[
|
|
'PROMPT_COMMAND=\'AFTER_FIRST_PROMPT=1; printf "PROMPT_HOOK\\n"\'',
|
|
'trap \'if [[ -n "${AFTER_FIRST_PROMPT:-}" ]]; then\n printf "USER_DEBUG_AFTER\\n"\nfi\' DEBUG'
|
|
].join('\n')
|
|
)
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
|
|
const output = runInteractiveBashRcfile(config.args[1] as string, homeDir)
|
|
|
|
expect(output).toContain('PROMPT_HOOK')
|
|
expect(output).toContain('USER_DEBUG_AFTER')
|
|
expectBashOsc133Lifecycle(output)
|
|
})
|
|
|
|
itWithBash('normalizes relay bash array PROMPT_COMMAND hooks', () => {
|
|
writeFileSync(
|
|
join(homeDir, '.bash_profile'),
|
|
'PROMPT_COMMAND=(\'AFTER_ARRAY_PROMPT=1; printf "PROMPT_ARRAY\\n"\')\n'
|
|
)
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
|
|
const output = runInteractiveBashRcfile(config.args[1] as string, homeDir)
|
|
|
|
expect(output).toContain('PROMPT_ARRAY')
|
|
expectBashOsc133Lifecycle(output)
|
|
})
|
|
|
|
// Why: RHEL-family /etc/bashrc prepends "history -a; " to PROMPT_COMMAND
|
|
// outside its BASHRCSOURCED guard (repeated across re-sources), so the value
|
|
// Orca inherits ends in a ";"+whitespace separator. Prepend/append must not
|
|
// splice an empty command (";;") that breaks the prompt with a syntax error.
|
|
itWithBash('normalizes an inherited PROMPT_COMMAND ending in a separator', () => {
|
|
writeFileSync(
|
|
join(homeDir, '.bash_profile'),
|
|
'PROMPT_COMMAND=\'AFTER_SEP_PROMPT=1; printf "PROMPT_SEP\\n"; \'\n'
|
|
)
|
|
const config = getRelayShellLaunchConfig('/bin/bash', { HOME: homeDir })
|
|
const output = runInteractiveBashRcfile(config.args[1] as string, homeDir)
|
|
|
|
expect(output).not.toContain('syntax error')
|
|
expect(output).toContain('PROMPT_SEP')
|
|
expectBashOsc133Lifecycle(output)
|
|
})
|
|
})
|