Skip to content

Commit fe18ced

Browse files
MoerAIsteipete
andauthored
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 <[email protected]> --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent 772e0f7 commit fe18ced

2 files changed

Lines changed: 5 additions & 2 deletions

File tree

src/gateway/server-methods/commands-list-result.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Command list serialization gathers chat, skill, and plugin commands into the
22
// gateway protocol result while clamping names, descriptions, aliases, and args.
33
import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/string-coerce";
4+
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
45
import type {
56
CommandEntry,
67
CommandsListResult,
@@ -36,7 +37,7 @@ type SerializedArg = NonNullable<CommandEntry["args"]>[number];
3637
type CommandNameSurface = "text" | "native";
3738

3839
function clampString(value: string, maxLength: number): string {
39-
return value.length > maxLength ? value.slice(0, maxLength) : value;
40+
return value.length > maxLength ? truncateUtf16Safe(value, maxLength) : value;
4041
}
4142

4243
function trimClampNonEmpty(value: string, maxLength: number): string | null {

src/gateway/server-methods/commands.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,8 @@ describe("commands.list handler", () => {
517517
const originalCommands = [...mockChatCommands];
518518
const longToken = "x".repeat(COMMAND_NAME_MAX_LENGTH + 50);
519519
const aliasBase = "alias".repeat(20);
520-
const longDescription = "d".repeat(COMMAND_DESCRIPTION_MAX_LENGTH + 50);
520+
const descriptionPrefix = "d".repeat(COMMAND_DESCRIPTION_MAX_LENGTH - 1);
521+
const longDescription = `${descriptionPrefix}😀tail`;
521522
const oversizedArgs = Array.from({ length: COMMAND_ARGS_MAX_ITEMS + 5 }, (_, argIndex) => ({
522523
name: `${longToken}-${argIndex}`,
523524
description: longDescription,
@@ -554,6 +555,7 @@ describe("commands.list handler", () => {
554555
expect((first.description as string).length).toBeLessThanOrEqual(
555556
COMMAND_DESCRIPTION_MAX_LENGTH,
556557
);
558+
expect(first.description).toBe(descriptionPrefix);
557559
expect((first.textAliases as unknown[]).length).toBeLessThanOrEqual(COMMAND_ALIAS_MAX_ITEMS);
558560
expect(first.args as unknown[]).toHaveLength(COMMAND_ARGS_MAX_ITEMS);
559561
const firstArg = (first.args as Array<Record<string, unknown>>)[0];

0 commit comments

Comments
 (0)