Files
orca/config/scripts/vitest-node-runtime-pool.test.ts
T
Max HsuandNeil c915be1a22 fix(test): carry Error objects across the node-runtime vitest pool IPC (#30079)
devalue rejects Error objects, so when a NODE_RUNTIME_INCLUDE test file
failed to load, the pool's send() threw DevalueError and the run ended
without a summary. Give both ends of the IPC channel one codec with a
matching Error reducer/reviver (name, message, stack, cause) so the load
failure reaches the reporter as a normal FAIL.

Fixes #29854

Co-authored-by: Neil <neil@stably.ai>
2026-10-26 22:25:16 -07:00

136 lines
4.5 KiB
TypeScript

import { once } from 'node:events'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { PassThrough } from 'node:stream'
import { afterAll, afterEach, beforeAll, expect, it, onTestFinished, vi } from 'vitest'
import { createVitest, type PoolOptions, type Vitest } from 'vitest/node'
import * as processes from '@orca/process-host'
import type { ChildProcessHandle } from '@orca/process-host/process-spec'
import { nodeRuntimePool } from './vitest-node-runtime-pool'
let context: Vitest
const children: ChildProcessHandle[] = []
const spawn = processes.spawnProcess
beforeAll(async () => {
context = await createVitest('test', { config: false, watch: false, include: [] })
})
afterEach(async () => {
vi.useRealTimers()
vi.restoreAllMocks()
for (const child of children.splice(0)) {
if (child.pid && child.exitCode === null && child.signalCode === null) {
const exited = once(child, 'exit')
child.kill('SIGKILL')
await exited
}
}
})
afterAll(async () => {
await context.close()
})
function options(executable = process.execPath): PoolOptions {
return {
distPath: '',
project: context.getRootProject(),
method: 'run',
environment: { name: 'node', options: null },
execArgv: [],
env: { ...process.env, ORCA_TEST_NODE_EXECUTABLE: executable }
}
}
function captureChild(): void {
vi.spyOn(processes, 'spawnProcess').mockImplementation((spec) => {
const child = spawn(spec)
children.push(child)
return child
})
}
it('rejects an unavailable Node executable without an uncaught error or leaked pipes', async () => {
const output = context.logger.outputStream
const before = output.getMaxListeners()
const worker = nodeRuntimePool.createPoolWorker(options(resolve('missing-node-executable')))
await expect(worker.start()).rejects.toMatchObject({ code: 'ENOENT' })
await worker.stop()
expect(output.getMaxListeners()).toBe(before)
})
it('clears shutdown timers and listeners when the kill syscall throws', async () => {
captureChild()
const worker = nodeRuntimePool.createPoolWorker(options())
await worker.start()
const child = children[0]
vi.spyOn(child, 'kill').mockImplementation(() => {
throw new Error('kill denied')
})
vi.useFakeTimers()
await expect(worker.stop()).rejects.toThrow('kill denied')
expect(vi.getTimerCount()).toBe(0)
expect(child.listenerCount('exit')).toBe(0)
})
it('waits for a running Node worker to exit and restores its output listener budget', async () => {
captureChild()
const output = context.logger.outputStream
const before = output.getMaxListeners()
const worker = nodeRuntimePool.createPoolWorker(options())
await worker.start()
await worker.stop()
expect(children[0].signalCode).toBe('SIGTERM')
expect(output.getMaxListeners()).toBe(before)
})
it('reports a failed kill within its deadline rather than hanging teardown', async () => {
captureChild()
const worker = nodeRuntimePool.createPoolWorker(options())
const output = context.logger.outputStream
const before = output.getMaxListeners()
await worker.start()
const child = children[0]
vi.spyOn(child, 'kill').mockReturnValue(false)
vi.useFakeTimers()
const stopped = expect(worker.stop()).rejects.toThrow('did not exit after SIGKILL')
await vi.advanceTimersByTimeAsync(2_000)
await stopped
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
expect(child.listenerCount('exit')).toBe(0)
expect(output.getMaxListeners()).toBe(before)
})
it('reports a test file that fails to load as a failed file instead of crashing the run', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-node-runtime-pool-'))
onTestFinished(() => rm(dir, { recursive: true, force: true }))
await writeFile(join(dir, 'load-failure.test.ts'), "import './orca-missing-module'\n")
const nested = await createVitest(
'test',
{
config: false,
watch: false,
dir,
include: ['**/*.test.ts'],
pool: 'node-runtime',
poolRunner: nodeRuntimePool,
reporters: [],
env: { ORCA_TEST_NODE_EXECUTABLE: process.execPath }
},
{},
{ stdout: new PassThrough(), stderr: new PassThrough() }
)
try {
await nested.start()
const [file] = nested.state.getFiles()
expect(file.result?.state).toBe('fail')
expect(file.result?.errors?.[0]?.message).toContain('orca-missing-module')
expect(nested.state.getUnhandledErrors()).toEqual([])
} finally {
await nested.close()
}
})