import { describe, expect, it } from "vitest"; import { createManagedRun, enqueueSystemEventMock, requestHeartbeatNowMock, EXISTING_CODEX_CONFIG, setupCliRunnerTestModule, supervisorSpawnMock, } from "./cli-runner.test-support.js"; import { executePreparedCliRun } from "./cli-runner/execute.js"; import { resolveCliNoOutputTimeoutMs } from "./cli-runner/helpers.js"; import type { PreparedCliRunContext } from "./cli-runner/types.js"; function buildPreparedContext(params?: { sessionKey?: string; cliSessionId?: string; runId?: string; }): PreparedCliRunContext { const backend = { command: "codex", args: ["exec", "--json"], output: "text" as const, input: "arg" as const, modelArg: "--model", sessionMode: "existing" as const, serialize: true, }; return { params: { sessionId: "s1", sessionKey: params?.sessionKey, sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp", prompt: "hi", provider: "codex-cli", model: "gpt-5.4", timeoutMs: 1_000, runId: params?.runId ?? "run-2", }, started: Date.now(), workspaceDir: "/tmp", backendResolved: { id: "codex-cli", config: backend, bundleMcp: false, pluginId: "openai", }, preparedBackend: { backend, env: {}, }, reusableCliSession: params?.cliSessionId ? { sessionId: params.cliSessionId } : {}, modelId: "gpt-5.4", normalizedModel: "gpt-5.4", systemPrompt: "You are a helpful assistant.", systemPromptReport: {} as PreparedCliRunContext["systemPromptReport"], bootstrapPromptWarningLines: [], }; } describe("runCliAgent reliability", () => { it("fails with timeout when no-output watchdog trips", async () => { supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ reason: "no-output-timeout", exitCode: null, exitSignal: "SIGKILL", durationMs: 200, stdout: "", stderr: "", timedOut: true, noOutputTimedOut: true, }), ); await expect( executePreparedCliRun( buildPreparedContext({ cliSessionId: "thread-123", runId: "run-2" }), "thread-123", ), ).rejects.toThrow("produced no output"); }); it("enqueues a system event and heartbeat wake on no-output watchdog timeout for session runs", async () => { supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ reason: "no-output-timeout", exitCode: null, exitSignal: "SIGKILL", durationMs: 200, stdout: "", stderr: "", timedOut: true, noOutputTimedOut: true, }), ); await expect( executePreparedCliRun( buildPreparedContext({ sessionKey: "agent:main:main", cliSessionId: "thread-123", runId: "run-2b", }), "thread-123", ), ).rejects.toThrow("produced no output"); expect(enqueueSystemEventMock).toHaveBeenCalledTimes(1); const [notice, opts] = enqueueSystemEventMock.mock.calls[0] ?? []; expect(String(notice)).toContain("produced no output"); expect(String(notice)).toContain("interactive input or an approval prompt"); expect(opts).toMatchObject({ sessionKey: "agent:main:main" }); expect(requestHeartbeatNowMock).toHaveBeenCalledWith({ reason: "cli:watchdog:stall", sessionKey: "agent:main:main", }); }); it("fails with timeout when overall timeout trips", async () => { supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ reason: "overall-timeout", exitCode: null, exitSignal: "SIGKILL", durationMs: 200, stdout: "", stderr: "", timedOut: true, noOutputTimedOut: false, }), ); await expect( executePreparedCliRun( buildPreparedContext({ cliSessionId: "thread-123", runId: "run-3" }), "thread-123", ), ).rejects.toThrow("exceeded timeout"); }); it("rethrows the retry failure when session-expired recovery retry also fails", async () => { const runCliAgent = await setupCliRunnerTestModule(); supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ reason: "exit", exitCode: 1, exitSignal: null, durationMs: 150, stdout: "", stderr: "session expired", timedOut: false, noOutputTimedOut: false, }), ); supervisorSpawnMock.mockResolvedValueOnce( createManagedRun({ reason: "exit", exitCode: 1, exitSignal: null, durationMs: 150, stdout: "", stderr: "rate limit exceeded", timedOut: false, noOutputTimedOut: false, }), ); await expect( runCliAgent({ sessionId: "s1", sessionKey: "agent:main:subagent:retry", sessionFile: "/tmp/session.jsonl", workspaceDir: "/tmp", prompt: "hi", provider: "codex-cli", model: "gpt-5.4", config: EXISTING_CODEX_CONFIG, timeoutMs: 1_000, runId: "run-retry-failure", cliSessionId: "thread-123", }), ).rejects.toThrow("rate limit exceeded"); expect(supervisorSpawnMock).toHaveBeenCalledTimes(2); }); }); describe("resolveCliNoOutputTimeoutMs", () => { it("uses backend-configured resume watchdog override", () => { const timeoutMs = resolveCliNoOutputTimeoutMs({ backend: { command: "codex", reliability: { watchdog: { resume: { noOutputTimeoutMs: 42_000, }, }, }, }, timeoutMs: 120_000, useResume: true, }); expect(timeoutMs).toBe(42_000); }); });