import { describe, expect, it } from "vitest";
import { createAssistantMarkdownParser } from "./assistant-markdown-parser";
describe("createAssistantMarkdownParser", () => {
it("keeps bold text bold through every partial closing marker", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
for (const source of ["**bold", "**bold*", "**bold**"]) {
expect(parser.renderInline(source)).toBe("bold");
}
});
it("shows the growing link label and only links a complete destination", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
const source = "[docs](https://example.com/path)";
for (let length = 1; length < source.length; length++) {
expect(parser.renderInline(source.slice(0, length))).toBe(
"docs".slice(0, Math.max(0, length - 1)),
);
}
expect(parser.renderInline(source)).toBe('docs');
});
it.each([
["**", "strong"],
["__", "strong"],
["*", "em"],
["_", "em"],
["~~", "s"],
["`", "code"],
["``", "code"],
])("keeps %s formatting stable as text and closing markers arrive", (marker, tag) => {
const parser = createAssistantMarkdownParser({ streaming: true });
for (let length = 1; length <= 4; length++) {
expect(parser.renderInline(marker + "text".slice(0, length))).toBe(
`<${tag}>${"text".slice(0, length)}${tag}>`,
);
}
for (let length = 0; length <= marker.length; length++) {
expect(parser.renderInline(marker + "text" + marker.slice(0, length))).toBe(
`<${tag}>text${tag}>`,
);
}
});
it("keeps combined and nested emphasis stable through closing markers", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
for (const closing of ["", "*", "**", "***"]) {
expect(parser.renderInline("***both" + closing)).toBe("both");
}
expect(parser.renderInline("**bold and *italic")).toBe(
"bold and italic",
);
expect(parser.renderInline("*italic and **bold")).toBe(
"italic and bold",
);
});
it.each(["*", "**", "***", "_", "__", "~~", "`", "``"])(
"hides an opening %s while waiting for its text",
(marker) => {
expect(
createAssistantMarkdownParser({ streaming: true }).renderInline("hello " + marker),
).toBe("hello ");
},
);
it.each([
"[docs](https://example.com/a(b)c)",
'[docs](https://example.com "a title)")',
"[docs]()",
"[docs](file:///tmp/example.ts)",
])("waits for the entire destination of %s", (source) => {
const parser = createAssistantMarkdownParser({ streaming: true });
for (let length = 6; length < source.length; length++) {
expect(parser.renderInline(source.slice(0, length))).toBe("docs");
}
expect(parser.renderInline(source)).toBe(createAssistantMarkdownParser().renderInline(source));
});
it("preserves formatting in incomplete labels and around incomplete links", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
expect(parser.renderInline("[**bold")).toBe("bold");
expect(parser.renderInline("[**bold**](https://exam")).toBe("bold");
expect(parser.renderInline("**see [docs](https://exam")).toBe("see docs");
});
it("does not auto-link a URL label before the destination is complete", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
expect(parser.renderInline("Read [example.com](https://exam")).toBe("Read example.com");
expect(parser.renderInline("Read [example.com](https://example.org)")).toBe(
'Read example.com',
);
});
it.each([
"\\*literal",
"\\[literal",
"some_identifier",
"some__identifier",
"`**literal [link](url`",
"``a ` b``",
"**already closed** after",
"[bad](javascript:alert(1))",
" {
expect(createAssistantMarkdownParser({ streaming: true }).renderInline(source)).toBe(
createAssistantMarkdownParser().renderInline(source),
);
});
it.each([
"```md\n**literal [link](url",
"~~~md\n**literal [link](url",
" **literal [link](url",
"**earlier\n\nplain tail",
"- **earlier\n- plain tail",
])("leaves literal code and earlier blocks alone: %s", (source) => {
expect(createAssistantMarkdownParser({ streaming: true }).render(source)).toBe(
createAssistantMarkdownParser().render(source),
);
});
it("completes inline formatting inside the final list item", () => {
expect(createAssistantMarkdownParser({ streaming: true }).render("- first\n- **bold")).toBe(
"\n",
);
});
it("hides incomplete images until their source is complete", () => {
const parser = createAssistantMarkdownParser({ streaming: true });
expect(parser.renderInline("before ).toBe("before ");
expect(parser.renderInline("before ")).toBe(
'before
',
);
});
it("keeps ordinary parsing for completed messages", () => {
const parser = createAssistantMarkdownParser();
expect(parser.renderInline("**unfinished")).toBe("**unfinished");
expect(parser.renderInline("[unfinished")).toBe("[unfinished");
});
it("renders agent text verbatim", () => {
const parser = createAssistantMarkdownParser();
// The reported bug, plus the substitutions that share its cause.
expect(parser.renderInline("(c) (C) (r) (tm) (p)")).toBe("(c) (C) (r) (tm) (p)");
expect(parser.renderInline("wait for it...")).toBe("wait for it...");
expect(parser.renderInline("a -- b")).toBe("a -- b");
// Smart quotes are off too: a curled quote is not pasteable into a shell.
expect(parser.renderInline(`run --name="my repo"`)).toBe("run --name="my repo"");
expect(parser.renderInline("it's fine")).toBe("it's fine");
});
it("allows file:// links, unlike every other parser", () => {
const parser = createAssistantMarkdownParser();
expect(parser.render("[open](file:///tmp/a.ts)")).toContain('href="file:///tmp/a.ts"');
});
it("still rejects javascript: links", () => {
const parser = createAssistantMarkdownParser();
expect(parser.render("[x](javascript:alert(1))")).not.toContain("href");
});
});