diff --git a/scripts/lib/cross-os-release-checks/network-smokes.ts b/scripts/lib/cross-os-release-checks/network-smokes.ts index 0f8acf2cbe0a..c37a9d23c463 100644 --- a/scripts/lib/cross-os-release-checks/network-smokes.ts +++ b/scripts/lib/cross-os-release-checks/network-smokes.ts @@ -1,5 +1,6 @@ import { randomUUID } from "node:crypto"; import { join } from "node:path"; +import { truncateUtf16Safe } from "../../../packages/normalization-core/src/utf16-slice.ts"; import type { GatewayHandle, LaneState } from "./config.ts"; import { CROSS_OS_DASHBOARD_FETCH_TIMEOUT_MS, @@ -157,7 +158,7 @@ export async function readBoundedCrossOsResponseText( text += decoder.decode(value, { stream: true }); if (text.length > maxChars) { - text = text.slice(0, maxChars); + text = truncateUtf16Safe(text, maxChars); truncated = true; break; } diff --git a/scripts/lib/cross-os-release-checks/shared.ts b/scripts/lib/cross-os-release-checks/shared.ts index 7caba1940b1d..fa345e0a67f5 100644 --- a/scripts/lib/cross-os-release-checks/shared.ts +++ b/scripts/lib/cross-os-release-checks/shared.ts @@ -1,5 +1,6 @@ import { existsSync } from "node:fs"; import { join } from "node:path"; +import { truncateUtf16Safe } from "../../../packages/normalization-core/src/utf16-slice.ts"; export function resolveCommandPath(command: string) { const pathValue = process.env.PATH ?? ""; @@ -28,7 +29,7 @@ export function trimForSummary(value: string) { if (trimmed.length <= 600) { return trimmed; } - return `${trimmed.slice(0, 600)}...`; + return `${truncateUtf16Safe(trimmed, 600)}...`; } export function formatError(error: unknown) { diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 53b3e9541d97..ae875e1dba14 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -88,6 +88,7 @@ import { resolveRunnerMatrix, resolveStaticFileContentType, startStaticFileServer, + trimForSummary, shouldExerciseManagedGatewayLifecycleAfterInstall, shouldRunPackagedUpgradeStatusProbe, shouldRunWindowsInstalledBrowserOverrideImportSmoke, @@ -210,6 +211,41 @@ describe("scripts/openclaw-cross-os-release-checks", () => { expect(CROSS_OS_FETCH_BODY_MAX_CHARS).toBeGreaterThan(1024); }); + it.each([ + { + caseName: "drops a split surrogate pair", + responseBody: `abc\u{1f600}tail`, + expectedText: "abc\n[truncated]", + }, + { + caseName: "preserves a complete surrogate pair", + responseBody: `ab\u{1f600}tail`, + expectedText: `ab\u{1f600}\n[truncated]`, + }, + ])( + "keeps cross-OS response truncation UTF-16 safe: $caseName", + async ({ responseBody, expectedText }) => { + const response = new Response(responseBody); + + await expect(readBoundedCrossOsResponseText(response, 4)).resolves.toBe(expectedText); + }, + ); + + it.each([ + { + caseName: "drops a split surrogate pair", + input: `${"x".repeat(599)}\u{1f600}tail`, + expected: `${"x".repeat(599)}...`, + }, + { + caseName: "preserves a complete surrogate pair", + input: `${"x".repeat(598)}\u{1f600}tail`, + expected: `${"x".repeat(598)}\u{1f600}...`, + }, + ])("keeps cross-OS summaries UTF-16 safe: $caseName", ({ input, expected }) => { + expect(trimForSummary(input)).toBe(expected); + }); + it("keeps cross-OS fetch timeouts active while reading response bodies", async () => { let canceled = false; const abortController = new AbortController();