import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { EventEmitter } from 'events'; const { ProcessGuard, FRESH_DEFAULTS, RESUME_DEFAULTS } = require('../../engine/process-guard'); /** 构造一个假的 ChildProcess */ function createMockProc() { const proc = new EventEmitter(); proc.killed = false; proc.kill = vi.fn((signal) => { if (signal === 'SIGKILL') proc.killed = true; }); proc.pid = 12345; return proc; } describe('ProcessGuard', () => { beforeEach(() => vi.useFakeTimers()); afterEach(() => vi.useRealTimers()); describe('Overall Timeout', () => { it('should kill process when overall timeout is reached', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 5000 }); const timeoutPromise = new Promise((resolve) => { guard.on('timeout', resolve); }); await vi.advanceTimersByTimeAsync(5000); const event = await timeoutPromise; expect(event.reason).toBe('overall-timeout'); expect(guard.wasKilled).toBe(true); expect(proc.kill).toHaveBeenCalledWith('SIGTERM'); }); it('should not trigger if cancelled before timeout', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 5000 }); await vi.advanceTimersByTimeAsync(3000); guard.cancel(); await vi.advanceTimersByTimeAsync(5000); expect(guard.wasKilled).toBe(false); expect(proc.kill).not.toHaveBeenCalled(); }); }); describe('Watchdog (No-Output) Timeout', () => { it('should kill process when no output for watchdog duration', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, noOutputTimeoutMs: 3000, }); const timeoutPromise = new Promise((resolve) => { guard.on('timeout', resolve); }); await vi.advanceTimersByTimeAsync(3000); const event = await timeoutPromise; expect(event.reason).toBe('no-output-timeout'); expect(guard.wasKilled).toBe(true); }); it('should reset watchdog on touch()', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, noOutputTimeoutMs: 3000, }); // 2s 后 touch → 从这里重新倒计 3s await vi.advanceTimersByTimeAsync(2000); guard.touch(); // 再过 2s(总共 4s)→ 还没超时 await vi.advanceTimersByTimeAsync(2000); expect(guard.wasKilled).toBe(false); // 再过 1.5s(总共 5.5s, touch 后 3.5s)→ 超时 const timeoutPromise = new Promise((resolve) => { guard.on('timeout', resolve); }); await vi.advanceTimersByTimeAsync(1500); const event = await timeoutPromise; expect(event.reason).toBe('no-output-timeout'); }); it('should not trigger watchdog if continuously touched', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, noOutputTimeoutMs: 3000, }); // 模拟每秒收到输出 for (let i = 0; i < 10; i++) { await vi.advanceTimersByTimeAsync(1000); guard.touch(); } expect(guard.wasKilled).toBe(false); guard.cancel(); }); }); describe('Watchdog Profile Computation', () => { it('should use FRESH_DEFAULTS for new sessions', () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, isResume: false, }); // 600k * 0.8 = 480k, clamped by max 600k → 480k expect(guard.noOutputTimeoutMs).toBe(480_000); guard.cancel(); }); it('should use RESUME_DEFAULTS for resume sessions', () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, isResume: true, }); // 600k * 0.3 = 180k, clamped [60k, 180k] → 180k expect(guard.noOutputTimeoutMs).toBe(180_000); guard.cancel(); }); it('should clamp to minMs for short overall timeout', () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 60_000, isResume: false, }); // 60k * 0.8 = 48k, below minMs 180k → 180k expect(guard.noOutputTimeoutMs).toBe(180_000); guard.cancel(); }); it('should allow explicit noOutputTimeoutMs override', () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, noOutputTimeoutMs: 10_000, }); expect(guard.noOutputTimeoutMs).toBe(10_000); guard.cancel(); }); }); describe('Process Kill Sequence', () => { it('should SIGTERM first, then SIGKILL after 2s', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 1000, }); await vi.advanceTimersByTimeAsync(1000); // SIGTERM 已发送 expect(proc.kill).toHaveBeenCalledWith('SIGTERM'); expect(proc.kill).not.toHaveBeenCalledWith('SIGKILL'); // 2s 后 SIGKILL await vi.advanceTimersByTimeAsync(2000); expect(proc.kill).toHaveBeenCalledWith('SIGKILL'); }); }); describe('Cleanup on Process Exit', () => { it('should clear all timers when process closes', async () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000, noOutputTimeoutMs: 3000, }); // 进程提前退出 proc.emit('close', 0); // 推进超过所有超时 → 不应触发任何 kill await vi.advanceTimersByTimeAsync(700_000); expect(guard.wasKilled).toBe(false); expect(proc.kill).not.toHaveBeenCalled(); }); }); describe('Manual Terminate', () => { it('should allow manual termination', () => { const proc = createMockProc(); const guard = new ProcessGuard(proc, { overallTimeoutMs: 600_000 }); guard.terminate(); expect(guard.wasKilled).toBe(true); expect(guard.terminationReason).toBe('manual-cancel'); expect(proc.kill).toHaveBeenCalledWith('SIGTERM'); }); }); });