From 9a8ed45935c41dc35cd21b8f0a89270618213f79 Mon Sep 17 00:00:00 2001 From: Leon-SK668 <0668001470@xydigit.com> Date: Wed, 15 Jul 2026 15:16:39 +0800 Subject: [PATCH] fix(linux-node): keep command errors UTF-16 safe (#107717) --- .../linux-node/src/command-utils.test.ts | 31 +++++++++++++++++++ extensions/linux-node/src/command-utils.ts | 3 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 extensions/linux-node/src/command-utils.test.ts diff --git a/extensions/linux-node/src/command-utils.test.ts b/extensions/linux-node/src/command-utils.test.ts new file mode 100644 index 000000000000..5f8ae06d0666 --- /dev/null +++ b/extensions/linux-node/src/command-utils.test.ts @@ -0,0 +1,31 @@ +import type { SpawnResult } from "openclaw/plugin-sdk/process-runtime"; +import { describe, expect, it } from "vitest"; +import { assertToolResult, formatToolError } from "./command-utils.js"; + +const UNPAIRED_SURROGATE_RE = + /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? { + it("keeps truncated tool errors within the limit without splitting surrogate pairs", () => { + const result = failedCommand(`${"x".repeat(299)}\u{1f600}tail`); + const detail = formatToolError(result); + + expect(detail).toBe("x".repeat(299)); + expect(detail.length).toBeLessThanOrEqual(300); + expect(UNPAIRED_SURROGATE_RE.test(detail)).toBe(false); + expect(() => assertToolResult(result, "TOOL_UNAVAILABLE")).toThrow( + `TOOL_UNAVAILABLE: ${detail}`, + ); + }); +}); diff --git a/extensions/linux-node/src/command-utils.ts b/extensions/linux-node/src/command-utils.ts index 883122aeb6ab..de4d519a1de9 100644 --- a/extensions/linux-node/src/command-utils.ts +++ b/extensions/linux-node/src/command-utils.ts @@ -1,5 +1,6 @@ import type { OpenClawPluginNodeHostCommandAvailabilityContext } from "openclaw/plugin-sdk/plugin-entry"; import type { CommandOptions, SpawnResult } from "openclaw/plugin-sdk/process-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { resolveLinuxNodePluginConfigFromHost, type ResolvedLinuxNodePluginConfig, @@ -32,7 +33,7 @@ export function clamp(value: number, minimum: number, maximum: number): number { export function formatToolError(result: SpawnResult): string { const detail = result.stderr.trim() || result.stdout.trim(); return detail - ? detail.replaceAll(/\s+/gu, " ").slice(0, 300) + ? truncateUtf16Safe(detail.replaceAll(/\s+/gu, " "), 300) : `exit ${result.code ?? "unknown"}`; }