Files
ccs/tests/unit/utils/process-utils.test.ts
T

166 lines
4.9 KiB
TypeScript

/**
* Unit tests for process-utils.ts
*/
import { describe, it, expect, jest } from 'bun:test';
import { EventEmitter } from 'events';
import { killWithEscalation } from '../../../src/utils/process-utils';
import type { ChildProcess } from 'child_process';
// Mock ChildProcess using EventEmitter
function createMockProcess(exitCode: number | null = null): ChildProcess {
const proc = new EventEmitter() as any;
proc.killed = false;
proc.exitCode = exitCode;
proc.kill = jest.fn((signal?: string) => {
if (signal === 'SIGTERM' || signal === 'SIGKILL') {
proc.killed = true;
}
return true;
});
return proc as ChildProcess;
}
function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
describe('killWithEscalation', () => {
it('should send SIGTERM immediately', () => {
const proc = createMockProcess();
killWithEscalation(proc);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
});
it('should send SIGKILL after grace period if process still running', async () => {
const proc = createMockProcess(null); // exitCode null = still running
killWithEscalation(proc, 10);
// SIGTERM sent immediately
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
await wait(40);
// SIGKILL sent after grace period
expect(proc.kill).toHaveBeenCalledWith('SIGKILL');
expect(proc.kill).toHaveBeenCalledTimes(2);
});
it('should NOT send SIGKILL if process exits before grace period', async () => {
const proc = createMockProcess(null);
killWithEscalation(proc, 40);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
// Simulate process exit before grace timeout
await wait(10);
proc.exitCode = 0;
proc.emit('exit', 0);
await wait(60);
// SIGKILL should NOT have been sent
expect(proc.kill).toHaveBeenCalledTimes(1);
expect(proc.kill).not.toHaveBeenCalledWith('SIGKILL');
});
it('should use default grace period of 3000ms', () => {
const proc = createMockProcess(null);
const originalSetTimeout = globalThis.setTimeout;
const fakeTimer = {
unref: () => fakeTimer,
ref: () => fakeTimer,
hasRef: () => false,
refresh: () => fakeTimer,
} as unknown as ReturnType<typeof setTimeout>;
let observedDelay: number | undefined;
globalThis.setTimeout = ((handler: TimerHandler, timeout?: number) => {
observedDelay = timeout;
void handler; // avoid executing callback in this assertion-only test
return fakeTimer;
}) as typeof globalThis.setTimeout;
try {
killWithEscalation(proc);
expect(observedDelay).toBe(3000);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});
it('should respect custom grace period', () => {
const proc = createMockProcess(null);
const originalSetTimeout = globalThis.setTimeout;
const fakeTimer = {
unref: () => fakeTimer,
ref: () => fakeTimer,
hasRef: () => false,
refresh: () => fakeTimer,
} as unknown as ReturnType<typeof setTimeout>;
let observedDelay: number | undefined;
globalThis.setTimeout = ((handler: TimerHandler, timeout?: number) => {
observedDelay = timeout;
void handler;
return fakeTimer;
}) as typeof globalThis.setTimeout;
try {
killWithEscalation(proc, 5000);
expect(observedDelay).toBe(5000);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
} finally {
globalThis.setTimeout = originalSetTimeout;
}
});
it('should clear timer when process exits', async () => {
const proc = createMockProcess(null);
const originalClearTimeout = globalThis.clearTimeout;
let clearCalled = false;
globalThis.clearTimeout = ((id: ReturnType<typeof setTimeout>) => {
clearCalled = true;
return originalClearTimeout(id);
}) as typeof globalThis.clearTimeout;
try {
killWithEscalation(proc, 50);
// Simulate immediate exit
proc.exitCode = 0;
proc.emit('exit', 0);
await wait(70);
// Should only have SIGTERM, timer was cleared
expect(clearCalled).toBe(true);
expect(proc.kill).toHaveBeenCalledTimes(1);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
} finally {
globalThis.clearTimeout = originalClearTimeout;
}
});
it('should handle process that already exited', async () => {
const proc = createMockProcess(0); // Already exited
killWithEscalation(proc, 10);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
await wait(30);
// SIGKILL should NOT be sent because exitCode is not null
expect(proc.kill).toHaveBeenCalledTimes(1);
});
});