mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
fix(gateway): keep command-list field clamping UTF-16 safe (#102816)
* fix(gateway): keep command-list field clamping UTF-16 safe clampString truncated command names, descriptions, aliases, arg names/descriptions, and choice values/labels with a raw value.slice(0, maxLength). When an emoji (or other astral code point) straddles the clamp limit, the raw slice keeps a dangling high surrogate and emits a lone surrogate over the gateway commands.list protocol result. Route the clamp through the shared truncateUtf16Safe primitive so the boundary code point is dropped whole. Adds a regression test through buildCommandsListResult. * test(gateway): fold UTF-16 clamp regression into bounds coverage Co-authored-by: MoerAI <friendnt@g.skku.edu> --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// Command list serialization gathers chat, skill, and plugin commands into the
|
||||
// gateway protocol result while clamping names, descriptions, aliases, and args.
|
||||
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
|
||||
import type {
|
||||
CommandEntry,
|
||||
CommandsListResult,
|
||||
@@ -36,7 +37,7 @@ type SerializedArg = NonNullable<CommandEntry["args"]>[number];
|
||||
type CommandNameSurface = "text" | "native";
|
||||
|
||||
function clampString(value: string, maxLength: number): string {
|
||||
return value.length > maxLength ? value.slice(0, maxLength) : value;
|
||||
return value.length > maxLength ? truncateUtf16Safe(value, maxLength) : value;
|
||||
}
|
||||
|
||||
function trimClampNonEmpty(value: string, maxLength: number): string | null {
|
||||
|
||||
@@ -517,7 +517,8 @@ describe("commands.list handler", () => {
|
||||
const originalCommands = [...mockChatCommands];
|
||||
const longToken = "x".repeat(COMMAND_NAME_MAX_LENGTH + 50);
|
||||
const aliasBase = "alias".repeat(20);
|
||||
const longDescription = "d".repeat(COMMAND_DESCRIPTION_MAX_LENGTH + 50);
|
||||
const descriptionPrefix = "d".repeat(COMMAND_DESCRIPTION_MAX_LENGTH - 1);
|
||||
const longDescription = `${descriptionPrefix}😀tail`;
|
||||
const oversizedArgs = Array.from({ length: COMMAND_ARGS_MAX_ITEMS + 5 }, (_, argIndex) => ({
|
||||
name: `${longToken}-${argIndex}`,
|
||||
description: longDescription,
|
||||
@@ -554,6 +555,7 @@ describe("commands.list handler", () => {
|
||||
expect((first.description as string).length).toBeLessThanOrEqual(
|
||||
COMMAND_DESCRIPTION_MAX_LENGTH,
|
||||
);
|
||||
expect(first.description).toBe(descriptionPrefix);
|
||||
expect((first.textAliases as unknown[]).length).toBeLessThanOrEqual(COMMAND_ALIAS_MAX_ITEMS);
|
||||
expect(first.args as unknown[]).toHaveLength(COMMAND_ARGS_MAX_ITEMS);
|
||||
const firstArg = (first.args as Array<Record<string, unknown>>)[0];
|
||||
|
||||
Reference in New Issue
Block a user