@@ -562,6 +562,28 @@ describe("compactRawCommand middle truncation", () => {
|
562 | 562 | expect(result).not.toContain("AKIDABCDEFGHIJKLMNOP1234567890"); |
563 | 563 | expect(result).toContain("AKIDAB…7890"); |
564 | 564 | }); |
| 565 | + |
| 566 | +it("does not split a surrogate pair when the head boundary lands on an emoji", () => { |
| 567 | +// The one-line form is 140 UTF-16 units. With the default maxLength=120 the head |
| 568 | +// slice ends at index 59, but the 😀 emoji (U+1F600, a surrogate pair) occupies |
| 569 | +// indices 58-59 — so a raw .slice(0, 59) would keep the high surrogate and drop |
| 570 | +// its low half, leaving a lone surrogate that renders as the replacement char. |
| 571 | +const emoji = String.fromCodePoint(0x1f600); |
| 572 | +// Unknown binary so resolveExecDetail returns the compact raw form directly. |
| 573 | +const longCommand = `/opt/custom/bin/run ${"a".repeat(38)}${emoji}${"b".repeat(80)}`; |
| 574 | +const result = resolveExecDetail({ command: longCommand }); |
| 575 | + |
| 576 | +expect(result).toBeDefined(); |
| 577 | +// The whole emoji is dropped at the boundary rather than half of it. |
| 578 | +expect(result).not.toContain(emoji); |
| 579 | +// No dangling/lone surrogate code units remain in the rendered detail. |
| 580 | +expect(result).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/); |
| 581 | +expect(result).not.toMatch(/(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/); |
| 582 | +// Start and end of the command are still preserved around the ellipsis. |
| 583 | +expect(result).toContain("/opt/custom/bin/run"); |
| 584 | +expect(result).toContain("…"); |
| 585 | +expect(result).toMatch(/b{4}$/); |
| 586 | +}); |
565 | 587 | }); |
566 | 588 | |
567 | 589 | describe("coerceDisplayValue middle truncation", () => { |
|