mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 05:53:55 +00:00
fix(control-ui-assets): keep emoji / surrogate pairs intact during last-line truncation (#101591)
* fix(control-ui-assets): keep emoji / surrogate pairs intact during last-line truncation .slice(0, 239) counts UTF-16 code units; an emoji straddling the cut point produces a lone surrogate rendering as in WebUI assets. Use [...last] spread to count full code points instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(control-ui-assets): preserve diagnostic length cap --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -8,6 +8,7 @@ type FakeFsEntry = { kind: "file"; content: string } | { kind: "dir" };
|
||||
const state = vi.hoisted(() => ({
|
||||
entries: new Map<string, FakeFsEntry>(),
|
||||
realpaths: new Map<string, string>(),
|
||||
runCommandWithTimeout: vi.fn(),
|
||||
}));
|
||||
|
||||
const abs = (p: string) => path.resolve(p);
|
||||
@@ -69,7 +70,11 @@ vi.mock("./openclaw-root.js", () => ({
|
||||
resolveOpenClawPackageRoot: vi.fn(async () => null),
|
||||
resolveOpenClawPackageRootSync: vi.fn(() => null),
|
||||
}));
|
||||
vi.mock("../process/exec.js", () => ({
|
||||
runCommandWithTimeout: state.runCommandWithTimeout,
|
||||
}));
|
||||
|
||||
let ensureControlUiAssetsBuilt: typeof import("./control-ui-assets.js").ensureControlUiAssetsBuilt;
|
||||
let resolveControlUiRepoRoot: typeof import("./control-ui-assets.js").resolveControlUiRepoRoot;
|
||||
let resolveControlUiDistIndexPath: typeof import("./control-ui-assets.js").resolveControlUiDistIndexPath;
|
||||
let resolveControlUiDistIndexHealth: typeof import("./control-ui-assets.js").resolveControlUiDistIndexHealth;
|
||||
@@ -81,6 +86,7 @@ let openclawRoot: typeof import("./openclaw-root.js");
|
||||
describe("control UI assets helpers (fs-mocked)", () => {
|
||||
beforeAll(async () => {
|
||||
({
|
||||
ensureControlUiAssetsBuilt,
|
||||
resolveControlUiRepoRoot,
|
||||
resolveControlUiDistIndexPath,
|
||||
resolveControlUiDistIndexHealth,
|
||||
@@ -94,6 +100,7 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
beforeEach(() => {
|
||||
state.entries.clear();
|
||||
state.realpaths.clear();
|
||||
state.runCommandWithTimeout.mockReset();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
@@ -179,6 +186,39 @@ describe("control UI assets helpers (fs-mocked)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a truncated build failure diagnostic within its UTF-16 limit", async () => {
|
||||
const root = abs("fixtures/build-failure");
|
||||
const argv1 = path.join(root, "src", "index.ts");
|
||||
const originalArgv1 = process.argv[1];
|
||||
setFile(path.join(root, "ui", "vite.config.ts"), "export {};\n");
|
||||
setFile(path.join(root, "scripts", "ui.js"), "");
|
||||
state.runCommandWithTimeout.mockResolvedValueOnce({
|
||||
stdout: "",
|
||||
stderr: `${"y".repeat(238)}🚀xx`,
|
||||
code: 1,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
});
|
||||
process.argv[1] = argv1;
|
||||
|
||||
try {
|
||||
const result = await ensureControlUiAssetsBuilt({
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
built: false,
|
||||
message: `Control UI build failed: ${"y".repeat(238)}…`,
|
||||
});
|
||||
} finally {
|
||||
process.argv[1] = originalArgv1;
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves control-ui root from override file or directory", () => {
|
||||
const root = abs("fixtures/override");
|
||||
const uiDir = path.join(root, "dist", "control-ui");
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { normalizeStringEntries } from "@openclaw/normalization-core/string-normalization";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import { runCommandWithTimeout } from "../process/exec.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import * as controlUiFsRuntime from "./control-ui-assets.fs.runtime.js";
|
||||
@@ -285,7 +286,7 @@ function summarizeCommandOutput(text: string): string | undefined {
|
||||
if (!last) {
|
||||
return undefined;
|
||||
}
|
||||
return last.length > 240 ? `${last.slice(0, 239)}…` : last;
|
||||
return last.length > 240 ? `${truncateUtf16Safe(last, 239)}…` : last;
|
||||
}
|
||||
|
||||
export async function ensureControlUiAssetsBuilt(
|
||||
|
||||
Reference in New Issue
Block a user