// Covers assistant error formatting for streaming, sandbox, and context errors. import type { AssistantMessage } from "openclaw/plugin-sdk/llm"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE } from "../../shared/assistant-error-format.js"; import { makeAssistantMessageFixture } from "../test-helpers/assistant-message-fixtures.js"; import { formatAssistantErrorText, formatUserFacingAssistantErrorText } from "./error-text.js"; const { toolPolicyAuditInfo } = vi.hoisted(() => ({ toolPolicyAuditInfo: vi.fn(), })); vi.mock("../../logging/subsystem.js", () => ({ createSubsystemLogger: () => ({ debug: vi.fn(), error: vi.fn(), info: toolPolicyAuditInfo, warn: vi.fn(), }), })); describe("formatAssistantErrorText streaming JSON parse classification", () => { beforeEach(() => { toolPolicyAuditInfo.mockClear(); }); const makeAssistantError = (errorMessage: string): AssistantMessage => makeAssistantMessageFixture({ errorMessage, content: [{ type: "text", text: errorMessage }], }); it("suppresses transport-classified malformed streaming fragments", () => { // Transport JSON fragmentation is not user-authored content and should get // stable retry copy instead of raw parser text. const msg = makeAssistantError(MALFORMED_STREAMING_FRAGMENT_ERROR_MESSAGE); expect(formatAssistantErrorText(msg)).toBe( "LLM streaming response contained a malformed fragment. Please try again.", ); }); it("does not suppress unclassified JSON.parse text", () => { const msg = makeAssistantError( "Expected ',' or '}' after property value in JSON at position 334 (line 1 column 335)", ); expect(formatAssistantErrorText(msg)).toBe( "Expected ',' or '}' after property value in JSON at position 334 (line 1 column 335)", ); }); it.each(["request timed out", "LLM request timed out."])( "preserves safe user-facing timeout copy for provider error %j", (errorMessage) => { expect(formatUserFacingAssistantErrorText(makeAssistantError(errorMessage))).toBe( "LLM request timed out.", ); }, ); it("keeps non-streaming provider request-validation syntax diagnostics", () => { const msg = makeAssistantError( '{"type":"error","error":{"type":"invalid_request_error","message":"Expected value in JSON at position 12 for messages.0.content"}}', ); expect(formatAssistantErrorText(msg)).toBe( "LLM request rejected: Expected value in JSON at position 12 for messages.0.content", ); }); it.each([ { sessionKey: "agent:main:mobilechat:g1", agentId: "main", mode: "non-main" as const }, { sessionKey: "global", agentId: "worker", mode: "all" as const }, ])( "audits a sandbox tool-policy block once per assistant error for $sessionKey", ({ sessionKey, agentId, mode }) => { // Formatting may be called multiple times for the same error; audit logs // should stay deduplicated per blocked assistant error. const cfg: OpenClawConfig = { agents: { defaults: { sandbox: { mode, scope: "agent" }, }, entries: { main: {}, worker: {} }, }, tools: { sandbox: { tools: { deny: ["browser"], }, }, }, }; const msg = makeAssistantError("unknown tool: browser"); expect(formatAssistantErrorText(msg, { cfg, sessionKey, agentId })).toContain( `--agent ${agentId}`, ); expect(formatAssistantErrorText(msg, { cfg, sessionKey, agentId })).toContain( 'Tool "browser" blocked by sandbox tool policy', ); expect(toolPolicyAuditInfo).toHaveBeenCalledTimes(1); expect(toolPolicyAuditInfo).toHaveBeenCalledWith( "sandbox tool policy blocked browser via tools.sandbox.tools.deny; matched browser", { tool: "browser", ruleKind: "deny", ruleSource: "global", configKey: "tools.sandbox.tools.deny", matchedRule: "browser", sandboxMode: mode, }, ); }, ); });