From 6655e32f2544c511bdd5fc165b730ea6d6802978 Mon Sep 17 00:00:00 2001 From: Yunfei He Date: Wed, 27 May 2026 14:42:19 +0800 Subject: [PATCH] test: add sanitize-ansi parity tests from Ink (+24) Port 24 missing tests from Ink's sanitize-ansi test suite covering: - C1 OSC hyperlinks with various terminators (C1 ST, ESC ST, BEL) - C1 CSI sequences (both SGR and non-SGR) - DCS passthrough (tmux wrappers, incomplete, BEL in payload) - SOS control strings (ESC and C1 variants, various terminators) - Standalone C1 control characters and ST bytes - ESC ST sequences, malformed ESC sequences, incomplete CSI Co-Authored-By: Claude Opus 4.7 (1M context) --- .../runtime/src/paint/sanitize-ansi.test.ts | 228 +++++++++++++++++- 1 file changed, 224 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/paint/sanitize-ansi.test.ts b/packages/runtime/src/paint/sanitize-ansi.test.ts index 6e6aa43..494825b 100644 --- a/packages/runtime/src/paint/sanitize-ansi.test.ts +++ b/packages/runtime/src/paint/sanitize-ansi.test.ts @@ -1,25 +1,245 @@ import { describe, test, expect } from "vite-plus/test"; import { sanitizeAnsi } from "./sanitize-ansi.ts"; +// Minimal ANSI-stripping helper for test assertions (avoids strip-ansi dep). +function stripAnsi(s: string): string { + // eslint-disable-next-line no-control-regex + return s.replace( + /[›][[()#;?]*(?:[0-9]{1,4}(?:[:;][0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]|[]\].*?(?:|\\|œ)|[˜][\s\S]*?(?:|\\|œ)|[€-Ÿ]/g, + "", + ); +} + describe("sanitize-ansi", () => { test("preserves SGR (color) sequences", () => { - expect(sanitizeAnsi("\x1b[31mred\x1b[0m")).toBe("\x1b[31mred\x1b[0m"); + expect(sanitizeAnsi("red")).toBe("red"); + }); + + test("preserve SGR sequences (colon-form 24-bit color)", () => { + const output = sanitizeAnsi("A[38:2::255:100:0mcolorB"); + + expect(output).toContain("[38:2::255:100:0m"); + expect(stripAnsi(output)).toBe("AcolorB"); }); test("strips cursor movement", () => { - expect(sanitizeAnsi("\x1b[2Ahello")).toBe("hello"); + expect(sanitizeAnsi("hello")).toBe("hello"); }); test("strips screen clearing", () => { - expect(sanitizeAnsi("\x1b[2Jhello")).toBe("hello"); + expect(sanitizeAnsi("hello")).toBe("hello"); }); test("preserves OSC (hyperlinks)", () => { - const link = "\x1b]8;;https://example.com\x07click\x1b]8;;\x07"; + const link = "]8;;https://example.comclick]8;;"; expect(sanitizeAnsi(link)).toBe(link); }); + test("preserve OSC hyperlinks (ESC-ST terminated)", () => { + const output = sanitizeAnsi("]8;;https://example.com\\link]8;;\\"); + + expect(output).toContain("]8;;https://example.com"); + expect(stripAnsi(output)).toBe("link"); + }); + test("passes through plain text unchanged", () => { expect(sanitizeAnsi("hello world")).toBe("hello world"); }); + + // --- Ink parity tests below --- + + test("preserve OSC hyperlinks terminated by C1 ST", () => { + const output = sanitizeAnsi("]8;;https://example.comœlink]8;;œ"); + + expect(output).toContain("]8;;https://example.comœ"); + expect(stripAnsi(output)).toBe("link"); + }); + + test("preserve C1 OSC hyperlinks terminated by C1 ST", () => { + const input = "8;;https://example.comœlink8;;œ"; + const output = sanitizeAnsi(input); + + expect(output).toContain("8;;https://example.comœ"); + expect(output).toBe(input); + }); + + test("preserve C1 OSC hyperlinks terminated by ESC ST", () => { + const input = "8;;https://example.com\\link8;;\\"; + const output = sanitizeAnsi(input); + + expect(output).toContain("8;;https://example.com\\"); + expect(output).toBe(input); + }); + + test("preserve C1 OSC hyperlinks terminated by BEL", () => { + const input = "8;;https://example.comlink8;;"; + const output = sanitizeAnsi(input); + + expect(output).toContain("8;;https://example.com"); + expect(output).toBe(input); + }); + + test("strip non-SGR CSI sequences as complete units", () => { + const output = sanitizeAnsi("A[>4;2mB[2 qC"); + + expect(output).not.toContain("4;2m"); + expect(output).not.toContain(" q"); + expect(stripAnsi(output)).toBe("ABC"); + }); + + test("strip C1 non-SGR CSI sequences as complete units", () => { + const output = sanitizeAnsi("A›>4;2mB›2 qC"); + + expect(output).not.toContain("4;2m"); + expect(output).not.toContain(" q"); + expect(stripAnsi(output)).toBe("ABC"); + }); + + test("preserve C1 SGR CSI sequences", () => { + const output = sanitizeAnsi("A›31mgreen›0mB"); + + expect(output).toContain("›31m"); + expect(stripAnsi(output)).toBe("AgreenB"); + }); + + test("strip private-parameter m-sequences that are not SGR", () => { + const output = sanitizeAnsi("A[>4;2mB"); + + expect(output).not.toContain("[>4;2m"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip tmux DCS passthrough wrappers with escaped ST payload terminators", () => { + const wrappedHyperlinkStart = "Ptmux;]8;;https://example.com\\\\"; + const wrappedHyperlinkEnd = "Ptmux;]8;;\\\\"; + const output = sanitizeAnsi(`${wrappedHyperlinkStart}link${wrappedHyperlinkEnd}`); + + expect(output).not.toContain("tmux;"); + expect(output).not.toContain("P"); + expect(stripAnsi(output)).toBe("link"); + }); + + test("strip incomplete DCS passthrough sequences to avoid payload leaks", () => { + const output = sanitizeAnsi("APtmux;link"); + + expect(output).not.toContain("tmux;"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip DCS control strings with BEL in payload until ST terminator", () => { + const output = sanitizeAnsi("APpayloadstill-payload\\B"); + + expect(output).not.toContain("payload"); + expect(output).not.toContain("still-payload"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip ESC SOS control strings as complete units", () => { + const output = sanitizeAnsi("AXpayload\\B"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip ESC SOS control strings with C1 ST terminator", () => { + const output = sanitizeAnsi("AXpayloadœB"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip C1 SOS control strings as complete units with C1 ST terminator", () => { + const output = sanitizeAnsi("A˜payloadœB"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip C1 SOS control strings as complete units with ESC ST terminator", () => { + const output = sanitizeAnsi("A˜payload\\B"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip ESC SOS with BEL terminator as malformed control string", () => { + const output = sanitizeAnsi("AXpayloadB"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip C1 SOS with BEL terminator as malformed control string", () => { + const output = sanitizeAnsi("A˜payloadB"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip incomplete ESC SOS control strings to avoid payload leaks", () => { + const output = sanitizeAnsi("AXpayload"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip incomplete C1 SOS control strings to avoid payload leaks", () => { + const output = sanitizeAnsi("A˜payload"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip SOS with escaped ESC in payload until final ST terminator", () => { + const output = sanitizeAnsi("AXfoo\\bar\\B"); + + expect(output).not.toContain("foo"); + expect(output).not.toContain("bar"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("preserve SGR around stripped SOS control strings", () => { + const output = sanitizeAnsi("ARXpayload\\B"); + + expect(output).toContain(""); + expect(output).toContain(""); + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("ARB"); + }); + + test("strip ESC ST sequences", () => { + const output = sanitizeAnsi("A\\B"); + + expect(output).not.toContain("\\"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip malformed ESC control sequences with intermediates and non-final bytes", () => { + const output = sanitizeAnsi("A#payload"); + + expect(output).not.toContain("payload"); + expect(stripAnsi(output)).toBe("A"); + }); + + test("strip incomplete CSI after preserving prior SGR content", () => { + const output = sanitizeAnsi("AB["); + + expect(output).toContain(""); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip standalone ST bytes", () => { + const output = sanitizeAnsi("AœB"); + + expect(output).not.toContain("œ"); + expect(stripAnsi(output)).toBe("AB"); + }); + + test("strip standalone C1 control characters", () => { + const output = sanitizeAnsi("A…BŽC"); + + expect(output).not.toContain("…"); + expect(output).not.toContain("Ž"); + expect(stripAnsi(output)).toBe("ABC"); + }); });