mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
155 lines
5.2 KiB
TypeScript
155 lines
5.2 KiB
TypeScript
import { describe, it, expect, jest } from 'bun:test';
|
|
import { EventEmitter } from 'events';
|
|
import type { ChildProcess } from 'child_process';
|
|
import { forwardSignals, wireChildProcessSignals } from '../../../src/utils/signal-forwarder';
|
|
|
|
type MockChildProcess = EventEmitter & {
|
|
killed: boolean;
|
|
kill: ChildProcess['kill'];
|
|
};
|
|
|
|
function createMockChildProcess(): ChildProcess {
|
|
const child = new EventEmitter() as MockChildProcess;
|
|
child.killed = false;
|
|
child.kill = jest.fn(() => true) as ChildProcess['kill'];
|
|
return child as ChildProcess;
|
|
}
|
|
|
|
function getSignalListenerCounts(): Record<'SIGINT' | 'SIGTERM' | 'SIGHUP', number> {
|
|
return {
|
|
SIGINT: process.listenerCount('SIGINT'),
|
|
SIGTERM: process.listenerCount('SIGTERM'),
|
|
SIGHUP: process.listenerCount('SIGHUP'),
|
|
};
|
|
}
|
|
|
|
describe('signal-forwarder', () => {
|
|
it('forwardSignals should register and cleanup listeners', () => {
|
|
const child = createMockChildProcess();
|
|
const before = getSignalListenerCounts();
|
|
|
|
const cleanup = forwardSignals(child);
|
|
|
|
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT + 1);
|
|
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM + 1);
|
|
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP + 1);
|
|
|
|
cleanup();
|
|
|
|
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
|
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
|
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
|
});
|
|
|
|
it('wireChildProcessSignals should use default exit behavior for exit code', () => {
|
|
const child = createMockChildProcess();
|
|
const before = getSignalListenerCounts();
|
|
|
|
const exitSpy = jest
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined as never) as typeof process.exit);
|
|
const killSpy = jest
|
|
.spyOn(process, 'kill')
|
|
.mockImplementation((() => true) as typeof process.kill);
|
|
|
|
try {
|
|
wireChildProcessSignals(child, () => {});
|
|
child.emit('exit', 7, null);
|
|
|
|
expect(exitSpy).toHaveBeenCalledWith(7);
|
|
expect(killSpy).not.toHaveBeenCalled();
|
|
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
|
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
|
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
|
} finally {
|
|
exitSpy.mockRestore();
|
|
killSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('wireChildProcessSignals should use default exit behavior for signal', () => {
|
|
const child = createMockChildProcess();
|
|
const before = getSignalListenerCounts();
|
|
|
|
const exitSpy = jest
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined as never) as typeof process.exit);
|
|
const killSpy = jest
|
|
.spyOn(process, 'kill')
|
|
.mockImplementation((() => true) as typeof process.kill);
|
|
|
|
try {
|
|
wireChildProcessSignals(child, () => {});
|
|
child.emit('exit', null, 'SIGTERM');
|
|
|
|
expect(killSpy).toHaveBeenCalledWith(process.pid, 'SIGTERM');
|
|
expect(exitSpy).not.toHaveBeenCalled();
|
|
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
|
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
|
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
|
} finally {
|
|
exitSpy.mockRestore();
|
|
killSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it('wireChildProcessSignals should invoke onError and cleanup listeners', async () => {
|
|
const child = createMockChildProcess();
|
|
const before = getSignalListenerCounts();
|
|
const onError = jest.fn(async () => {});
|
|
const err = Object.assign(new Error('spawn failed'), {
|
|
code: 'ENOENT',
|
|
}) as NodeJS.ErrnoException;
|
|
|
|
wireChildProcessSignals(child, onError);
|
|
child.emit('error', err);
|
|
await Promise.resolve();
|
|
|
|
expect(onError).toHaveBeenCalledWith(err);
|
|
expect(process.listenerCount('SIGINT')).toBe(before.SIGINT);
|
|
expect(process.listenerCount('SIGTERM')).toBe(before.SIGTERM);
|
|
expect(process.listenerCount('SIGHUP')).toBe(before.SIGHUP);
|
|
});
|
|
|
|
it('wireChildProcessSignals should run only one terminal callback when error is followed by exit', async () => {
|
|
const child = createMockChildProcess();
|
|
const onError = jest.fn(async () => {});
|
|
const onExit = jest.fn();
|
|
const err = Object.assign(new Error('spawn failed'), {
|
|
code: 'ENOENT',
|
|
}) as NodeJS.ErrnoException;
|
|
|
|
wireChildProcessSignals(child, onError, onExit);
|
|
child.emit('error', err);
|
|
child.emit('exit', 1, null);
|
|
await Promise.resolve();
|
|
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
expect(onExit).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('wireChildProcessSignals should exit with code 1 when onError throws', async () => {
|
|
const child = createMockChildProcess();
|
|
const onError = jest.fn(async () => {
|
|
throw new Error('handler exploded');
|
|
});
|
|
const exitSpy = jest
|
|
.spyOn(process, 'exit')
|
|
.mockImplementation((() => undefined as never) as typeof process.exit);
|
|
|
|
try {
|
|
const err = Object.assign(new Error('spawn failed'), {
|
|
code: 'ENOENT',
|
|
}) as NodeJS.ErrnoException;
|
|
wireChildProcessSignals(child, onError);
|
|
child.emit('error', err);
|
|
await Promise.resolve();
|
|
|
|
expect(onError).toHaveBeenCalledTimes(1);
|
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
} finally {
|
|
exitSpy.mockRestore();
|
|
}
|
|
});
|
|
});
|