Files
deepseek-harness/packages/subprocess/win32-process/tests/process.spec.ts
T

245 lines
7.7 KiB
TypeScript

import koffi from 'koffi'
import { describe, expect, it, vi } from 'vitest'
import {
Win32Error,
drainPipe,
spawnInheritedJobProcess,
spawnPipedProcess,
} from '../src/index.ts'
import { CREATE_SUSPENDED } from '../src/abi.ts'
import { PROCESS_INFORMATION } from '../src/ffi.ts'
import type { NativePtr, Win32ProcessBindings } from '../src/index.ts'
const PVOID = koffi.pointer('void')
function inheritedApi(overrides: Partial<Win32ProcessBindings> = {}): {
api: Win32ProcessBindings
events: string[]
createProcessAsUserW: ReturnType<typeof vi.fn>
assignProcessToJobObject: ReturnType<typeof vi.fn>
resumeThread: ReturnType<typeof vi.fn>
} {
const events: string[] = []
const createProcessAsUserWImpl: Win32ProcessBindings['createProcessAsUserW'] =
overrides.createProcessAsUserW
?? ((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
events.push('create')
koffi.encode(info, PROCESS_INFORMATION, {
hProcess: 60n,
hThread: 61n,
dwProcessId: 1234,
dwThreadId: 5678,
})
return 1
})
const createProcessAsUserW = vi.fn(createProcessAsUserWImpl)
const assignProcessToJobObject = vi.fn(() => {
events.push('assign')
return 1
})
const resumeThread = vi.fn(() => {
events.push('resume')
return 1
})
const api = {
createJobObjectW: vi.fn(() => 50n),
setInformationJobObject: vi.fn(() => 1),
getStdHandle: vi.fn((selector: number) => BigInt(100 - selector)),
setHandleInformation: vi.fn((_handle: NativePtr, _mask: number, flags: number) => {
events.push(flags === 0 ? 'restore' : 'inherit')
return 1
}),
assignProcessToJobObject,
resumeThread,
terminateProcess: vi.fn(() => 1),
closeHandle: vi.fn((handle: NativePtr) => { events.push(`close:${handle}`); return 1 }),
getLastError: vi.fn(() => 5),
formatMessageW: vi.fn(() => 0),
...overrides,
createProcessAsUserW,
} as unknown as Win32ProcessBindings
return {
api,
events,
createProcessAsUserW,
assignProcessToJobObject,
resumeThread,
}
}
describe('spawnInheritedJobProcess', () => {
const token = 70n as NativePtr
it('creates suspended, assigns the Job, then resumes the restricted child', () => {
const {
api,
events,
createProcessAsUserW,
assignProcessToJobObject,
resumeThread,
} = inheritedApi()
const child = spawnInheritedJobProcess(api, {
command: 'cmd.exe',
args: ['/c', 'exit', '0'],
cwd: 'C:\\work',
token,
})
expect(child).toEqual({ pid: 1234, process: 60n, job: 50n })
expect(events.indexOf('create')).toBeLessThan(events.indexOf('assign'))
expect(events.indexOf('assign')).toBeLessThan(events.indexOf('resume'))
expect(assignProcessToJobObject).toHaveBeenCalledWith(50n, 60n)
expect(resumeThread).toHaveBeenCalledWith(61n)
expect(createProcessAsUserW).toHaveBeenCalledWith(
token,
null,
'cmd.exe /c exit 0',
null,
null,
1,
CREATE_SUSPENDED,
null,
'C:\\work',
expect.anything(),
expect.anything(),
)
})
it('restores already-enabled stdio and closes the Job when inheritance setup fails', () => {
let calls = 0
const closeHandle = vi.fn((_handle: NativePtr) => 1)
const setHandleInformation = vi.fn((_handle: NativePtr, _mask: number, flags: number) => {
if (flags === 0) return 1
calls += 1
return calls === 2 ? 0 : 1
})
const { api } = inheritedApi({ closeHandle, setHandleInformation })
expect(() => spawnInheritedJobProcess(api, {
command: 'cmd.exe',
args: [],
cwd: 'C:\\work',
token,
})).toThrow(Win32Error)
expect(setHandleInformation).toHaveBeenCalledWith(expect.anything(), 1, 0)
expect(closeHandle).toHaveBeenCalledWith(50n)
})
it('captures a GetStdHandle error before Job cleanup changes last-error', () => {
let lastError = 123
const { api } = inheritedApi({
getStdHandle: vi.fn(() => 0n as NativePtr),
getLastError: vi.fn(() => lastError),
closeHandle: vi.fn(() => { lastError = 999; return 1 }),
})
let caught: unknown
try {
spawnInheritedJobProcess(api, { command: 'cmd.exe', args: [], cwd: 'C:\\work', token })
} catch (error) {
caught = error
}
expect(caught).toMatchObject({ api: 'GetStdHandle', win32Code: 123 })
})
it('captures a CreateProcess error before inheritance restoration changes last-error', () => {
let lastError = 87
const { api } = inheritedApi({
createProcessAsUserW: vi.fn(() => 0),
getLastError: vi.fn(() => lastError),
setHandleInformation: vi.fn((_handle, _mask, flags) => {
if (flags === 0) lastError = 999
return 1
}),
closeHandle: vi.fn(() => { lastError = 998; return 1 }),
})
let caught: unknown
try {
spawnInheritedJobProcess(api, { command: 'cmd.exe', args: [], cwd: 'C:\\work', token })
} catch (error) {
caught = error
}
expect(caught).toMatchObject({ api: 'CreateProcessAsUserW', win32Code: 87 })
})
it('terminates the suspended process and closes the Job when CreateProcessAsUserW returns a null thread handle', () => {
const closeHandle = vi.fn((_handle: NativePtr) => 1)
const terminateProcess = vi.fn(() => 1)
const { api } = inheritedApi({
closeHandle,
terminateProcess,
createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
koffi.encode(info, PROCESS_INFORMATION, {
hProcess: 60n,
hThread: 0n,
dwProcessId: 1234,
dwThreadId: 0,
})
return 1
}),
})
expect(() => spawnInheritedJobProcess(api, {
command: 'cmd.exe',
args: [],
cwd: 'C:\\work',
token,
})).toThrow('null process/thread handles')
expect(terminateProcess).toHaveBeenCalledWith(60n, 1)
expect(closeHandle).toHaveBeenCalledWith(50n)
expect(closeHandle).toHaveBeenCalledWith(60n)
})
})
describe('wait and pipe cleanup', () => {
const token = 70n as NativePtr
it('waits when a pipe is temporarily empty before observing EOF', async () => {
const closeHandle = vi.fn(() => 1)
let peeks = 0
const api = {
peekNamedPipe: vi.fn((_handle, _buffer, _size, _read, available) => {
peeks += 1
if (peeks === 1) {
koffi.encode(available, 'uint32', 0)
return 1
}
return 0
}),
getLastError: vi.fn(() => 109),
closeHandle,
} as unknown as Win32ProcessBindings
await expect(drainPipe(api, 80n as NativePtr)).resolves.toEqual(Buffer.alloc(0))
expect(closeHandle).toHaveBeenCalledWith(80n)
})
it('terminates a piped child when CreateProcess returns a null thread handle', () => {
let nextPipe = 10n
const terminateProcess = vi.fn(() => 1)
const closeHandle = vi.fn(() => 1)
const api = {
createPipe: vi.fn((readSlot, writeSlot) => {
koffi.encode(readSlot, PVOID, nextPipe++)
koffi.encode(writeSlot, PVOID, nextPipe++)
return 1
}),
setHandleInformation: vi.fn(() => 1),
createProcessAsUserW: vi.fn((_token, _app, _line, _pa, _ta, _inherit, _flags, _env, _cwd, _startup, info) => {
koffi.encode(info, PROCESS_INFORMATION, {
hProcess: 60n,
hThread: 0n,
dwProcessId: 1234,
dwThreadId: 0,
})
return 1
}),
terminateProcess,
closeHandle,
} as unknown as Win32ProcessBindings
expect(() => spawnPipedProcess(api, {
command: 'cmd.exe',
args: [],
cwd: 'C:\\work',
token,
})).toThrow('null process/thread handles')
expect(terminateProcess).toHaveBeenCalledWith(60n, 1)
})
})