Files
ccs/tests/unit/utils/process-utils.test.ts
T
Tam Nhu Tran dc9b27623b test(utils): add unit tests for killWithEscalation
- Add 7 test cases covering SIGTERM/SIGKILL escalation, timer cleanup,
  default and custom grace periods, and already-exited process edge case
- Add timer.unref() to prevent keeping event loop alive during shutdown
- Add comment explaining 10s grace period in headless-executor timeout
2026-02-11 02:25:10 +07:00

133 lines
3.9 KiB
TypeScript

/**
* Unit tests for process-utils.ts
*/
import { describe, it, expect, beforeEach, afterEach, 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;
}
describe('killWithEscalation', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
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', () => {
const proc = createMockProcess(null); // exitCode null = still running
killWithEscalation(proc, 3000);
// SIGTERM sent immediately
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
// Advance time by grace period
jest.advanceTimersByTime(3000);
// 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', () => {
const proc = createMockProcess(null);
killWithEscalation(proc, 3000);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
expect(proc.kill).toHaveBeenCalledTimes(1);
// Simulate process exit after 1 second
jest.advanceTimersByTime(1000);
proc.exitCode = 0; // Process exited
proc.emit('exit', 0);
// Advance remaining time
jest.advanceTimersByTime(2000);
// 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);
killWithEscalation(proc); // No grace period argument
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
// Advance by default 3000ms
jest.advanceTimersByTime(3000);
expect(proc.kill).toHaveBeenCalledWith('SIGKILL');
});
it('should respect custom grace period', () => {
const proc = createMockProcess(null);
killWithEscalation(proc, 5000); // Custom 5 second grace period
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
// Advance by less than grace period
jest.advanceTimersByTime(4999);
expect(proc.kill).toHaveBeenCalledTimes(1); // Still only SIGTERM
// Advance to grace period
jest.advanceTimersByTime(1);
expect(proc.kill).toHaveBeenCalledWith('SIGKILL');
});
it('should clear timer when process exits', () => {
const proc = createMockProcess(null);
killWithEscalation(proc, 3000);
// Simulate immediate exit
proc.exitCode = 0;
proc.emit('exit', 0);
// Advance way past grace period
jest.advanceTimersByTime(10000);
// Should only have SIGTERM, timer was cleared
expect(proc.kill).toHaveBeenCalledTimes(1);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
});
it('should handle process that already exited', () => {
const proc = createMockProcess(0); // Already exited
killWithEscalation(proc, 3000);
expect(proc.kill).toHaveBeenCalledWith('SIGTERM');
// Even though exitCode is not null, timer still fires
// (because we check exitCode at timer callback time)
jest.advanceTimersByTime(3000);
// SIGKILL should NOT be sent because exitCode is not null
expect(proc.kill).toHaveBeenCalledTimes(1);
});
});