/** * ClaudeCliProtocol · mock 模式 smoke test */ const { ClaudeCliProtocol } = require('./ClaudeCliProtocol'); describe('ClaudeCliProtocol · mock mode', () => { it('connect is no-op in mock mode', async () => { const p = new ClaudeCliProtocol({ name: 'devloop-pm' }, { mock: true }); await p.connect(null, process.cwd()); expect(p.isConnected).toBe(true); p.disconnect(); }); it('sendPrompt emits chunks + session/result from fixture', async () => { const p = new ClaudeCliProtocol({ name: 'devloop-pm' }, { mock: true }); await p.connect(null, process.cwd()); const chunks = []; let result = null; p.on('session_update', (u) => { if (u.update?.sessionUpdate === 'agent_message_chunk') { chunks.push(u.update.content.text); } }); p.on('acp_event', (ev) => { if (ev.method === 'session/result') result = ev.params; }); await p.sendPrompt('please plan the work'); expect(chunks.length).toBeGreaterThan(0); expect(result).not.toBeNull(); expect(result.mock).toBe(true); expect(result.stop_reason).toBe('end_turn'); p.disconnect(); }); it('falls back to echo response when no fixture matches', async () => { const p = new ClaudeCliProtocol( { name: 'no-such-agent-zzz' }, { mock: true, mockFixtureDir: '/tmp/devloop-mock-empty-' + Date.now() }, ); await p.connect(null, process.cwd()); let result = null; p.on('acp_event', (ev) => { if (ev.method === 'session/result') result = ev.params; }); await p.sendPrompt('hello'); expect(result.result).toMatch(/mock-ok/); p.disconnect(); }); }); describe('ClaudeCliProtocol · idle timeout (real-CLI path, non-mock)', () => { // 复现 T-mt5esage: 子进程已经进入 tool_use(吐出终态判词前的最后一步), // 但之后再也没有任何 stdout(session/result 迟迟不来)——旧代码 IDLE_TIMEOUT_TOOL_MS=24h // 会让 sendPrompt() 的 promise 挂 24 小时才被兜住;现在应在 15min 兜住。 it('rejects with a tool-mode timeout after 15min of silence following a tool_use start, not 24h', async () => { vi.useFakeTimers(); try { const p = new ClaudeCliProtocol({ name: 'devloop-coder' }, {}); // 伪造一个"已连接"的子进程:sendPrompt 只需要 _child.stdin.write 不抛错 p.isConnected = true; p._child = { stdin: { write: () => {} } }; const promise = p.sendPrompt('run the tests'); const assertion = expect(promise).rejects.toThrow(/mode=tool/); // 模拟收到 tool_use content_block_start → 切到 tool 态长超时 p._handleInnerStreamEvent({ type: 'content_block_start', content_block: { type: 'tool_use', id: 't1', name: 'Bash' } }); // 14min59s:还没到 15min 阈值,promise 不应 settle await vi.advanceTimersByTimeAsync(14 * 60_000 + 59_000); // 再推 2s 跨过 15min 边界,触发 fireTimeout await vi.advanceTimersByTimeAsync(2_000); await assertion; } finally { vi.useRealTimers(); } }); });