Skip to content

Commit ca210f7

Browse files
committed
fix(msteams): keep truncated parent context text well-formed
summarizeParentMessage truncated the parent body with text.slice(0, PARENT_TEXT_MAX_CHARS - 1), which can cut a surrogate pair in half and emit a lone surrogate into the injected "Replying to @sender: …" system event. Use the shared truncateUtf16Safe helper so truncation falls back to a whole code-point boundary. Add a regression asserting the summary stays isWellFormed() when the limit lands inside an emoji.
1 parent c6f5725 commit ca210f7

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

extensions/msteams/src/thread-parent-context.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
summarizeParentMessage,
1111
} from "./thread-parent-context.js";
1212

13+
// Matches an unpaired UTF-16 surrogate (lone high or lone low), without relying
14+
// on the ES2024 String.prototype.isWellFormed() runtime API.
15+
const UNPAIRED_SURROGATE_RE =
16+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
17+
1318
describe("summarizeParentMessage", () => {
1419
it("returns undefined for missing message", () => {
1520
expect(summarizeParentMessage(undefined)).toBeUndefined();
@@ -81,6 +86,20 @@ describe("summarizeParentMessage", () => {
8186
expect(summary?.text.length).toBeLessThanOrEqual(400);
8287
expect(summary?.text.endsWith("…")).toBe(true);
8388
});
89+
90+
it("keeps truncated parent text well-formed when truncating surrogate pairs", () => {
91+
const msg: GraphThreadMessage = {
92+
id: "p1",
93+
from: { user: { displayName: "Dana" } },
94+
body: { content: `${"a".repeat(398)}🦞${"b".repeat(50)}`, contentType: "text" },
95+
};
96+
97+
const summary = summarizeParentMessage(msg);
98+
99+
expect(summary?.text).not.toMatch(UNPAIRED_SURROGATE_RE);
100+
expect(summary?.text).toBe(`${"a".repeat(398)}…`);
101+
expect(summary?.text.endsWith("\ud83e…")).toBe(false);
102+
});
84103
});
85104

86105
describe("formatParentContextEvent", () => {

extensions/msteams/src/thread-parent-context.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
asDateTimestampMs,
1818
resolveExpiresAtMsFromDurationMs,
1919
} from "openclaw/plugin-sdk/number-runtime";
20+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
2021
import { fetchChannelMessage, stripHtmlFromTeamsMessage } from "./graph-thread.js";
2122
import type { GraphThreadMessage } from "./graph-thread.js";
2223

@@ -138,7 +139,9 @@ export function summarizeParentMessage(
138139
return {
139140
sender,
140141
text:
141-
text.length > PARENT_TEXT_MAX_CHARS ? `${text.slice(0, PARENT_TEXT_MAX_CHARS - 1)}…` : text,
142+
text.length > PARENT_TEXT_MAX_CHARS
143+
? `${truncateUtf16Safe(text, PARENT_TEXT_MAX_CHARS - 1)}…`
144+
: text,
142145
};
143146
}
144147

0 commit comments

Comments
 (0)