mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 02:27:43 +00:00
* fix(agents): direct work-item and checks-fix launches quote for the execution host GitHub Project Start, PR checks Fix and the source-control/checks preflights resolved their launch platform from the client OS whenever the repo had no SSH connection, so a Windows client starting work on a paired Linux Orca server sent a PowerShell-quoted --prefill to bash. They now read the execution-host facts #27259 introduced (server-reported OS, SSH-reported OS, WSL), and the path-shape resolver is removed so one helper decides. * fix(agents): resolve direct-launch and preflight platforms from the repo owner Review follow-up: a WSL worktree on a paired Windows server quotes for Linux, the owner-row fixture reports its server OS, and the source-control preflights read the repo owner rather than the focus-defaulted worktree route. * fix(agents): preflight platform honours a worktree's own host Review follow-up: a per-worktree host outranks the repo owner, the host-platform test's mutable client platform is typed, and the source-control panel fixture carries the host slices. * fix(agents): a server-projected SSH worktree reads its server's SSH state Review follow-up: the preflight resolver keeps the runtime owner for nested SSH worktrees instead of reading this client's own SSH connections. * fix(agents): cmd.exe pastes a multi-line draft instead of prefilling it Review follow-up: now that direct launches pass the local Windows shell, a multi-line --prefill under cmd.exe would submit at its first newline. * fix(agents): direct launches quote for the shell their tab spawns A project's Windows runtime can override the terminal setting (WSL setting, Windows runtime spawns PowerShell), so resolve the tab's effective shell before quoting. The cmd.exe parity row now expects cmd quoting.
161 lines
6.1 KiB
TypeScript
161 lines
6.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildShellCommandFromArgv,
|
|
clearEnvCommand,
|
|
commandSeparator,
|
|
isPosixStartupShell,
|
|
quoteStartupArg,
|
|
tokenizeStartupCommand
|
|
} from './tui-agent-startup-shell'
|
|
import { buildAgentDraftLaunchPlan } from './tui-agent-startup'
|
|
|
|
function expectSpansCoverTokens(source: string, shell: 'powershell' | 'cmd'): string[] {
|
|
const result = tokenizeStartupCommand(source, shell)
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
return []
|
|
}
|
|
expect(result.spans).toHaveLength(result.tokens.length)
|
|
let previousEnd = 0
|
|
for (const [index, { start, end }] of result.spans.entries()) {
|
|
expect(start).toBeGreaterThanOrEqual(previousEnd)
|
|
expect(end).toBeGreaterThan(start)
|
|
// Every raw span must re-tokenize to exactly its own token.
|
|
const slice = tokenizeStartupCommand(source.slice(start, end), shell)
|
|
expect(slice.ok && slice.tokens).toEqual([result.tokens[index]])
|
|
previousEnd = end
|
|
}
|
|
return result.spans.map(({ start, end }) => source.slice(start, end))
|
|
}
|
|
|
|
describe('tokenizeStartupCommand spans (windows shells)', () => {
|
|
it('covers plain and quoted tokens on powershell', () => {
|
|
const source = "claude --msg 'hello world'"
|
|
const result = tokenizeStartupCommand(source, 'powershell')
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
tokens: ['claude', '--msg', 'hello world'],
|
|
spans: [
|
|
{ start: 0, end: 6, divergesFromShell: false },
|
|
{ start: 7, end: 12, divergesFromShell: false },
|
|
{ start: 13, end: 26, divergesFromShell: false }
|
|
]
|
|
})
|
|
})
|
|
|
|
it('starts a span at a token-leading escape character', () => {
|
|
expect(expectSpansCoverTokens('claude ^&literal next', 'cmd')).toEqual([
|
|
'claude',
|
|
'^&literal',
|
|
'next'
|
|
])
|
|
expect(expectSpansCoverTokens('claude `x tail', 'powershell')).toEqual(['claude', '`x', 'tail'])
|
|
})
|
|
|
|
it('spans a powershell doubled-quote token as one raw range', () => {
|
|
expect(expectSpansCoverTokens("claude 'a''b' end", 'powershell')).toEqual([
|
|
'claude',
|
|
"'a''b'",
|
|
'end'
|
|
])
|
|
})
|
|
|
|
it('spans a token opened by a quote at end of input', () => {
|
|
expect(expectSpansCoverTokens('claude ""', 'cmd')).toEqual(['claude', '""'])
|
|
})
|
|
})
|
|
|
|
describe('one Unix startup dialect', () => {
|
|
it('clears variables with a self-contained branch, not a per-shell builtin', () => {
|
|
// Why not `unset`/`set -e` alone, and why not a wrapper-defined helper:
|
|
// Orca only wraps zsh/bash/fish, so an `sh`/`dash`/`ksh` login shell — and
|
|
// any shell the user pastes copied text into — would not have the helper.
|
|
// startup-shell-portability.live-shell.test.ts proves this form works in
|
|
// real sh/bash/zsh/dash/ksh/fish.
|
|
expect(clearEnvCommand('CODEX_HOME', 'posix')).toBe(
|
|
`command test -n "$fish_pid" && set --erase -g CODEX_HOME; command test -z "$fish_pid" && unset CODEX_HOME; true`
|
|
)
|
|
expect(clearEnvCommand(['A', 'B'], 'posix')).toBe(
|
|
`command test -n "$fish_pid" && set --erase -g A B; command test -z "$fish_pid" && unset A B; true`
|
|
)
|
|
expect(clearEnvCommand('CODEX_HOME', 'cmd')).toBe('set "CODEX_HOME="')
|
|
expect(clearEnvCommand('CODEX_HOME', 'powershell')).toBe(
|
|
'Remove-Item Env:CODEX_HOME -ErrorAction SilentlyContinue'
|
|
)
|
|
})
|
|
|
|
it.each([
|
|
'FOO; rm -rf /tmp/x',
|
|
'FOO BAR',
|
|
'',
|
|
'1LEADING_DIGIT',
|
|
'has-dash',
|
|
'$FOO',
|
|
'PATH\nHOME'
|
|
])('refuses %j as an environment variable name', (bad) => {
|
|
// Why: the name is interpolated straight into a shell line. Anything but an
|
|
// identifier is a command injection, and in fish `set --erase -g` would
|
|
// really delete whatever it names — PATH or HOME included.
|
|
expect(() => clearEnvCommand(bad, 'posix')).toThrow(/not an environment variable name/)
|
|
expect(() => clearEnvCommand(['OK', bad], 'posix')).toThrow(/not an environment variable name/)
|
|
})
|
|
|
|
it('quotes so fish reads back the same bytes sh does', () => {
|
|
// fish single quotes are NOT literal — `\\` and `\'` are escapes inside them —
|
|
// so the sh `'\''` idiom would halve these backslashes when fish read them,
|
|
// and a trailing backslash would be a hard syntax error.
|
|
expect(quoteStartupArg("it's", 'posix')).toBe(`'it'"'"'s'`)
|
|
expect(quoteStartupArg(String.raw`\\server\share`, 'posix')).toBe(
|
|
`"\\\\""\\\\"'server'"\\\\"'share'`
|
|
)
|
|
expect(quoteStartupArg('ends\\', 'posix')).toBe(`'ends'"\\\\"`)
|
|
expect(quoteStartupArg('', 'posix')).toBe(`''`)
|
|
})
|
|
|
|
it('keeps the sh-family grammar claims that do hold for fish', () => {
|
|
expect(commandSeparator('posix')).toBe('; ')
|
|
expect(isPosixStartupShell('posix')).toBe(true)
|
|
expect(isPosixStartupShell('powershell')).toBe(false)
|
|
expect(buildShellCommandFromArgv(['codex', 'resume', 'a b'], 'posix')).toBe(
|
|
`'codex' 'resume' 'a b'`
|
|
)
|
|
})
|
|
|
|
it('round-trips an adversarial argument through quote then tokenize', () => {
|
|
for (const value of [
|
|
String.raw`use \d+ and \\server\share`,
|
|
"it's mine",
|
|
'ends\\',
|
|
'a "b" c',
|
|
'$PATH *.ts'
|
|
]) {
|
|
const tokenized = tokenizeStartupCommand(quoteStartupArg(value, 'posix'), 'posix')
|
|
expect(tokenized.ok && tokenized.tokens).toEqual([value])
|
|
}
|
|
})
|
|
|
|
it('clears an agent draft prefill variable with the portable teardown', () => {
|
|
const plan = buildAgentDraftLaunchPlan({
|
|
agent: 'pi',
|
|
draft: 'hello',
|
|
cmdOverrides: {},
|
|
platform: 'darwin'
|
|
})
|
|
|
|
expect(plan?.launchCommand).toBe(
|
|
`pi; command test -n "$fish_pid" && set --erase -g ORCA_PI_PREFILL; command test -z "$fish_pid" && unset ORCA_PI_PREFILL; true`
|
|
)
|
|
expect(plan?.env?.ORCA_PI_PREFILL).toBe('hello')
|
|
})
|
|
|
|
it('leaves a multi-line argv draft to the paste fallback in cmd.exe', () => {
|
|
const draft = 'Linked Linear issue: ENG-1\nhttps://linear.app/acme/issue/ENG-1'
|
|
const args = { agent: 'claude' as const, draft, cmdOverrides: {}, platform: 'win32' as const }
|
|
|
|
expect(buildAgentDraftLaunchPlan({ ...args, shell: 'cmd' })).toBeNull()
|
|
expect(buildAgentDraftLaunchPlan({ ...args, shell: 'powershell' })?.launchCommand).toContain(
|
|
'`n'
|
|
)
|
|
})
|
|
})
|