mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 16:02:35 +00:00
- Fix workspace space cache and git status cleanup checks - Treat browser tabs as workspace delete blockers - Move deletion readiness into shared presentation logic so tests cover it - Preserve explicit empty git status entries for clean worktrees - Add package manager cache cleanup to space manager - Detect npm, pnpm, Yarn, and Bun caches from lockfiles during scans - Add explicit cleanup IPC for safe and aggressive cache actions - Support local, SSH, and Windows command execution paths safely - Tighten workspace deletion decisions with git, editor, agent, and terminal state
143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { execFileMock, execFileSyncMock, spawnMock } = vi.hoisted(() => ({
|
|
execFileMock: vi.fn(),
|
|
execFileSyncMock: vi.fn(),
|
|
spawnMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execFile: execFileMock,
|
|
execFileSync: execFileSyncMock,
|
|
spawn: spawnMock
|
|
}))
|
|
|
|
import { commandExecFileAsync } from './runner'
|
|
|
|
type MockChildProcess = EventEmitter & {
|
|
stdout: EventEmitter
|
|
stderr: EventEmitter
|
|
pid: number
|
|
kill: ReturnType<typeof vi.fn>
|
|
unref?: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
function createMockChildProcess(pid: number): MockChildProcess {
|
|
const child = new EventEmitter() as MockChildProcess
|
|
child.stdout = new EventEmitter()
|
|
child.stderr = new EventEmitter()
|
|
child.pid = pid
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
function createMockTaskkillProcess(): MockChildProcess {
|
|
const child = createMockChildProcess(9000)
|
|
child.unref = vi.fn()
|
|
return child
|
|
}
|
|
|
|
async function withPlatform<T>(platform: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
|
|
const original = process.platform
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
|
|
try {
|
|
return await fn()
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: original })
|
|
}
|
|
}
|
|
|
|
describe('commandExecFileAsync Windows command shims', () => {
|
|
const originalComSpec = process.env.ComSpec
|
|
|
|
beforeEach(() => {
|
|
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
|
|
execFileMock.mockReset()
|
|
execFileSyncMock.mockReset()
|
|
spawnMock.mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
if (originalComSpec === undefined) {
|
|
delete process.env.ComSpec
|
|
} else {
|
|
process.env.ComSpec = originalComSpec
|
|
}
|
|
})
|
|
|
|
it('kills aborted Windows .cmd shim executions as a process tree', async () => {
|
|
await withPlatform('win32', async () => {
|
|
const command = createMockChildProcess(1234)
|
|
const taskkill = createMockTaskkillProcess()
|
|
spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command))
|
|
|
|
const controller = new AbortController()
|
|
const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['--version'], {
|
|
cwd: 'C:\\repo',
|
|
signal: controller.signal
|
|
})
|
|
const rejection = expect(promise).rejects.toMatchObject({ name: 'AbortError' })
|
|
controller.abort()
|
|
|
|
await rejection
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'taskkill',
|
|
['/pid', '1234', '/t', '/f'],
|
|
expect.objectContaining({ stdio: 'ignore', windowsHide: true })
|
|
)
|
|
expect(command.kill).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('kills timed-out Windows .cmd shim executions as a process tree', async () => {
|
|
vi.useFakeTimers()
|
|
await withPlatform('win32', async () => {
|
|
const command = createMockChildProcess(1234)
|
|
const taskkill = createMockTaskkillProcess()
|
|
spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command))
|
|
|
|
const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['store', 'prune'], {
|
|
cwd: 'C:\\repo',
|
|
timeout: 1000
|
|
})
|
|
const rejection = expect(promise).rejects.toThrow('C:\\tools\\pnpm.cmd timed out.')
|
|
await vi.advanceTimersByTimeAsync(1000)
|
|
|
|
await rejection
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'taskkill',
|
|
['/pid', '1234', '/t', '/f'],
|
|
expect.objectContaining({ stdio: 'ignore', windowsHide: true })
|
|
)
|
|
expect(command.kill).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('kills over-buffer Windows .cmd shim executions as a process tree', async () => {
|
|
await withPlatform('win32', async () => {
|
|
const command = createMockChildProcess(1234)
|
|
const taskkill = createMockTaskkillProcess()
|
|
spawnMock.mockImplementation((cmd: string) => (cmd === 'taskkill' ? taskkill : command))
|
|
|
|
const promise = commandExecFileAsync('C:\\tools\\pnpm.cmd', ['store', 'prune'], {
|
|
cwd: 'C:\\repo',
|
|
maxBuffer: 2
|
|
})
|
|
const rejection = expect(promise).rejects.toThrow(
|
|
'C:\\tools\\pnpm.cmd stdout exceeded maxBuffer.'
|
|
)
|
|
command.stdout.emit('data', Buffer.from('too much output'))
|
|
|
|
await rejection
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'taskkill',
|
|
['/pid', '1234', '/t', '/f'],
|
|
expect.objectContaining({ stdio: 'ignore', windowsHide: true })
|
|
)
|
|
expect(command.kill).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|