Skip to content

Commit 7424d62

Browse files
authored
test(qqbot): focus UTF-16 approval regression
1 parent c7b6ef2 commit 7424d62

6 files changed

Lines changed: 7 additions & 81 deletions

File tree

extensions/qqbot/src/engine/approval/index.test.ts

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,16 @@ describe("buildApprovalKeyboard", () => {
2323
});
2424

2525
describe("buildExecApprovalText", () => {
26-
const hasLoneSurrogate = (value: string): boolean => {
27-
for (let i = 0; i < value.length; i++) {
28-
const code = value.charCodeAt(i);
29-
if (code >= 0xd800 && code <= 0xdbff) {
30-
const next = value.charCodeAt(i + 1);
31-
if (!(next >= 0xdc00 && next <= 0xdfff)) {
32-
return true;
33-
}
34-
i++;
35-
} else if (code >= 0xdc00 && code <= 0xdfff) {
36-
return true;
37-
}
38-
}
39-
return false;
40-
};
41-
4226
it("truncates the command preview on a UTF-16 boundary without splitting surrogate pairs", () => {
43-
// Mirrors the call shape at approval/index.ts:64 inside buildExecApprovalText:
44-
// lines.push(`\`\`\`\n${truncateUtf16Safe(cmd, 300)}\n\`\`\``);
45-
// The command preview is user-visible in the approval message and must
46-
// not split a surrogate pair (which would render as garbage in the
47-
// QQ approval card UI).
48-
const longCommand = "echo " + "测试命令参数🎉🎉🎉🎉🎉".repeat(50);
27+
const safePrefix = "x".repeat(299);
4928
const text = buildExecApprovalText({
5029
id: "approval-1",
5130
expiresAtMs: Date.now() + 60_000,
5231
request: {
53-
commandPreview: longCommand,
54-
command: longCommand,
55-
cwd: "/tmp",
32+
commandPreview: `${safePrefix}🎉 trailing text`,
5633
},
5734
});
5835
const codeFence = text.split("```\n")[1]?.split("\n```")[0] ?? "";
59-
expect(codeFence.length).toBeLessThanOrEqual(300);
60-
expect(hasLoneSurrogate(codeFence)).toBe(false);
36+
expect(codeFence).toBe(safePrefix);
6137
});
6238
});

extensions/qqbot/src/engine/messaging/reply-dispatcher.test.ts

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2-
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
32

43
const { openLocalFileMock, resolveLocalPathFromRootsSyncMock, sendMediaMock, sendTextMock } =
54
vi.hoisted(() => ({
@@ -431,47 +430,3 @@ describe("handleStructuredPayload", () => {
431430
},
432431
);
433432
});
434-
435-
// Pin the same UTF-16 boundary behavior that the production debugLog sites
436-
// in reply-dispatcher.ts / streaming-c2c.ts / streaming-media-send.ts /
437-
// sender.ts rely on.
438-
describe("reply-dispatcher UTF-16-safe truncation helper", () => {
439-
const hasLoneSurrogate = (value: string): boolean => {
440-
for (let i = 0; i < value.length; i++) {
441-
const code = value.charCodeAt(i);
442-
if (code >= 0xd800 && code <= 0xdbff) {
443-
const next = value.charCodeAt(i + 1);
444-
if (!(next >= 0xdc00 && next <= 0xdfff)) {
445-
return true;
446-
}
447-
i++;
448-
} else if (code >= 0xdc00 && code <= 0xdfff) {
449-
return true;
450-
}
451-
}
452-
return false;
453-
};
454-
455-
it("truncates TTS preview on UTF-16 boundary without splitting emoji", () => {
456-
// Mirrors the call shape at reply-dispatcher.ts:516
457-
// log?.debug?.(`TTS: "${truncateUtf16Safe(ttsText, 50)}..."`);
458-
const ttsText = "你好世界🎉🎉🎉这是TTS预览文本的剩余部分";
459-
const truncated = truncateUtf16Safe(ttsText, 50);
460-
expect(truncated.length).toBeLessThanOrEqual(50);
461-
expect(hasLoneSurrogate(truncated)).toBe(false);
462-
});
463-
464-
it("truncates streaming-c2c preview on UTF-16 boundary", () => {
465-
// Mirrors the call shape at streaming-c2c.ts:546
466-
// const preview = truncateUtf16Safe(payload.text ?? "", 60).replace(/\n/g, "\\n");
467-
const payloadText = "测试消息🎉🎉剩余内容在60字符内被截断的边界emoji字符串";
468-
const truncated = truncateUtf16Safe(payloadText, 60);
469-
expect(truncated.length).toBeLessThanOrEqual(60);
470-
expect(hasLoneSurrogate(truncated)).toBe(false);
471-
});
472-
473-
it("passes plain ASCII through unchanged (negative control)", () => {
474-
const ascii = "plain ASCII text without any emoji or CJK content";
475-
expect(truncateUtf16Safe(ascii, 60)).toBe(ascii);
476-
});
477-
});

extensions/qqbot/src/engine/messaging/reply-dispatcher.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,7 @@ function resolveStructuredPayloadPath(
282282
}
283283

284284
function sanitizeForLog(value: string, maxLen = 200): string {
285-
return truncateUtf16Safe(
286-
value
287-
.replace(/[\r\n\t]/g, " ")
288-
.replaceAll("\0", " "),
289-
maxLen,
290-
);
285+
return truncateUtf16Safe(value.replace(/[\r\n\t]/g, " ").replaceAll("\0", " "), maxLen);
291286
}
292287

293288
function describeMediaTargetForLog(pathValue: string, isHttpUrl: boolean): string {

extensions/qqbot/src/engine/messaging/sender.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*/
2626

2727
import os from "node:os";
28+
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
2829
import { ApiClient } from "../api/api-client.js";
2930
import { ChunkedMediaApi as ChunkedMediaApiClass } from "../api/media-chunked.js";
3031
import { downloadDirectUploadUrl, MediaApi as MediaApiClass } from "../api/media.js";
@@ -41,7 +42,6 @@ import {
4142
type OutboundMeta,
4243
type UploadMediaResponse,
4344
} from "../types.js";
44-
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
4545
import { getMaxUploadSize, LARGE_FILE_THRESHOLD } from "../utils/file-utils.js";
4646
import { formatErrorMessage } from "../utils/format.js";
4747
import { debugLog, debugError, debugWarn } from "../utils/log.js";

extensions/qqbot/src/engine/messaging/streaming-c2c.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
* it is treated as a new message.
1616
*/
1717

18-
import { getNextMsgSeq } from "../api/routes.js";
1918
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
19+
import { getNextMsgSeq } from "../api/routes.js";
2020
import type { GatewayAccount } from "../types.js";
2121
import {
2222
StreamInputMode,

extensions/qqbot/src/engine/messaging/streaming-media-send.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* 拆分、路径编码修复,以及统一的发送队列执行器。
66
*/
77

8-
import type { GatewayAccount } from "../types.js";
98
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
9+
import type { GatewayAccount } from "../types.js";
1010
import { normalizePath } from "../utils/platform.js";
1111
import type { OutboundMediaAccessContext } from "./outbound-types.js";
1212
import {

0 commit comments

Comments
 (0)