Skip to content

Commit 442322f

Browse files
solodmdclaude
andcommitted
fix(signal): truncate verbose inbound preview on code-point boundaries
Signal verbose inbound message previews truncate with body.slice(0, 200), which can split a UTF-16 surrogate pair when an emoji straddles position 200, producing a lone high surrogate in the debug log. Use truncateUtf16Safe from openclaw/plugin-sdk/text-utility-runtime so the preview always lands on a valid code-point boundary. Tests: 5 new tests covering the straddling-emoji boundary, emoji-before- boundary, plain ASCII truncation, position-0 edge case, and short-body pass-through. Co-Authored-By: Claude <[email protected]>
1 parent 54b0958 commit 442322f

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
2+
// Signal tests cover event handler.truncation plugin behavior.
3+
import { describe, expect, it } from "vitest";
4+
5+
describe("signal inbound preview truncation", () => {
6+
it("drops an emoji whole when it straddles the 200-char preview boundary", () => {
7+
// Emoji 😀 (U+1F600) occupies UTF-16 indices 199-200.
8+
// A raw .slice(0, 200) would keep the high surrogate at index 199 and
9+
// drop the low surrogate at index 200, leaving a dangling U+D83D.
10+
const body = `${"a".repeat(199)}\u{1F600}${"b".repeat(50)}`;
11+
expect(body.length).toBeGreaterThanOrEqual(251);
12+
13+
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
14+
15+
// No lone surrogate halves in the output
16+
expect(/[\u{D800}-\u{DFFF}]/u.test(preview)).toBe(false);
17+
18+
// The safe truncation drops both surrogates, yielding 199 ASCII chars
19+
expect(preview.length).toBe(199);
20+
expect(preview).toBe("a".repeat(199));
21+
});
22+
23+
it("preserves emoji when it fits within the 200-char limit", () => {
24+
const body = `${"a".repeat(50)}\u{1F600}${"b".repeat(50)}`;
25+
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
26+
27+
expect(/[\u{D800}-\u{DFFF}]/u.test(preview)).toBe(false);
28+
expect(preview).toBe(body);
29+
});
30+
31+
it("truncates long plain ASCII text without modification", () => {
32+
const body = "x".repeat(500);
33+
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
34+
35+
expect(preview.length).toBe(200);
36+
expect(preview).toBe("x".repeat(200));
37+
});
38+
39+
it("handles boundary at position 0", () => {
40+
const body = "\u{1F600}" + "a".repeat(50);
41+
const preview = truncateUtf16Safe(body, 1).replace(/\n/g, "\\n");
42+
// The emoji takes 2 code units, so truncating at 1 drops it
43+
expect(preview.length).toBe(0);
44+
expect(/[\u{D800}-\u{DFFF}]/u.test(preview)).toBe(false);
45+
});
46+
47+
it("handles body already shorter than limit", () => {
48+
const body = "hello";
49+
const preview = truncateUtf16Safe(body, 200).replace(/\n/g, "\\n");
50+
expect(preview).toBe("hello");
51+
});
52+
});

extensions/signal/src/monitor/event-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { resolvePinnedMainDmOwnerFromAllowlist } from "openclaw/plugin-sdk/secur
3838
import { readSessionUpdatedAt, resolveStorePath } from "openclaw/plugin-sdk/session-store-runtime";
3939
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
4040
import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime";
41-
import { normalizeE164 } from "openclaw/plugin-sdk/text-utility-runtime";
41+
import { normalizeE164, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
4242
import {
4343
maybeResolveSignalApprovalReaction,
4444
resolveSignalApprovalConversationKey,
@@ -263,7 +263,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) {
263263
});
264264

265265
if (shouldLogVerbose()) {
266-
const preview = body.slice(0, 200).replace(/\\n/g, "\\\\n");
266+
const preview = truncateUtf16Safe(body, 200).replace(/\\n/g, "\\\\n");
267267
logVerbose(`signal inbound: from=${ctxPayload.From} len=${body.length} preview="${preview}"`);
268268
}
269269

0 commit comments

Comments
 (0)