test(codex): verify completion idle watch arms after non-assistant rawResponseItem/completed

Regression test for the binary stall fix: when rawResponseItem/completed
arrives with a non-assistant type (e.g. "reasoning") and all tracked
items have completed, the completion idle watch must stay armed so the
stall is caught in 60s, not 30 minutes.

Refs openclaw/openclaw#87071

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Username
2026-05-27 08:27:55 +08:00
committed by Peter Steinberger
parent a36c82ba8b
commit 4d6bcf9f17

View File

@@ -5471,6 +5471,111 @@ describe("runCodexAppServerAttempt", () => {
).toBe(false);
});
it("arms completion idle watch after non-assistant rawResponseItem/completed with no active items", async () => {
let notify: (notification: CodexServerNotification) => Promise<void> = async () => undefined;
let handleRequest:
| ((request: { id: string; method: string; params?: unknown }) => Promise<unknown>)
| undefined;
const warn = vi.spyOn(embeddedAgentLog, "warn").mockImplementation(() => undefined);
const request = vi.fn(async (method: string) => {
if (method === "thread/start") {
return threadStartResult("thread-1");
}
if (method === "turn/start") {
return turnStartResult("turn-1", "inProgress");
}
return {};
});
setCodexAppServerClientFactoryForTest(
async () =>
({
request,
addNotificationHandler: (handler: typeof notify) => {
notify = handler;
return () => undefined;
},
addRequestHandler: (
handler: (request: {
id: string;
method: string;
params?: unknown;
}) => Promise<unknown>,
) => {
handleRequest = handler;
return () => undefined;
},
}) as never,
);
const params = createParams(
path.join(tempDir, "session.jsonl"),
path.join(tempDir, "workspace"),
);
params.timeoutMs = 60_000;
const run = runCodexAppServerAttempt(params, {
turnCompletionIdleTimeoutMs: 5,
turnAssistantCompletionIdleTimeoutMs: 500,
turnTerminalIdleTimeoutMs: 500,
});
await vi.waitFor(() => expect(handleRequest).toBeTypeOf("function"), fastWait);
const toolResult = (await handleRequest?.({
id: "request-tool-1",
method: "item/tool/call",
params: {
threadId: "thread-1",
turnId: "turn-1",
callId: "call-1",
namespace: null,
tool: "message",
arguments: { action: "send", text: "already sent" },
},
})) as { success?: boolean };
expect(toolResult.success).toBe(false);
// Send a rawResponseItem/completed with type "reasoning" — this does NOT
// qualify as postToolRawAssistantCompletionNeedsTerminalGuard (which
// requires type=message + role=assistant + text preview). Before the fix,
// this would disarm the completion idle watch via the catch-all disarm
// block, leaving only the 30-minute terminal timeout. After the fix,
// rawResponseItemCompletedWithNoActiveItems keeps the 60s (here 5ms)
// completion idle watch armed.
await notify({
method: "rawResponseItem/completed",
params: {
threadId: "thread-1",
turnId: "turn-1",
item: {
type: "reasoning",
id: "raw-reasoning-1",
},
},
});
const result = await run;
expect(result.aborted).toBe(true);
expect(result.timedOut).toBe(true);
expect(result.promptError).toBe(
"codex app-server turn idle timed out waiting for turn/completed",
);
const completionWarnCall = warn.mock.calls.find(
([message]) => message === "codex app-server turn idle timed out waiting for completion",
);
expect(completionWarnCall).toBeDefined();
const completionWarnData = completionWarnCall?.[1] as
| { lastActivityReason?: string; timeoutMs?: number }
| undefined;
expect(completionWarnData?.timeoutMs).toBe(5);
expect(completionWarnData?.lastActivityReason).toBe("notification:rawResponseItem/completed");
// The terminal idle watch (500ms) should NOT have fired — the shorter
// completion idle watch (5ms) should catch the stall first.
expect(
warn.mock.calls.some(
([message]) =>
message === "codex app-server turn idle timed out waiting for terminal event",
),
).toBe(false);
});
it("releases the session when Codex accepts a turn but never sends progress", async () => {
const harness = createStartedThreadHarness();
const params = createParams(