From 56d0721eb7ff859740f07a485db095fc96cd0da3 Mon Sep 17 00:00:00 2001 From: Leon-SK668 <0668001470@xydigit.com> Date: Fri, 17 Jul 2026 08:43:56 +0800 Subject: [PATCH] fix(release): preserve emoji in cross-OS response diagnostics (#108193) * fix(release): keep cross-OS responses valid Unicode * style(release): format cross-OS boundary test * fix(release): resolve UTF-16 helper in direct Node runs * fix(release): keep cross-OS summaries UTF-16 safe Co-authored-by: Leon-SK668 <0668001470@xydigit.com> --------- Co-authored-by: Peter Steinberger --- .../cross-os-release-checks/network-smokes.ts | 3 +- scripts/lib/cross-os-release-checks/shared.ts | 3 +- .../openclaw-cross-os-release-checks.test.ts | 36 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) 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();