mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
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>
136 lines
4.5 KiB
TypeScript
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()
|
|
}
|
|
})
|