| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- /**
- * Failure-path unit tests with minimal stub binding tables: the spawn
- * helpers must close every handle they created before throwing, and
- * every generic process failure remains owned by the shared package.
- * Pure stubs — no real Win32 calls, so these run on every platform.
- */
- import { describe, expect, it, vi } from 'vitest'
- import koffi from 'koffi'
- import {
- Win32Error,
- drainPipe,
- spawnInheritedJobProcess,
- spawnPipedProcess,
- waitForProcessExit,
- } from '../src/index.ts'
- import type { NativePtr, Win32ProcessBindings } from '../src/index.ts'
- import * as abi from '../src/abi.ts'
- import { PROCESS_INFORMATION } from '../src/ffi.ts'
- const PVOID = koffi.pointer('void')
- /** The stub the CreateProcessAsUserW failure branch needs: pipes "succeed", the spawn fails with Win32 5. */
- function pipeFailureApi(): { api: Win32ProcessBindings; closed: bigint[]; closeHandle: ReturnType<typeof vi.fn> } {
- const closed: bigint[] = []
- let next = 1n
- const closeHandle = vi.fn((handle: NativePtr) => {
- closed.push(handle)
- return 1
- })
- const api = {
- createPipe: vi.fn((readSlot: NativePtr, writeSlot: NativePtr) => {
- koffi.encode(readSlot, PVOID, next++)
- koffi.encode(writeSlot, PVOID, next++)
- return 1
- }),
- setHandleInformation: vi.fn(() => 1),
- createProcessAsUserW: vi.fn(() => 0),
- getLastError: vi.fn(() => 5), // ERROR_ACCESS_DENIED: the failure the branch reports
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- return { api, closed, closeHandle }
- }
- describe('spawn failure paths close their handles', () => {
- // A dummy token value; the stubbed spawn never reads it.
- const token = 1n as NativePtr
- it('closes all six pipe handles before throwing when CreateProcessAsUserW fails', () => {
- const { api, closed, closeHandle } = pipeFailureApi()
- let caught: unknown
- try {
- spawnPipedProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('CreateProcessAsUserW')
- expect((caught as Win32Error).win32Code).toBe(5)
- expect(closeHandle).toHaveBeenCalledTimes(6)
- expect(closed).toEqual([1n, 2n, 3n, 4n, 5n, 6n])
- })
- })
- /** The stub the pipe-happy path needs: CreatePipe fills both out slots with fresh handles. */
- function pipeOkApi(overrides: Partial<Win32ProcessBindings> = {}): {
- api: Win32ProcessBindings
- closed: bigint[]
- closeHandle: ReturnType<typeof vi.fn>
- } {
- const closed: bigint[] = []
- let next = 1n
- const closeHandle = vi.fn((handle: NativePtr) => {
- closed.push(handle)
- return 1
- })
- const api = {
- createPipe: vi.fn((readSlot: NativePtr, writeSlot: NativePtr) => {
- koffi.encode(readSlot, PVOID, next++)
- koffi.encode(writeSlot, PVOID, next++)
- return 1
- }),
- setHandleInformation: vi.fn(() => 1),
- createProcessAsUserW: vi.fn((
- _token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
- _inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
- ) => {
- koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: 200n, hThread: 201n, dwProcessId: 1234, dwThreadId: 5678 })
- return 1
- }),
- getLastError: vi.fn(() => 5),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- ...overrides,
- } as unknown as Win32ProcessBindings
- return { api, closed, closeHandle }
- }
- describe('spawn pipe failures close their handles', () => {
- const token = 1n as NativePtr
- it('reports a CreatePipe failure', () => {
- const api = {
- createPipe: vi.fn(() => 0),
- getLastError: vi.fn(() => 5),
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- let caught: unknown
- try {
- spawnPipedProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('CreatePipe')
- })
- it('reports a NULL pipe handle after CreatePipe succeeds', () => {
- const api = {
- createPipe: vi.fn(() => 1),
- getLastError: vi.fn(() => 5),
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- let caught: unknown
- try {
- spawnPipedProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('CreatePipe')
- })
- it('reports a SetHandleInformation failure', () => {
- const { api } = pipeOkApi({ setHandleInformation: vi.fn(() => 0) })
- let caught: unknown
- try {
- spawnPipedProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('SetHandleInformation')
- })
- it('rejects NULL process/thread handles after a successful spawn', () => {
- const { api } = pipeOkApi({
- createProcessAsUserW: vi.fn((
- _token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
- _inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
- ) => {
- koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: null, hThread: null, dwProcessId: 1234, dwThreadId: 5678 })
- return 1
- }),
- })
- expect(() => spawnPipedProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token }))
- .toThrow(/null process\/thread handles/u)
- })
- })
- describe('spawnInheritedJobProcess failure paths', () => {
- const token = 1n as NativePtr
- /** The stub the inherited-happy path needs; overrides flip one call per test. */
- function inheritedApi(overrides: Partial<Win32ProcessBindings> = {}): {
- api: Win32ProcessBindings
- closed: bigint[]
- closeHandle: ReturnType<typeof vi.fn>
- } {
- const closed: bigint[] = []
- let std = 50n
- const closeHandle = vi.fn((handle: NativePtr) => {
- closed.push(handle)
- return 1
- })
- const api = {
- createJobObjectW: vi.fn(() => 100n),
- setInformationJobObject: vi.fn(() => 1),
- getStdHandle: vi.fn(() => std++),
- setHandleInformation: vi.fn(() => 1),
- createProcessAsUserW: vi.fn((
- _token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
- _inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
- ) => {
- koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: 200n, hThread: 201n, dwProcessId: 1234, dwThreadId: 5678 })
- return 1
- }),
- assignProcessToJobObject: vi.fn(() => 1),
- resumeThread: vi.fn(() => 0),
- terminateProcess: vi.fn(() => 1),
- getLastError: vi.fn(() => 5),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- ...overrides,
- } as unknown as Win32ProcessBindings
- return { api, closed, closeHandle }
- }
- it('closes the job and reports when GetStdHandle yields a NULL handle', () => {
- const { api, closeHandle } = inheritedApi({ getStdHandle: vi.fn(() => 0n as NativePtr) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('GetStdHandle')
- expect(closeHandle).toHaveBeenCalledWith(100n)
- })
- it('reports a SetHandleInformation failure while enabling stdio inheritance', () => {
- const { api } = inheritedApi({ setHandleInformation: vi.fn(() => 0) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('SetHandleInformation')
- })
- it('closes the job and reports when CreateProcessAsUserW fails', () => {
- const { api, closeHandle } = inheritedApi({ createProcessAsUserW: vi.fn(() => 0) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('CreateProcessAsUserW')
- expect(closeHandle).toHaveBeenCalledWith(100n)
- })
- it('closes the job and rejects NULL process/thread handles after a successful spawn', () => {
- const { api, closeHandle } = inheritedApi({
- createProcessAsUserW: vi.fn((
- _token: unknown, _app: unknown, _cmd: unknown, _pa: unknown, _ta: unknown,
- _inherit: unknown, _flags: unknown, _env: unknown, _cwd: unknown, _si: unknown, processInfo: NativePtr,
- ) => {
- koffi.encode(processInfo, PROCESS_INFORMATION, { hProcess: null, hThread: null, dwProcessId: 1234, dwThreadId: 5678 })
- return 1
- }),
- })
- expect(() => spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token }))
- .toThrow(/null process\/thread handles/u)
- expect(closeHandle).toHaveBeenCalledWith(100n)
- })
- it('terminates the suspended child before closing handles when Job assignment fails', () => {
- const terminateProcess = vi.fn(() => 1)
- const { api, closeHandle, closed } = inheritedApi({
- assignProcessToJobObject: vi.fn(() => 0),
- terminateProcess,
- })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toMatchObject({ api: 'AssignProcessToJobObject', win32Code: 5 })
- expect(terminateProcess).toHaveBeenCalledWith(200n, 1)
- expect(closeHandle).toHaveBeenCalledWith(201n)
- expect(closeHandle).toHaveBeenCalledWith(200n)
- expect(closeHandle).toHaveBeenCalledWith(100n)
- expect(closed).toEqual([201n, 200n, 100n])
- })
- it('closes the assigned child and Job when ResumeThread fails', () => {
- const { api, closeHandle, closed } = inheritedApi({ resumeThread: vi.fn(() => 0xFFFFFFFF) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toMatchObject({ api: 'ResumeThread', win32Code: 5 })
- expect(closeHandle).toHaveBeenCalledWith(201n)
- expect(closeHandle).toHaveBeenCalledWith(200n)
- expect(closeHandle).toHaveBeenCalledWith(100n)
- expect(closed).toEqual([201n, 200n, 100n])
- })
- it('closes the job and reports when SetInformationJobObject fails', () => {
- const { api, closeHandle } = inheritedApi({ setInformationJobObject: vi.fn(() => 0) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('SetInformationJobObject')
- expect(closeHandle).toHaveBeenCalledWith(100n)
- })
- it('closes the job and reports a NULL job object', () => {
- const { api } = inheritedApi({ createJobObjectW: vi.fn(() => 0n as NativePtr) })
- let caught: unknown
- try {
- spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- } catch (error) {
- caught = error
- }
- expect(caught).toBeInstanceOf(Win32Error)
- expect((caught as Win32Error).api).toBe('CreateJobObjectW')
- })
- it('returns the pid, process handle, and kill-on-close job when every call succeeds', () => {
- const assignProcessToJobObject = vi.fn(() => 1)
- const resumeThread = vi.fn(() => 0)
- const { api, closeHandle } = inheritedApi({ assignProcessToJobObject, resumeThread })
- const spawned = spawnInheritedJobProcess(api, { command: 'probe.exe', args: [], cwd: 'C:\\', token })
- expect(spawned.pid).toBe(1234)
- expect(spawned.process).toBe(200n)
- expect(spawned.job).toBe(100n)
- // thread handle closed by the spawn; process and job handles stay with the caller.
- expect(closeHandle).toHaveBeenCalledWith(201n)
- expect(closeHandle).not.toHaveBeenCalledWith(200n)
- expect(closeHandle).not.toHaveBeenCalledWith(100n)
- expect(assignProcessToJobObject).toHaveBeenCalledWith(100n, 200n)
- expect(resumeThread).toHaveBeenCalledWith(201n)
- })
- })
- describe('drainPipe', () => {
- it('stops at ERROR_NO_DATA and closes the read end', () => {
- const closeHandle = vi.fn(() => 1)
- const api = {
- peekNamedPipe: vi.fn(() => 0),
- getLastError: vi.fn(() => abi.ERROR_NO_DATA),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- return drainPipe(api, 30n as NativePtr).then((buffer) => {
- expect(buffer.length).toBe(0)
- expect(closeHandle).toHaveBeenCalledWith(30n)
- })
- })
- it('reports a PeekNamedPipe failure that is not a clean EOF', () => {
- const closeHandle = vi.fn(() => 1)
- const api = {
- peekNamedPipe: vi.fn(() => 0),
- getLastError: vi.fn(() => 5),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- return expect(drainPipe(api, 30n as NativePtr)).rejects.toMatchObject({ api: 'PeekNamedPipe' })
- .then(() => { expect(closeHandle).toHaveBeenCalledWith(30n) })
- })
- it('reports a ReadFile failure after data was reported available', () => {
- const api = {
- peekNamedPipe: vi.fn((_pipe: unknown, _buffer: unknown, _size: unknown, _read: unknown, totalAvail: NativePtr) => {
- koffi.encode(totalAvail, 'uint32', 4)
- return 1
- }),
- readFile: vi.fn(() => 0),
- getLastError: vi.fn(() => 5),
- closeHandle: vi.fn(() => 1),
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- return expect(drainPipe(api, 30n as NativePtr)).rejects.toMatchObject({ api: 'ReadFile' })
- })
- it('drains one chunk and stops at ERROR_BROKEN_PIPE', () => {
- let peeks = 0
- const api = {
- peekNamedPipe: vi.fn((_pipe: unknown, _buffer: unknown, _size: unknown, _read: unknown, totalAvail: NativePtr) => {
- peeks++
- if (peeks > 1) return 0
- koffi.encode(totalAvail, 'uint32', 4)
- return 1
- }),
- readFile: vi.fn((_file: unknown, chunk: Buffer, _count: unknown, read: NativePtr) => {
- chunk.write('ab', 0, 'utf8')
- koffi.encode(read, 'uint32', 2)
- return 1
- }),
- getLastError: vi.fn(() => abi.ERROR_BROKEN_PIPE),
- closeHandle: vi.fn(() => 1),
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- return drainPipe(api, 30n as NativePtr).then((buffer) => {
- expect(buffer.toString('utf8')).toBe('ab')
- })
- })
- })
- describe('waitForProcessExit', () => {
- it('reports a WaitForSingleObject failure', () => {
- const closeHandle = vi.fn(() => 1)
- const api = {
- waitForSingleObject: vi.fn(() => 0xFFFFFFFF),
- getLastError: vi.fn(() => 5),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- expect(() => waitForProcessExit(api, 200n as NativePtr)).toThrow(Win32Error)
- expect(closeHandle).toHaveBeenCalledWith(200n)
- })
- it('reports a GetExitCodeProcess failure', () => {
- const closeHandle = vi.fn(() => 1)
- const api = {
- waitForSingleObject: vi.fn(() => 0),
- getExitCodeProcess: vi.fn(() => 0),
- getLastError: vi.fn(() => 5),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- expect(() => waitForProcessExit(api, 200n as NativePtr)).toThrow(Win32Error)
- expect(closeHandle).toHaveBeenCalledWith(200n)
- })
- it('returns the exit code and closes the process handle', () => {
- const closeHandle = vi.fn(() => 1)
- const api = {
- waitForSingleObject: vi.fn(() => 0),
- getExitCodeProcess: vi.fn((_process: unknown, slot: NativePtr) => {
- koffi.encode(slot, 'uint32', 42)
- return 1
- }),
- closeHandle,
- formatMessageW: vi.fn(() => 0),
- } as unknown as Win32ProcessBindings
- expect(waitForProcessExit(api, 200n as NativePtr)).toBe(42)
- expect(closeHandle).toHaveBeenCalledWith(200n)
- })
- })
|