mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
* chore: shorten process-host comments * chore: audit CLI parsing and host-routing comments * chore: remove stale host-selector restriction comment * chore: shorten remaining CLI root comments * Shorten CLI handler comments * Shorten CLI runtime and command specification comments * Shorten top-level main process A–G comments * [comment audit] Shorten main process H–K comments --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
472 lines
17 KiB
TypeScript
472 lines
17 KiB
TypeScript
import type * as GitRunner from './git/runner'
|
||
|
||
import { describe, expect, it, vi } from 'vitest'
|
||
import { makeHookTestRepo } from './hooks-test-fixtures'
|
||
|
||
vi.mock('fs', () => ({
|
||
readFileSync: vi.fn(),
|
||
existsSync: vi.fn(),
|
||
mkdirSync: vi.fn(),
|
||
writeFileSync: vi.fn(),
|
||
rmSync: vi.fn(),
|
||
chmodSync: vi.fn()
|
||
}))
|
||
|
||
const { gitExecFileSyncMock } = vi.hoisted(() => ({
|
||
gitExecFileSyncMock: vi.fn()
|
||
}))
|
||
|
||
vi.mock('child_process', () => ({
|
||
exec: vi.fn(),
|
||
execFile: vi.fn(),
|
||
execFileSync: vi.fn(),
|
||
spawn: vi.fn()
|
||
}))
|
||
|
||
vi.mock('./git/runner', async () => ({
|
||
...(await vi.importActual<typeof GitRunner>('./git/runner')),
|
||
gitExecFileSync: gitExecFileSyncMock
|
||
}))
|
||
|
||
describe('runner script builders', () => {
|
||
it('builds Windows runners for newline-heavy scripts without line-array splitting', async () => {
|
||
const { buildWindowsRunnerScript } = await import('./setup-runner-script-text')
|
||
const script = `${'\r\n'.repeat(10_000)}pnpm install\r\nnpm run build\n`
|
||
const splitSpy = vi.spyOn(String.prototype, 'split')
|
||
const replaceSpy = vi.spyOn(String.prototype, 'replace')
|
||
|
||
try {
|
||
const result = buildWindowsRunnerScript(script)
|
||
|
||
expect(
|
||
result.startsWith('@echo off\r\nsetlocal EnableExtensions DisableDelayedExpansion\r\n')
|
||
).toBe(true)
|
||
expect(result).toContain('call pnpm install\r\nif errorlevel 1 exit /b %errorlevel%')
|
||
expect(result).toContain('call npm run build\r\nif errorlevel 1 exit /b %errorlevel%')
|
||
const usedLineSplit = splitSpy.mock.calls.some(
|
||
([separator]) =>
|
||
(typeof separator === 'string' && separator === '\n') ||
|
||
(separator instanceof RegExp && separator.source === '\\r?\\n')
|
||
)
|
||
const usedNewlineReplace = replaceSpy.mock.calls.some(
|
||
([pattern]) =>
|
||
pattern instanceof RegExp && (pattern.source === '\\r?\\n' || pattern.source === '\\r\\n')
|
||
)
|
||
expect(usedLineSplit).toBe(false)
|
||
expect(usedNewlineReplace).toBe(false)
|
||
} finally {
|
||
splitSpy.mockRestore()
|
||
replaceSpy.mockRestore()
|
||
}
|
||
})
|
||
|
||
it('builds POSIX runners without regex-wide CRLF normalization', async () => {
|
||
const { buildPosixRunnerScript } = await import('./setup-runner-script-text')
|
||
const script = `${'echo setup\r\n'.repeat(10_000)}echo done`
|
||
const replaceSpy = vi.spyOn(String.prototype, 'replace')
|
||
|
||
try {
|
||
const result = buildPosixRunnerScript(script)
|
||
|
||
expect(result.startsWith('#!/usr/bin/env bash\nset -e\necho setup\n')).toBe(true)
|
||
expect(result.endsWith('echo done\n')).toBe(true)
|
||
const usedCrlfReplace = replaceSpy.mock.calls.some(
|
||
([pattern]) => pattern instanceof RegExp && pattern.source === '\\r\\n'
|
||
)
|
||
expect(usedCrlfReplace).toBe(false)
|
||
} finally {
|
||
replaceSpy.mockRestore()
|
||
}
|
||
})
|
||
})
|
||
|
||
describe('createSetupRunnerScript', () => {
|
||
const makeRepo = (setupAgentStartupPolicy?: 'start-immediately' | 'wait-for-setup') =>
|
||
makeHookTestRepo({
|
||
mode: 'auto',
|
||
setupAgentStartupPolicy,
|
||
scripts: { setup: '', archive: '' }
|
||
})
|
||
|
||
it('writes POSIX setup runners for shebang-declared scripts on native Windows paths', async () => {
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.sh\n')
|
||
const fs = await import('node:fs')
|
||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||
const chmodSyncMock = vi.mocked(fs.chmodSync)
|
||
writeFileSyncMock.mockClear()
|
||
chmodSyncMock.mockClear()
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
const result = createSetupRunnerScript(
|
||
makeRepo(),
|
||
'C:\\repo-worktree',
|
||
'#!/usr/bin/env bash\r\npnpm install\r\nnpm run build',
|
||
undefined,
|
||
{ family: 'posix' }
|
||
)
|
||
|
||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||
['rev-parse', '--git-path', 'orca/setup-runner.sh'],
|
||
{ cwd: 'C:\\repo-worktree' }
|
||
)
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'C:\\repo\\.git\\orca\\setup-runner.sh',
|
||
'#!/usr/bin/env bash\nset -e\npnpm install\nnpm run build\n',
|
||
'utf-8'
|
||
)
|
||
expect(chmodSyncMock).not.toHaveBeenCalled()
|
||
expect(result).toMatchObject({
|
||
runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh',
|
||
shell: { family: 'posix' }
|
||
})
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('keeps batch setup scripts on cmd.exe when the terminal is Git Bash', async () => {
|
||
// Batch scripts must retain cmd semantics even when the pane uses Git Bash (#6967).
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.cmd\n')
|
||
const fs = await import('node:fs')
|
||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||
writeFileSyncMock.mockClear()
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
const result = createSetupRunnerScript(
|
||
makeRepo(),
|
||
'C:\\repo-worktree',
|
||
'copy .env.example .env\r\nxcopy /E assets dist',
|
||
undefined,
|
||
{ family: 'posix' }
|
||
)
|
||
|
||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||
['rev-parse', '--git-path', 'orca/setup-runner.cmd'],
|
||
{ cwd: 'C:\\repo-worktree' }
|
||
)
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||
expect.stringContaining('call copy .env.example .env\r\n'),
|
||
'utf-8'
|
||
)
|
||
expect(result).toMatchObject({
|
||
runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||
// shell describes the pane; the .cmd extension selects the runner’s interpreter.
|
||
shell: { family: 'posix' }
|
||
})
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('hands a Git Bash pane a launch command MSYS cannot rewrite for a cmd runner', async () => {
|
||
// MSYS rewrites cmd.exe /c into a drive path, leaving setup stuck at an interactive prompt (#6896).
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.cmd\n')
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
const { buildSetupRunnerCommand } = await import('../shared/setup-runner-command')
|
||
const result = createSetupRunnerScript(
|
||
makeRepo(),
|
||
'C:\\repo-worktree',
|
||
'copy .env.example .env',
|
||
undefined,
|
||
{ family: 'posix' }
|
||
)
|
||
|
||
const command = buildSetupRunnerCommand(result.runnerScriptPath, 'windows', result.shell)
|
||
|
||
expect(command).not.toContain('cmd.exe /c')
|
||
expect(command).not.toContain('bash ')
|
||
expect(command).toContain('powershell.exe')
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('replays interpreter flags declared on the shebang line', async () => {
|
||
// bash <path> ignores the script’s shebang options unless the runner reapplies them.
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.sh\n')
|
||
const fs = await import('node:fs')
|
||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||
writeFileSyncMock.mockClear()
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
createSetupRunnerScript(
|
||
makeRepo(),
|
||
'C:\\repo-worktree',
|
||
'#!/usr/bin/env -S bash -euo pipefail\nmake build | tee build.log',
|
||
undefined,
|
||
{ family: 'posix' }
|
||
)
|
||
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'C:\\repo\\.git\\orca\\setup-runner.sh',
|
||
'#!/usr/bin/env bash\nset -e\nset -euo pipefail\nmake build | tee build.log\n',
|
||
'utf-8'
|
||
)
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('preserves cmd.exe setup runner semantics for configured cmd users', async () => {
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('C:\\repo\\.git\\orca\\setup-runner.cmd\n')
|
||
const fs = await import('node:fs')
|
||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||
writeFileSyncMock.mockClear()
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
const result = createSetupRunnerScript(
|
||
makeRepo(),
|
||
'C:\\repo-worktree',
|
||
'pnpm install\nnpm run build',
|
||
undefined,
|
||
{ family: 'cmd' }
|
||
)
|
||
|
||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||
['rev-parse', '--git-path', 'orca/setup-runner.cmd'],
|
||
{ cwd: 'C:\\repo-worktree' }
|
||
)
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||
expect.stringContaining('call pnpm install\r\nif errorlevel 1 exit /b %errorlevel%'),
|
||
'utf-8'
|
||
)
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'C:\\repo\\.git\\orca\\setup-runner.cmd',
|
||
expect.stringContaining('call npm run build\r\nif errorlevel 1 exit /b %errorlevel%'),
|
||
'utf-8'
|
||
)
|
||
expect(result.shell).toEqual({ family: 'cmd' })
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('keeps POSIX runner behavior on POSIX platforms', async () => {
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n')
|
||
const fs = await import('node:fs')
|
||
const writeFileSyncMock = vi.mocked(fs.writeFileSync)
|
||
const chmodSyncMock = vi.mocked(fs.chmodSync)
|
||
writeFileSyncMock.mockClear()
|
||
chmodSyncMock.mockClear()
|
||
const originalPlatform = process.platform
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
|
||
|
||
try {
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
const result = createSetupRunnerScript(makeRepo(), '/test/worktree', 'pnpm install')
|
||
|
||
expect(gitExecFileSyncMock).toHaveBeenCalledWith(
|
||
['rev-parse', '--git-path', 'orca/setup-runner.sh'],
|
||
{ cwd: '/test/worktree' }
|
||
)
|
||
expect(writeFileSyncMock).toHaveBeenCalledWith(
|
||
'/test/repo/.git/orca/setup-runner.sh',
|
||
'#!/usr/bin/env bash\nset -e\npnpm install\n',
|
||
'utf-8'
|
||
)
|
||
expect(chmodSyncMock).toHaveBeenCalledWith('/test/repo/.git/orca/setup-runner.sh', 0o755)
|
||
expect(result.shell).toBeUndefined()
|
||
} finally {
|
||
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
|
||
}
|
||
})
|
||
|
||
it('waits when either local or project policy requires completed setup', async () => {
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n')
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
createSetupRunnerScript(makeRepo(), '/test/worktree', 'echo setup').waitForAgentStartup
|
||
).toBeUndefined()
|
||
expect(
|
||
createSetupRunnerScript(makeRepo('start-immediately'), '/test/worktree', 'echo setup')
|
||
.waitForAgentStartup
|
||
).toBeUndefined()
|
||
expect(
|
||
createSetupRunnerScript(makeRepo('wait-for-setup'), '/test/worktree', 'echo setup')
|
||
.waitForAgentStartup
|
||
).toBe(true)
|
||
expect(
|
||
createSetupRunnerScript(
|
||
makeRepo('start-immediately'),
|
||
'/test/worktree',
|
||
'echo setup',
|
||
undefined,
|
||
undefined,
|
||
'wait-for-setup'
|
||
).waitForAgentStartup
|
||
).toBe(true)
|
||
expect(
|
||
createSetupRunnerScript(
|
||
makeRepo('wait-for-setup'),
|
||
'/test/worktree',
|
||
'echo setup',
|
||
undefined,
|
||
undefined,
|
||
'start-immediately'
|
||
).waitForAgentStartup
|
||
).toBe(true)
|
||
})
|
||
|
||
it('marks setup-runner terminals for the always-on credential guard', async () => {
|
||
gitExecFileSyncMock.mockReset()
|
||
gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n')
|
||
const { createSetupRunnerScript } = await import('./worktree-runner-script')
|
||
|
||
const setup = createSetupRunnerScript(makeRepo(), '/test/worktree', 'git fetch')
|
||
|
||
expect(setup.envVars).toMatchObject({
|
||
ORCA_ROOT_PATH: '/test/repo',
|
||
ORCA_WORKTREE_PATH: '/test/worktree',
|
||
ORCA_INTERNAL_TERMINAL_GIT_CREDENTIAL_GUARD_POLICY: 'guard'
|
||
})
|
||
})
|
||
})
|
||
|
||
describe('resolveSetupRunnerShell', () => {
|
||
const installedGitBash = {
|
||
resolveGitBashShellPath: () => 'C:\\Program Files\\Git\\bin\\bash.exe'
|
||
}
|
||
|
||
it('maps git-bash to POSIX setup launch metadata on Windows', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'git-bash' }, 'win32', installedGitBash)
|
||
).toEqual({
|
||
family: 'posix'
|
||
})
|
||
})
|
||
|
||
it('falls back to the cmd runner when the git-bash setting has no installed Git Bash', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'git-bash' }, 'win32', {
|
||
resolveGitBashShellPath: () => null
|
||
})
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
|
||
it('keeps the cmd runner for a non-Git bash such as Cygwin', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'C:\\cygwin64\\bin\\bash.exe' }, 'win32', {
|
||
resolveGitBashShellPath: () => null
|
||
})
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
|
||
it('keeps the cmd runner for a bare bash whose flavor cannot be resolved', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'bash' }, 'win32', {
|
||
resolveGitBashShellPath: () => null
|
||
})
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
|
||
it('uses the POSIX runner for a bare bash that resolves to Git Bash', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'bash' }, 'win32', installedGitBash)
|
||
).toEqual({ family: 'posix' })
|
||
})
|
||
|
||
it('uses the POSIX runner for an extension-less Git Bash path', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell(
|
||
{ terminalWindowsShell: 'C:\\Program Files\\Git\\bin\\bash' },
|
||
'win32',
|
||
installedGitBash
|
||
)
|
||
).toEqual({ family: 'posix' })
|
||
})
|
||
|
||
it('preserves the existing cmd runner for PowerShell terminals', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell(
|
||
{
|
||
terminalWindowsShell: 'powershell.exe',
|
||
terminalWindowsPowerShellImplementation: 'pwsh.exe'
|
||
},
|
||
'win32',
|
||
installedGitBash
|
||
)
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
|
||
it('preserves cmd setup compatibility when a Windows-host project has a WSL shell setting', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell({ terminalWindowsShell: 'wsl.exe' }, 'win32', installedGitBash)
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
|
||
it('classifies an installed explicit Git Bash executable as a POSIX runner', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
const { resolveWindowsGitBashShellPath } = await import('./git-bash')
|
||
|
||
expect(
|
||
resolveSetupRunnerShell(
|
||
{ terminalWindowsShell: 'C:\\Program Files\\Git\\bin\\bash.exe' },
|
||
'win32',
|
||
{
|
||
resolveGitBashShellPath: (shell) =>
|
||
resolveWindowsGitBashShellPath(shell, { platform: 'win32', exists: () => true })
|
||
}
|
||
)
|
||
).toEqual({ family: 'posix' })
|
||
})
|
||
|
||
it('falls back to the cmd runner when the explicit Git Bash path no longer exists', async () => {
|
||
const { resolveSetupRunnerShell } = await import('./worktree-runner-script')
|
||
const { resolveWindowsGitBashShellPath } = await import('./git-bash')
|
||
|
||
// Reject stale shells before setup waits on a runner the PTY cannot spawn.
|
||
expect(
|
||
resolveSetupRunnerShell(
|
||
{ terminalWindowsShell: 'C:\\Program Files\\Git\\bin\\bash.exe' },
|
||
'win32',
|
||
{
|
||
resolveGitBashShellPath: (shell) =>
|
||
resolveWindowsGitBashShellPath(shell, { platform: 'win32', exists: () => false })
|
||
}
|
||
)
|
||
).toEqual({ family: 'cmd' })
|
||
})
|
||
})
|