ClaudeCliProtocol.mock.test.js 3.01 KB
/**
 * 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();
    }
  });
});