Files
openclaw/extensions/telegram/src/send.chunks.test.ts
nas df87b40bec fix(telegram): guard UTF-16 surrogate pairs in outbound chunkers (#93938)
Merged via squash.

Prepared head SHA: 583b22354d
Co-authored-by: Nas01010101 <156536069+Nas01010101@users.noreply.github.com>
Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com>
Reviewed-by: @vincentkoc
2026-06-17 18:56:25 +08:00

58 lines
2.2 KiB
TypeScript

// Telegram tests cover plain-text chunk-splitting behavior.
import { describe, expect, it } from "vitest";
import { splitTelegramPlainTextChunksForTests } from "./send.js";
function containsLoneSurrogate(text: string): boolean {
for (let index = 0; index < text.length; index += 1) {
const code = text.charCodeAt(index);
const isHigh = code >= 0xd800 && code <= 0xdbff;
const isLow = code >= 0xdc00 && code <= 0xdfff;
if (isHigh) {
const next = text.charCodeAt(index + 1);
if (!(next >= 0xdc00 && next <= 0xdfff)) {
return true;
}
index += 1;
} else if (isLow) {
return true;
}
}
return false;
}
describe("splitTelegramPlainTextChunks", () => {
it("does not split an astral char across the chunk boundary", () => {
// Emoji surrogate pair straddles index 10 (limit): high at 9, low at 10.
const input = `${"A".repeat(9)}😀${"B".repeat(20)}`;
const chunks = splitTelegramPlainTextChunksForTests(input, 10);
expect(chunks.length).toBeGreaterThan(1);
expect(chunks.join("")).toBe(input);
for (const chunk of chunks) {
expect(containsLoneSurrogate(chunk)).toBe(false);
}
});
it("does not hang when limit=1 and text starts with an astral char", () => {
// Regression: with limit=1 the clamp would return start (no advance),
// causing the while-loop to spin forever. The surrogate pair must be
// emitted as a unit (2 code units) so the loop always advances.
const input = "😀X";
const chunks = splitTelegramPlainTextChunksForTests(input, 1);
expect(chunks.join("")).toBe(input);
for (const chunk of chunks) {
expect(containsLoneSurrogate(chunk)).toBe(false);
}
});
it("does not hang when limit=1 and an astral char appears mid-string at a chunk boundary", () => {
// 'A' + emoji: with limit=1, second iteration starts at index 1 (high
// surrogate) — same stall condition as above, now mid-string.
const input = "A😀B";
const chunks = splitTelegramPlainTextChunksForTests(input, 1);
expect(chunks.join("")).toBe(input);
for (const chunk of chunks) {
expect(containsLoneSurrogate(chunk)).toBe(false);
}
});
});