mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-26 04:09:32 +00:00
fix(slack): truncate on code-point boundaries to avoid splitting surrogate pairs (#96382)
truncateSlackText sliced by UTF-16 code unit ('trimmed.slice(0, max - 1)'), so an
emoji or other astral character straddling the limit was cut in half, leaving a
lone high surrogate before the ellipsis — e.g. truncateSlackText('abc😀def', 5)
returned 'abc\uD83D…' instead of 'abc…'. That invalid half-character is sent in
live Slack payloads (message text and Block Kit section/button/header/option
labels, which truncate at limits as small as 75).
Use the repo's canonical sliceUtf16Safe (already re-exported from
plugin-sdk/text-utility-runtime, the module slack code imports from) so a
straddling pair is dropped whole. Behavior is byte-identical for all-BMP input.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
27
extensions/slack/src/truncate.test.ts
Normal file
27
extensions/slack/src/truncate.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// Slack tests cover truncate plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { truncateSlackText } from "./truncate.js";
|
||||
|
||||
describe("truncateSlackText", () => {
|
||||
it("drops a surrogate-pair emoji whole when it straddles the limit", () => {
|
||||
// "abc😀def": 😀 (U+1F600) sits at the cut point. Slicing by UTF-16 code unit
|
||||
// would keep only its high surrogate — a lone \uD83D — before the ellipsis,
|
||||
// which serializes to an invalid character in the Slack payload.
|
||||
const out = truncateSlackText("abc😀def", 5);
|
||||
expect(out).toBe("abc…");
|
||||
// No dangling high surrogate (a high surrogate not followed by a low one).
|
||||
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(out)).toBe(false);
|
||||
});
|
||||
|
||||
it("truncates plain BMP text unchanged", () => {
|
||||
expect(truncateSlackText("hello world", 5)).toBe("hell…");
|
||||
});
|
||||
|
||||
it("keeps an emoji that fits before the cut", () => {
|
||||
expect(truncateSlackText("😀abcdef", 5)).toBe("😀ab…");
|
||||
});
|
||||
|
||||
it("returns the trimmed input unchanged when it fits", () => {
|
||||
expect(truncateSlackText("ab😀cd", 10)).toBe("ab😀cd");
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,16 @@
|
||||
// Slack plugin module implements truncate behavior.
|
||||
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
|
||||
export function truncateSlackText(value: string, max: number): string {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.length <= max) {
|
||||
return trimmed;
|
||||
}
|
||||
// Slice on a code-point boundary so a surrogate pair (emoji / astral char)
|
||||
// straddling the limit is dropped whole, instead of leaving a lone surrogate
|
||||
// half that serializes to an invalid `\uD83D` in the Slack payload.
|
||||
if (max <= 1) {
|
||||
return trimmed.slice(0, max);
|
||||
return sliceUtf16Safe(trimmed, 0, max);
|
||||
}
|
||||
return `${trimmed.slice(0, max - 1)}…`;
|
||||
return `${sliceUtf16Safe(trimmed, 0, max - 1)}…`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user