mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 00:02:56 +00:00
Co-authored-by: terry-li-hm <12233004+terry-li-hm@users.noreply.github.com>
531 lines
22 KiB
TypeScript
531 lines
22 KiB
TypeScript
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { buildWslInteractiveLoginShellCommand } from '../../shared/wsl-login-shell-command'
|
|
import { resolveSetupRunnerCommand } from '../../shared/setup-runner-command'
|
|
import { resolveWindowsShellLaunchArgs } from './windows-shell-args'
|
|
// Why resolved rather than hardcoded: the wrapper tree is content-addressed.
|
|
import { getShellReadyWrapperRoot } from './local-pty-shell-ready-wrapper-root'
|
|
|
|
const CODEX_LAUNCH_PREFLIGHT = 'C:\\Program Files\\Orca\\orca.exe'
|
|
const CMD_CODEX_LAUNCH_PREFLIGHT =
|
|
'if defined ORCA_CODEX_LAUNCH_PREFLIGHT call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE% agent hooks prepare-codex > nul 2>&1'
|
|
|
|
function expectedWslArgs(linuxCwd: string, distro?: string): string[] {
|
|
const command = `cd '${linuxCwd}' && export PATH="$HOME/.local/bin:$PATH" && ${buildWslInteractiveLoginShellCommand()}`
|
|
// Why spelled out rather than calling buildWslExecArgs: deriving the
|
|
// expectation from the helper under test would still pass if it regressed
|
|
// to the `--` separator.
|
|
const shellArgs = ['--exec', 'sh', '-c', command]
|
|
return distro ? ['-d', distro, ...shellArgs] : shellArgs
|
|
}
|
|
|
|
function decodePowerShellCommand(result: ReturnType<typeof resolveWindowsShellLaunchArgs>): string {
|
|
expect(result.shellArgs.slice(0, 3)).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand'])
|
|
return Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
|
|
}
|
|
|
|
function expectedPowerShellRestoreCwdCommand(cwdLiteral: string): string {
|
|
return `try { Set-Location -LiteralPath ${cwdLiteral} -ErrorAction Stop } catch { Write-Warning "Failed to restore working directory: $_" }`
|
|
}
|
|
|
|
function getGitBashRcfilePath(command: string): string {
|
|
const rcfilePath = command.match(/--rcfile '([^']+)'/)?.[1]
|
|
if (!rcfilePath) {
|
|
throw new Error(`Git Bash launch command has no rcfile: ${command}`)
|
|
}
|
|
return rcfilePath
|
|
}
|
|
|
|
describe('resolveWindowsShellLaunchArgs', () => {
|
|
let previousUserDataPath: string | undefined
|
|
let userDataPath: string
|
|
|
|
beforeEach(() => {
|
|
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
|
|
userDataPath = mkdtempSync(join(tmpdir(), 'windows-shell-args-test-'))
|
|
process.env.ORCA_USER_DATA_PATH = userDataPath
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (previousUserDataPath === undefined) {
|
|
delete process.env.ORCA_USER_DATA_PATH
|
|
} else {
|
|
process.env.ORCA_USER_DATA_PATH = previousUserDataPath
|
|
}
|
|
rmSync(userDataPath, { recursive: true, force: true })
|
|
})
|
|
|
|
it('returns cmd.exe args with chcp 65001 for UTF-8 output', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'cmd.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
undefined,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
expect(result.shellArgs).toEqual(['/K', `chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT}`])
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice')
|
|
})
|
|
|
|
it('embeds short cmd.exe startup commands in shell args', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'cmd.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'codex --no-alt-screen',
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
expect(result.shellArgs).toEqual([
|
|
'/K',
|
|
`chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT} & codex --no-alt-screen`
|
|
])
|
|
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
|
|
})
|
|
|
|
it('keeps quoted cmd.exe startup commands on stdin delivery', () => {
|
|
// Why: direct queued cmd commands need normal quotes, which node-pty's
|
|
// C-runtime argv escaping corrupts when delivered through `/K`.
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'cmd.exe',
|
|
'C:\\Users\\alice\\repo',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'cd /d "C:\\Users\\alice\\repo" && claude "--resume" "session one"'
|
|
)
|
|
expect(result.shellArgs).toEqual(['/K', 'chcp 65001 > nul'])
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
})
|
|
|
|
it('keeps large cmd.exe startup commands on stdin delivery', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'cmd.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
`codex ${'x'.repeat(7000)}`,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
expect(result.shellArgs).toEqual(['/K', `chcp 65001 > nul & ${CMD_CODEX_LAUNCH_PREFLIGHT}`])
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
})
|
|
|
|
it('returns PowerShell args that install OSC 133 bootstrap after normal profile loading', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice'
|
|
)
|
|
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
|
|
|
|
const command = decodePowerShellCommand(result)
|
|
const outputEncodingIndex = command.indexOf('[Console]::OutputEncoding')
|
|
const opencodeRestoreIndex = command.indexOf(
|
|
'$env:OPENCODE_CONFIG_DIR = $env:ORCA_OPENCODE_CONFIG_DIR'
|
|
)
|
|
const mimocodeRestoreIndex = command.indexOf('$env:MIMOCODE_HOME = $env:ORCA_MIMOCODE_HOME')
|
|
const duplicateStateGuardIndex = command.indexOf('Test-Path variable:global:__OrcaOsc133State')
|
|
const languageModeGuardIndex = command.indexOf('LanguageMode -eq "FullLanguage"')
|
|
const ompWrapperIndex = command.indexOf('function Global:omp')
|
|
const ompExtensionIndex = command.indexOf('--extension $env:ORCA_OMP_STATUS_EXTENSION')
|
|
const codexRestoreIndex = command.indexOf('$env:CODEX_HOME = $env:ORCA_CODEX_HOME')
|
|
const promptIndex = command.indexOf('function Global:prompt')
|
|
const cwdRestoreIndex = command.indexOf(
|
|
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'")
|
|
)
|
|
|
|
expect(command).not.toContain('$PROFILE')
|
|
expect(command).not.toContain('ORCA_PI_CODING_AGENT_DIR')
|
|
expect(command).not.toContain('ORCA_OMP_CODING_AGENT_DIR')
|
|
expect(command).not.toContain('$env:PI_CODING_AGENT_DIR = $env:ORCA_OMP_SOURCE_AGENT_DIR')
|
|
for (const restoreIndex of [opencodeRestoreIndex, mimocodeRestoreIndex, codexRestoreIndex]) {
|
|
expect(restoreIndex).toBeGreaterThanOrEqual(0)
|
|
expect(restoreIndex).toBeLessThan(duplicateStateGuardIndex)
|
|
expect(restoreIndex).toBeLessThan(languageModeGuardIndex)
|
|
}
|
|
expect(outputEncodingIndex).toBeGreaterThan(languageModeGuardIndex)
|
|
expect(outputEncodingIndex).toBeGreaterThan(duplicateStateGuardIndex)
|
|
expect(ompWrapperIndex).toBeGreaterThan(outputEncodingIndex)
|
|
expect(ompExtensionIndex).toBeGreaterThan(ompWrapperIndex)
|
|
expect(promptIndex).toBeGreaterThan(ompWrapperIndex)
|
|
expect(cwdRestoreIndex).toBeGreaterThan(promptIndex)
|
|
expect(command).toContain('Esc = [char]27')
|
|
expect(command).toContain('Bel = [char]7')
|
|
expect(command).toContain(')]133;D;$fakeExitCode$(')
|
|
expect(command).toContain(')]133;C$(')
|
|
expect(command).not.toContain('`e]133')
|
|
})
|
|
|
|
it('normalizes MSYS drive cwd before spawning native PowerShell', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'/c/Users/alice/project',
|
|
'C:\\Users\\alice'
|
|
)
|
|
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice\\project')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice\\project')
|
|
expect(decodePowerShellCommand(result)).toContain(
|
|
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice\\project'")
|
|
)
|
|
})
|
|
|
|
it('quotes the PowerShell cwd restore command literally', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
"C:\\Users\\alice\\client's app",
|
|
'C:\\Users\\alice'
|
|
)
|
|
|
|
expect(decodePowerShellCommand(result)).toContain(
|
|
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice\\client''s app'")
|
|
)
|
|
})
|
|
|
|
it('embeds short PowerShell startup commands after the OSC 133 bootstrap', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
"& 'codex' '--no-alt-screen'"
|
|
)
|
|
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
|
|
|
|
const command = decodePowerShellCommand(result)
|
|
expect(command).toContain('function Global:prompt')
|
|
expect(command).toContain(expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'"))
|
|
expect(command.trimEnd().endsWith("& 'codex' '--no-alt-screen'")).toBe(true)
|
|
})
|
|
|
|
it('preserves complex PowerShell startup command text through EncodedCommand', () => {
|
|
const startupCommand =
|
|
'& "C:\\Program Files\\Orca CLI\\orca.exe" "--label" "quoted value"; $env:ORCA_VALUE = "nested"'
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
startupCommand
|
|
)
|
|
|
|
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
|
|
const command = decodePowerShellCommand(result)
|
|
expect(command).toContain(`\n${startupCommand}`)
|
|
expect(command.trimEnd().endsWith(startupCommand)).toBe(true)
|
|
})
|
|
|
|
it('keeps large PowerShell startup commands on stdin delivery', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
`orca ${'x'.repeat(7000)}`
|
|
)
|
|
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
|
|
expect(decodePowerShellCommand(result)).toContain(
|
|
expectedPowerShellRestoreCwdCommand("'C:\\Users\\alice'")
|
|
)
|
|
})
|
|
|
|
it('handles pwsh.exe (PowerShell Core) the same as Windows PowerShell', () => {
|
|
const result = resolveWindowsShellLaunchArgs('pwsh.exe', 'C:\\', 'C:\\Users\\alice')
|
|
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
|
|
expect(decodePowerShellCommand(result)).toContain(expectedPowerShellRestoreCwdCommand("'C:\\'"))
|
|
})
|
|
|
|
it('does not change PowerShell args when the cmd/Git Bash preflight is provided', () => {
|
|
const baseline = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice'
|
|
)
|
|
const withWindowsPreflight = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
undefined,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
|
|
expect(withWindowsPreflight).toEqual(baseline)
|
|
})
|
|
|
|
it('starts Git Bash as an interactive login shell with UTF-8 console setup', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
'/c',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
undefined,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
|
|
// Why: Git Bash inherits the ConPTY OEM code page (CP437), so a byte-writing
|
|
// UTF-8 TUI mojibakes unless the console is switched to UTF-8 before it runs.
|
|
// Assert the contract behaviorally: chcp.com 65001 must precede the exec'd
|
|
// interactive login shell (which preserves `--login -i` and the shell's
|
|
// foreground identity), rather than pinning the exact command string.
|
|
expect(result.shellArgs[0]).toBe('-c')
|
|
const bashCommand = result.shellArgs[1]
|
|
expect(bashCommand).toContain('chcp.com 65001')
|
|
expect(bashCommand).toContain('--rcfile')
|
|
expect(bashCommand).toMatch(/exec "\$BASH" .* -i$/)
|
|
// The UTF-8 switch must run before the login shell is exec'd.
|
|
expect(bashCommand.indexOf('chcp.com')).toBeLessThan(bashCommand.indexOf('exec'))
|
|
// Must stay fail-open: `;` (not `&&`) so a missing chcp.com can't abort the
|
|
// exec and kill the terminal on startup.
|
|
expect(bashCommand).not.toContain('&&')
|
|
expect(result.effectiveCwd).toBe('C:\\')
|
|
expect(result.validationCwd).toBe('C:\\')
|
|
|
|
const bashRcfile = readFileSync(getGitBashRcfilePath(bashCommand), 'utf8')
|
|
expect(bashRcfile).toContain('"${ORCA_CODEX_LAUNCH_PREFLIGHT}" agent hooks prepare-codex')
|
|
expect(bashRcfile).not.toContain(CODEX_LAUNCH_PREFLIGHT)
|
|
})
|
|
|
|
it('keeps Git Bash startup commands on stdin delivery with the preflight wrapper', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'codex --no-alt-screen',
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
|
|
expect(result.shellArgs[1]).toContain('chcp.com 65001')
|
|
expect(result.shellArgs[1]).toContain('--rcfile')
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
})
|
|
|
|
it('quotes a spaced preflight path through each shell environment', () => {
|
|
const cmd = resolveWindowsShellLaunchArgs(
|
|
'cmd.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
undefined,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
const gitBash = resolveWindowsShellLaunchArgs(
|
|
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
undefined,
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
|
|
expect(cmd.shellArgs[1]).toContain(
|
|
'call %ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%%ORCA_CODEX_LAUNCH_PREFLIGHT%%ORCA_CODEX_LAUNCH_PREFLIGHT_CMD_QUOTE%'
|
|
)
|
|
expect(cmd.shellArgs[1]).not.toContain('"')
|
|
expect(cmd.shellArgs[1]).not.toContain(CODEX_LAUNCH_PREFLIGHT)
|
|
const rcfilePath = getGitBashRcfilePath(gitBash.shellArgs[1])
|
|
expect(readFileSync(rcfilePath, 'utf8')).toContain('"${ORCA_CODEX_LAUNCH_PREFLIGHT}"')
|
|
})
|
|
|
|
it('does not apply Git Bash launch args to unrelated bash.exe paths', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
'C:\\Users\\alice\\code',
|
|
'C:\\Users\\alice'
|
|
)
|
|
|
|
expect(result.shellArgs).toEqual([])
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice\\code')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice\\code')
|
|
})
|
|
|
|
it('translates Windows cwd to /mnt/<drive>/... for wsl.exe', () => {
|
|
const baseline = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'C:\\Users\\alice\\code',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'codex'
|
|
)
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'C:\\Users\\alice\\code',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'codex',
|
|
CODEX_LAUNCH_PREFLIGHT
|
|
)
|
|
expect(result).toEqual(baseline)
|
|
expect(result.shellArgs).toEqual(expectedWslArgs('/mnt/c/Users/alice/code'))
|
|
expect(result.startupCommandDeliveredInShellArgs).toBeUndefined()
|
|
// Why: WSL cannot cd into a Windows path, so node-pty must start from the
|
|
// user's Windows home and we inject the Linux cd into the shellArgs above.
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice\\code')
|
|
})
|
|
|
|
it('materializes shell-ready wrappers before building WSL shell args', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'C:\\Users\\alice\\code',
|
|
'C:\\Users\\alice'
|
|
)
|
|
|
|
expect(result.shellArgs).toEqual(expectedWslArgs('/mnt/c/Users/alice/code'))
|
|
expect(existsSync(join(getShellReadyWrapperRoot(), 'bash', 'rcfile'))).toBe(true)
|
|
expect(existsSync(join(getShellReadyWrapperRoot(), 'zsh', '.zshenv'))).toBe(true)
|
|
|
|
// Why: typed OMP keeps its existing shell integration, while typed Prime
|
|
// commands must reach the user's binary without Orca rewriting argv.
|
|
const bashRcfile = readFileSync(join(getShellReadyWrapperRoot(), 'bash', 'rcfile'), 'utf8')
|
|
// Why .zshenv: the omp wrapper is part of the epilogue defined there.
|
|
const zshEnv = readFileSync(join(getShellReadyWrapperRoot(), 'zsh', '.zshenv'), 'utf8')
|
|
for (const wrapperFile of [bashRcfile, zshEnv]) {
|
|
expect(wrapperFile).toContain('command omp --extension "${ORCA_OMP_STATUS_EXTENSION}" "$@"')
|
|
expect(wrapperFile).toContain('function omp { __orca_omp "$@"; }')
|
|
expect(wrapperFile).not.toContain('prime-agent()')
|
|
expect(wrapperFile).not.toContain('__orca_prime_agent')
|
|
expect(wrapperFile).not.toContain('ORCA_PRIME_AGENT_STATUS_EXTENSION')
|
|
expect(wrapperFile).not.toContain('command prime-agent --extension')
|
|
}
|
|
})
|
|
|
|
it('translates MSYS drive cwd to /mnt/<drive>/... for wsl.exe', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'/c/Users/alice/project',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
'codex'
|
|
)
|
|
|
|
expect(result.shellArgs).toEqual(expectedWslArgs('/mnt/c/Users/alice/project'))
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice\\project')
|
|
})
|
|
|
|
it.each(['/a', '/c'])('keeps a selected WSL runtime single-letter cwd exact (%s)', (cwd) => {
|
|
const result = resolveWindowsShellLaunchArgs('wsl.exe', cwd, 'C:\\Users\\alice', {
|
|
distro: 'Ubuntu',
|
|
treatPosixCwdAsWsl: true
|
|
})
|
|
|
|
expect(result.shellArgs).toEqual(expectedWslArgs(cwd, 'Ubuntu'))
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe(`\\\\wsl.localhost\\Ubuntu\\${cwd.slice(1)}`)
|
|
})
|
|
|
|
it('escapes single quotes when translating a WSL cwd', () => {
|
|
const result = resolveWindowsShellLaunchArgs('wsl.exe', "C:\\weird'path", 'C:\\Users\\alice')
|
|
// The injected sh cmd must not break out of the surrounding single quotes
|
|
// when the path contains a ' character.
|
|
expect(result.shellArgs[3]).toContain("cd '/mnt/c/weird'\\''path'")
|
|
expect(result.shellArgs[3]).toContain('exec "$_orca_wsl_shell" -l')
|
|
})
|
|
|
|
it('falls back to /mnt/c when cwd is not a drive-letter path', () => {
|
|
const result = resolveWindowsShellLaunchArgs('wsl.exe', '\\\\server\\share', 'C:\\Users\\alice')
|
|
expect(result.shellArgs[3]).toContain('cd \'/mnt/c\' && export PATH="$HOME/.local/bin:$PATH"')
|
|
})
|
|
|
|
it('keeps WSL UNC worktree cwd inside the matching distro', () => {
|
|
const originalPlatform = process.platform
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: 'win32'
|
|
})
|
|
|
|
try {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo',
|
|
'C:\\Users\\alice'
|
|
)
|
|
expect(result.shellArgs).toEqual(expectedWslArgs('/home/alice/repo', 'Ubuntu'))
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo')
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: originalPlatform
|
|
})
|
|
}
|
|
})
|
|
|
|
it('keeps POSIX cwd inside the worktree distro when WSL context is provided', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'wsl.exe',
|
|
'/home/alice/repo/subdir',
|
|
'C:\\Users\\alice',
|
|
{ distro: 'Ubuntu', treatPosixCwdAsWsl: true }
|
|
)
|
|
|
|
expect(result.shellArgs).toEqual(expectedWslArgs('/home/alice/repo/subdir', 'Ubuntu'))
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('\\\\wsl.localhost\\Ubuntu\\home\\alice\\repo\\subdir')
|
|
})
|
|
|
|
it('falls back to empty args + same cwd for unknown shells', () => {
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'C:\\tools\\fish.exe',
|
|
'C:\\Users\\alice',
|
|
'C:\\Users\\alice'
|
|
)
|
|
expect(result.shellArgs).toEqual([])
|
|
expect(result.effectiveCwd).toBe('C:\\Users\\alice')
|
|
expect(result.validationCwd).toBe('C:\\Users\\alice')
|
|
})
|
|
|
|
it('is case-insensitive on the shell basename', () => {
|
|
const result = resolveWindowsShellLaunchArgs('PowerShell.EXE', 'C:\\', 'C:\\')
|
|
expect(result.shellArgs).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand', expect.any(String)])
|
|
})
|
|
})
|
|
|
|
// Regression guard for issue #7236: a worktree Setup Script runs through a
|
|
// generated `.cmd` runner invoked as `cmd.exe /c "<runner>"`. When PowerShell
|
|
// received that command as raw typed stdin, a dropped/unbalanced quote surfaced
|
|
// as a "missing terminator" parser error. Delivering it via -EncodedCommand
|
|
// (base64 UTF-16) keeps the quotes balanced and the text verbatim, so it can
|
|
// never be re-parsed as an open string.
|
|
describe('issue #7236: PowerShell setup-runner command delivery', () => {
|
|
// git rev-parse hands back a forward-slash Windows-absolute path for the runner.
|
|
const runnerPath = 'C:/Users/alice/repo/.git/orca/setup-runner.cmd'
|
|
|
|
it('wraps the setup runner in balanced double quotes', () => {
|
|
const { command } = resolveSetupRunnerCommand(runnerPath, 'windows')
|
|
expect(command).toBe(`cmd.exe /c "${runnerPath}"`)
|
|
expect((command.match(/"/g) ?? []).length % 2).toBe(0)
|
|
})
|
|
|
|
it('delivers the setup-runner command through -EncodedCommand, never raw stdin', () => {
|
|
const { command } = resolveSetupRunnerCommand(runnerPath, 'windows')
|
|
const result = resolveWindowsShellLaunchArgs(
|
|
'powershell.exe',
|
|
'C:\\Users\\alice\\repo',
|
|
'C:\\Users\\alice',
|
|
undefined,
|
|
command
|
|
)
|
|
|
|
// The flag tells the daemon/provider NOT to also type the command over
|
|
// stdin — raw stdin delivery is the pre-encoded path that broke in #7236.
|
|
expect(result.startupCommandDeliveredInShellArgs).toBe(true)
|
|
expect(result.shellArgs.slice(0, 3)).toEqual(['-NoLogo', '-NoExit', '-EncodedCommand'])
|
|
|
|
const decoded = Buffer.from(result.shellArgs[3] ?? '', 'base64').toString('utf16le')
|
|
expect(decoded).toContain(`\n${command}`)
|
|
expect(decoded.trimEnd().endsWith(command)).toBe(true)
|
|
// Quotes survive encoding intact, so PowerShell parses one balanced string.
|
|
expect((decoded.slice(decoded.lastIndexOf(command)).match(/"/g) ?? []).length % 2).toBe(0)
|
|
})
|
|
})
|