import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const { IdleTimer } = require('../../engine/idle-timer'); describe('IdleTimer', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it('should fire callback after timeout with no touch', () => { const onTimeout = vi.fn(); const timer = new IdleTimer(1000, onTimeout); timer.start(); vi.advanceTimersByTime(999); expect(onTimeout).not.toHaveBeenCalled(); vi.advanceTimersByTime(1); expect(onTimeout).toHaveBeenCalledTimes(1); }); it('should reset on touch and delay timeout', () => { const onTimeout = vi.fn(); const timer = new IdleTimer(1000, onTimeout); timer.start(); // 800ms 过去,touch vi.advanceTimersByTime(800); timer.touch(); // 再过 800ms(距 touch 只有 800ms) vi.advanceTimersByTime(800); expect(onTimeout).not.toHaveBeenCalled(); // 再过 200ms(距 touch 已有 1000ms) vi.advanceTimersByTime(200); expect(onTimeout).toHaveBeenCalledTimes(1); }); it('should not fire after cancel', () => { const onTimeout = vi.fn(); const timer = new IdleTimer(1000, onTimeout); timer.start(); vi.advanceTimersByTime(500); timer.cancel(); vi.advanceTimersByTime(1000); expect(onTimeout).not.toHaveBeenCalled(); }); it('should not re-fire touch after already fired', () => { const onTimeout = vi.fn(); const timer = new IdleTimer(1000, onTimeout); timer.start(); vi.advanceTimersByTime(1000); expect(timer.fired).toBe(true); // touch after fired should not create new timer timer.touch(); vi.advanceTimersByTime(1000); expect(onTimeout).toHaveBeenCalledTimes(1); // 只触发一次 }); it('should support multiple touch calls', () => { const onTimeout = vi.fn(); const timer = new IdleTimer(500, onTimeout); timer.start(); // 模拟持续活动:每 200ms touch for (let i = 0; i < 10; i++) { vi.advanceTimersByTime(200); timer.touch(); } // 总共 2000ms 但始终有 touch,不应触发 expect(onTimeout).not.toHaveBeenCalled(); // 停止 touch,等待超时 vi.advanceTimersByTime(500); expect(onTimeout).toHaveBeenCalledTimes(1); }); it('should expose label', () => { const timer = new IdleTimer(1000, () => { }, { label: 'test-timer' }); expect(timer.label).toBe('test-timer'); }); }); describe('ACP Idle Timeout (integration)', () => { const { EventEmitter } = require('events'); beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); /** * 模拟 _runPromptInReusableSession 的核心逻辑 * 使用 mock protocol 测试 idle timeout 行为 */ function simulateAcpSession(protocol, idleTimeoutMs = 1000) { return new Promise((resolve, reject) => { let stdout = ''; let settled = false; const idleTimer = new IdleTimer(idleTimeoutMs, () => { if (settled) return; settled = true; idleTimer.cancel(); protocol.removeAllListeners('session_update'); const err = new Error(`Idle timeout: no data for ${idleTimeoutMs}ms`); err.code = 'IDLE_TIMEOUT'; reject(err); }); const onSessionUpdate = (params) => { idleTimer.touch(); const text = params.update?.content?.text || ''; stdout += text; }; protocol.on('session_update', onSessionUpdate); idleTimer.start(); protocol.sendPrompt('test').then( () => { if (settled) return; settled = true; idleTimer.cancel(); protocol.off('session_update', onSessionUpdate); resolve(stdout); }, (err) => { if (settled) return; settled = true; idleTimer.cancel(); protocol.off('session_update', onSessionUpdate); reject(err); } ); }); } it('should not timeout when stream data flows continuously', async () => { const protocol = new EventEmitter(); // mock sendPrompt: 发送 5 个 chunk 每隔 500ms,然后 resolve protocol.sendPrompt = () => new Promise((resolve) => { let count = 0; const interval = setInterval(() => { count++; protocol.emit('session_update', { update: { sessionUpdate: 'agent_message_chunk', content: { text: `chunk${count} ` }, }, }); if (count >= 5) { clearInterval(interval); resolve(); } }, 500); }); const promise = simulateAcpSession(protocol, 1000); // 模拟时间流逝:5 个 chunk × 500ms = 2500ms for (let i = 0; i < 5; i++) { vi.advanceTimersByTime(500); // 让 microtask queue 处理 await vi.advanceTimersByTimeAsync(0); } const result = await promise; expect(result).toBe('chunk1 chunk2 chunk3 chunk4 chunk5 '); }); it('should timeout when stream stops sending data', async () => { const protocol = new EventEmitter(); // mock sendPrompt: 发送 2 个 chunk 然后停止(不 resolve) protocol.sendPrompt = () => new Promise((resolve) => { let count = 0; const interval = setInterval(() => { count++; protocol.emit('session_update', { update: { sessionUpdate: 'agent_message_chunk', content: { text: `chunk${count} ` }, }, }); if (count >= 2) { clearInterval(interval); // 故意不 resolve — 模拟挂死 } }, 300); }); const promise = simulateAcpSession(protocol, 1000); // 先挂上 catch 防止 unhandled rejection const caughtPromise = promise.catch(err => err); // 两个 chunk: 300ms + 300ms await vi.advanceTimersByTimeAsync(300); await vi.advanceTimersByTimeAsync(300); // 现在停止推送,等待 idle timeout (1000ms) await vi.advanceTimersByTimeAsync(1000); const err = await caughtPromise; expect(err).toBeInstanceOf(Error); expect(err.message).toContain('Idle timeout'); }); it('should set IDLE_TIMEOUT error code', async () => { const protocol = new EventEmitter(); // sendPrompt 永远不 resolve protocol.sendPrompt = () => new Promise(() => { }); const promise = simulateAcpSession(protocol, 500); // 先挂上 catch 防止 unhandled rejection const caughtPromise = promise.catch(err => err); await vi.advanceTimersByTimeAsync(500); const err = await caughtPromise; expect(err).toBeInstanceOf(Error); expect(err.code).toBe('IDLE_TIMEOUT'); }); });