Skip to content

Commit cb7a8b4

Browse files
fix(msteams): drop dangling surrogates in message previews
1 parent 56c2d63 commit cb7a8b4

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

extensions/msteams/src/monitor-handler/message-handler.dm-media.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
// Msteams tests cover message handlerm media plugin behavior.
22
import { describe, expect, it } from "vitest";
3+
import {
4+
sliceUtf16Safe,
5+
truncateUtf16Safe,
6+
} from "openclaw/plugin-sdk/text-utility-runtime";
37
import { translateMSTeamsDmConversationIdForGraph } from "../inbound.js";
48

59
describe("translateMSTeamsDmConversationIdForGraph", () => {
@@ -53,3 +57,37 @@ describe("translateMSTeamsDmConversationIdForGraph", () => {
5357
expect(result).toBe("a:1abc2def3");
5458
});
5559
});
60+
61+
// Verifies the three rawText / text / preview sites in message-handler.ts
62+
// (lines 250, 251, 505) drop a surrogate pair that straddles the truncation
63+
// boundary instead of leaving a lone high-surrogate half in the preview.
64+
describe("message-handler preview UTF-16 truncation", () => {
65+
const emoji = "🎉";
66+
67+
it("truncateUtf16Safe drops a surrogate pair straddling the 50-char boundary (rawText path)", () => {
68+
const input = "a".repeat(49) + emoji;
69+
const out = truncateUtf16Safe(input, 50);
70+
expect(out.length).toBe(49);
71+
expect(out).toBe("a".repeat(49));
72+
expect(out.charCodeAt(out.length - 1)).toBeLessThan(0xd800);
73+
});
74+
75+
it("truncateUtf16Safe is a pass-through for plain ASCII (text path)", () => {
76+
const input = "hello world";
77+
expect(truncateUtf16Safe(input, 50)).toBe(input);
78+
});
79+
80+
it("sliceUtf16Safe preserves an emoji that sits entirely before the 160-char cut (preview path)", () => {
81+
const input = emoji + "a".repeat(160);
82+
const out = sliceUtf16Safe(input, 0, 160);
83+
expect(out.startsWith(emoji)).toBe(true);
84+
expect(out.length).toBe(160);
85+
});
86+
87+
it("sliceUtf16Safe drops a trailing surrogate straddling the 160-char cut (preview path)", () => {
88+
const input = "a".repeat(159) + emoji;
89+
const out = sliceUtf16Safe(input, 0, 160);
90+
expect(out.length).toBe(159);
91+
expect(out.charCodeAt(out.length - 1)).toBeLessThan(0xd800);
92+
});
93+
});

extensions/msteams/src/monitor-handler/message-handler.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import {
2020
createChannelHistoryWindow,
2121
type HistoryEntry,
2222
} from "openclaw/plugin-sdk/reply-history";
23+
import {
24+
sliceUtf16Safe,
25+
truncateUtf16Safe,
26+
} from "openclaw/plugin-sdk/text-utility-runtime";
2327
import { serializeMSTeamsAdaptiveCardActionValue } from "../adaptive-card-submit.js";
2428
import {
2529
buildMSTeamsAttachmentPlaceholder,
@@ -248,8 +252,8 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
248252
const htmlSummary = summarizeMSTeamsHtmlAttachments(attachments);
249253

250254
log.info("received message", {
251-
rawText: rawText.slice(0, 50),
252-
text: text.slice(0, 50),
255+
rawText: truncateUtf16Safe(rawText, 50),
256+
text: truncateUtf16Safe(text, 50),
253257
attachments: attachments.length,
254258
attachmentTypes,
255259
from: from?.id,
@@ -503,7 +507,7 @@ export function createMSTeamsMessageHandler(deps: MSTeamsMessageHandlerDeps) {
503507
replyToId: activity.replyToId,
504508
});
505509

506-
const preview = rawBody.replace(/\s+/g, " ").slice(0, 160);
510+
const preview = sliceUtf16Safe(rawBody.replace(/\s+/g, " "), 0, 160);
507511
const inboundLabel = isDirectMessage
508512
? `Teams DM from ${senderName}`
509513
: `Teams message in ${conversationType} from ${senderName}`;

0 commit comments

Comments
 (0)