mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 20:31:17 +00:00
* fix(agents): keep failover detail truncation UTF-16 safe Replace raw .slice(0, MAX_FAILOVER_DETAIL_CHARS) with truncateUtf16Safe so provider error details containing emoji or other non-BMP characters are not split at a surrogate-pair boundary before failover classification. Adds a regression test via extractFailoverSignalDetails. * test(agents): tighten failover UTF-16 coverage --------- Co-authored-by: chengzhichao-xydt <chengzhichao-xydt@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
// 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 {
|
|
extractFailoverSignalDetails,
|
|
formatAssistantErrorText,
|
|
isLikelyContextOverflowError,
|
|
} from "./errors.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("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("audits a sandbox tool-policy block once per assistant error", () => {
|
|
// 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: "non-main", scope: "agent" },
|
|
},
|
|
},
|
|
tools: {
|
|
sandbox: {
|
|
tools: {
|
|
deny: ["browser"],
|
|
},
|
|
},
|
|
},
|
|
};
|
|
const msg = makeAssistantError("unknown tool: browser");
|
|
|
|
expect(
|
|
formatAssistantErrorText(msg, { cfg, sessionKey: "agent:main:mobilechat:g1" }),
|
|
).toContain('Tool "browser" blocked by sandbox tool policy');
|
|
expect(
|
|
formatAssistantErrorText(msg, { cfg, sessionKey: "agent:main:mobilechat:g1" }),
|
|
).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: "non-main",
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("extractFailoverSignalDetails", () => {
|
|
it.each([
|
|
{
|
|
name: "backs off before a split surrogate pair",
|
|
input: `${"a".repeat(999)}🎉!`,
|
|
expected: "a".repeat(999),
|
|
},
|
|
{
|
|
name: "keeps the full ASCII budget",
|
|
input: "a".repeat(1001),
|
|
expected: "a".repeat(1000),
|
|
},
|
|
])("$name in nested provider details", ({ input, expected }) => {
|
|
const details = extractFailoverSignalDetails({ error: { body: { detail: input } } });
|
|
|
|
expect(details).toEqual([expected]);
|
|
});
|
|
});
|
|
|
|
describe("isLikelyContextOverflowError", () => {
|
|
it("detects Codex promptError wording for a full context window", () => {
|
|
expect(
|
|
isLikelyContextOverflowError(
|
|
"Codex ran out of room in the model's context window. Start a new thread or clear earlier history before retrying.",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|