Skip to content

Commit d68a8ca

Browse files
test(qqbot): add runtime UTF-16 proof for outbound Failed-to-download-image template
1 parent 865b152 commit d68a8ca

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

extensions/qqbot/src/engine/messaging/outbound-media-send.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,3 +676,57 @@ describe("outbound-media-send UTF-16 truncation cap boundary", () => {
676676
expect(truncateUtf16Safe(safePrefix + "🎉 trailing path", 80)).toBe(safePrefix);
677677
});
678678
});
679+
680+
// Runtime proof for the user-visible error-message template in
681+
// outbound-media-send.ts:460 — evaluates the EXACT production template
682+
// literal against an emoji-boundary input so the BEFORE/AFTER run can be
683+
// pasted into the PR body as evidence that the cap is enforced at the
684+
// surrogate boundary, not just on length.
685+
describe("outbound-media-send runtime UTF-16 evidence", () => {
686+
function hexCodeUnits(s: string): string {
687+
return Array.from(s)
688+
.map((c) => "U+" + c.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0"))
689+
.join(" ");
690+
}
691+
692+
it("'Failed to download image' template at cap 80 keeps the 79-ASCII prefix and drops the trailing emoji pair", () => {
693+
// EXACT prod template literal from outbound-media-send.ts:460
694+
// return { channel: "qqbot", error: `Failed to download image: ${truncateUtf16Safe(mediaPath, 80)}` };
695+
const pathPrefix = "/tmp/uploads/" + "x".repeat(66); // 13 + 66 = 79 ASCII chars total
696+
const mediaPath = pathPrefix + "🎉" + "tail"; // 79 ASCII + 2 (emoji) + 4 = 85 code units; the emoji straddles the cap-80 boundary at code-unit position 79.
697+
698+
const before = `Failed to download image: ${mediaPath.slice(0, 80)}`;
699+
const after = `Failed to download image: ${truncateUtf16Safe(mediaPath, 80)}`;
700+
701+
console.log(
702+
"\n=== PR 1a runtime proof: outbound-media-send Failed-to-download-image error ===",
703+
);
704+
console.log(`input mediaPath (${mediaPath.length} code units): ${mediaPath}`);
705+
console.log(`input hex: ${hexCodeUnits(mediaPath)}`);
706+
console.log(`slice(0, 80) path: ${mediaPath.slice(0, 80)}`);
707+
console.log(`slice(0, 80) hex: ${hexCodeUnits(mediaPath.slice(0, 80))}`);
708+
console.log(`truncateUtf16Safe: ${truncateUtf16Safe(mediaPath, 80)}`);
709+
console.log(`truncateUtf16Safe hex: ${hexCodeUnits(truncateUtf16Safe(mediaPath, 80))}`);
710+
console.log(`BEFORE full message (${before.length} code units):`);
711+
console.log(` ${before}`);
712+
console.log(` hex: ${hexCodeUnits(before)}`);
713+
console.log(`AFTER full message (${after.length} code units):`);
714+
console.log(` ${after}`);
715+
console.log(` hex: ${hexCodeUnits(after)}`);
716+
const beforeHasLone =
717+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(before);
718+
const afterHasLone =
719+
/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(after);
720+
console.log(`BEFORE contains lone surrogate? ${beforeHasLone}`);
721+
console.log(`AFTER contains lone surrogate? ${afterHasLone}`);
722+
723+
// The cap-80 helper preserves the 79-ASCII prefix and drops the
724+
// trailing emoji pair, so the AFTER message ends exactly at the
725+
// prefix without a half-surrogate dangling in the error string.
726+
expect(after).toBe(`Failed to download image: ${pathPrefix}`);
727+
expect(afterHasLone).toBe(false);
728+
// Sanity: the BEFORE path still emits a lone 0xD83D high surrogate
729+
// in the user-facing error string (the bug being fixed).
730+
expect(beforeHasLone).toBe(true);
731+
});
732+
});

0 commit comments

Comments
 (0)