From 79aaab403c51a3c600370d4c32b9d9941fd593de Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Sat, 14 Feb 2026 18:53:37 -0800 Subject: [PATCH] test (agents): cover empty-chunk timeout failover behavior --- ...helpers.classifyfailoverreason.e2e.test.ts | 1 + ...lpers.formatassistanterrortext.e2e.test.ts | 5 ++ ...pi-agent.auth-profile-rotation.e2e.test.ts | 54 +++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts index 1b175e77b..daf9d9cf5 100644 --- a/src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts +++ b/src/agents/pi-embedded-helpers.classifyfailoverreason.e2e.test.ts @@ -24,6 +24,7 @@ describe("classifyFailoverReason", () => { expect(classifyFailoverReason("invalid request format")).toBe("format"); expect(classifyFailoverReason("credit balance too low")).toBe("billing"); expect(classifyFailoverReason("deadline exceeded")).toBe("timeout"); + expect(classifyFailoverReason("request ended without sending any chunks")).toBe("timeout"); expect( classifyFailoverReason( "521 Web server is downCloudflare", diff --git a/src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts b/src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts index 9d0179e42..9ba67b6a1 100644 --- a/src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts +++ b/src/agents/pi-embedded-helpers.formatassistanterrortext.e2e.test.ts @@ -108,4 +108,9 @@ describe("formatAssistantErrorText", () => { const msg = makeAssistantError("429 rate limit reached"); expect(formatAssistantErrorText(msg)).toContain("rate limit reached"); }); + + it("returns a friendly message for empty stream chunk errors", () => { + const msg = makeAssistantError("request ended without sending any chunks"); + expect(formatAssistantErrorText(msg)).toBe("LLM request timed out."); + }); }); diff --git a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts index 24d6b1c38..83f757f13 100644 --- a/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts +++ b/src/agents/pi-embedded-runner.run-embedded-pi-agent.auth-profile-rotation.e2e.test.ts @@ -175,6 +175,60 @@ describe("runEmbeddedPiAgent auth profile rotation", () => { } }); + it("rotates when stream ends without sending chunks", async () => { + const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-")); + try { + await writeAuthStore(agentDir); + + runEmbeddedAttemptMock + .mockResolvedValueOnce( + makeAttempt({ + assistantTexts: [], + lastAssistant: buildAssistant({ + stopReason: "error", + errorMessage: "request ended without sending any chunks", + }), + }), + ) + .mockResolvedValueOnce( + makeAttempt({ + assistantTexts: ["ok"], + lastAssistant: buildAssistant({ + stopReason: "stop", + content: [{ type: "text", text: "ok" }], + }), + }), + ); + + await runEmbeddedPiAgent({ + sessionId: "session:test", + sessionKey: "agent:test:empty-chunk-stream", + sessionFile: path.join(workspaceDir, "session.jsonl"), + workspaceDir, + agentDir, + config: makeConfig(), + prompt: "hello", + provider: "openai", + model: "mock-1", + authProfileId: "openai:p1", + authProfileIdSource: "auto", + timeoutMs: 5_000, + runId: "run:empty-chunk-stream", + }); + + expect(runEmbeddedAttemptMock).toHaveBeenCalledTimes(2); + + const stored = JSON.parse( + await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf-8"), + ) as { usageStats?: Record }; + expect(typeof stored.usageStats?.["openai:p2"]?.lastUsed).toBe("number"); + } finally { + await fs.rm(agentDir, { recursive: true, force: true }); + await fs.rm(workspaceDir, { recursive: true, force: true }); + } + }); + it("does not rotate for compaction timeouts", async () => { const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-agent-")); const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-"));