import { describe, expect, it } from 'vitest'; import { type ArtifactEvent, createArtifactParser } from '../../src/artifacts/parser'; function collect(input: string): ArtifactEvent[] { const parser = createArtifactParser(); const events: ArtifactEvent[] = []; for (const e of parser.feed(input)) events.push(e); for (const e of parser.flush()) events.push(e); return events; } describe('createArtifactParser', () => { it('parses a real artifact tag in prose', () => { const events = collect( 'Here is a page:\n\n

Hi

\n
\nDone.', ); const start = events.find((e) => e.type === 'artifact:start'); const end = events.find((e) => e.type === 'artifact:end'); expect(start).toMatchObject({ identifier: 'hello', artifactType: 'text/html', title: 'Hi' }); expect(end).toMatchObject({ identifier: 'hello' }); const trailing = events .filter((e): e is Extract => e.type === 'text') .map((e) => e.delta) .join(''); expect(trailing).toContain('Done.'); }); it('does not enter artifact mode for a tag inside inline backticks', () => { const events = collect( 'To emit an artifact, wrap output in `` and continue writing normal prose afterwards.', ); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); const text = events .filter((e): e is Extract => e.type === 'text') .map((e) => e.delta) .join(''); expect(text).toContain('continue writing normal prose afterwards.'); }); it('does not enter artifact mode for a tag inside a fenced code block', () => { const events = collect( [ 'Example:', '```html', '', '

Demo

', '
', '```', 'After the fence, more prose.', ].join('\n'), ); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); const text = events .filter((e): e is Extract => e.type === 'text') .map((e) => e.delta) .join(''); expect(text).toContain('After the fence, more prose.'); }); it('still parses a real artifact tag when prose contains an inline triple-backtick that is not a fence', () => { // The chat markdown renderer (apps/web/src/runtime/markdown.tsx) only treats // ``` as a fence when it appears alone on a line. A mid-line ```html that // is not a fence per the renderer must not suppress a real artifact that // follows. (Reported by Codex review on PR #1132.) const events = collect( 'The opening marker is ```html and the response then writes:\n\n

real

\n
', ); expect(events.find((e) => e.type === 'artifact:start')).toMatchObject({ identifier: 'real', artifactType: 'text/html', title: 'Real', }); }); it('does not enter artifact mode for a tag wrapped in double backticks', () => { // lefarcen P2: double-backtick code spans (``…``) are valid Markdown. const events = collect( 'You can quote it as ```` in prose.', ); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); }); it('does not enter artifact mode on a triple-backtick string literal inside a fenced block', () => { // lefarcen P2: fenced JS example whose body contains a string with literal ``` should // not pop fence state early and expose a later as real. const events = collect( [ '```js', 'const fence = "```";', 'const tag = "";', '```', 'After.', ].join('\n'), ); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); }); it('holds back when a chunk ends mid-line on a renderer-valid fence opener prefix', () => { // lefarcen polish P2: the streaming tail-line guard must mirror the // renderer's FENCE_OPEN_RE shape, not a stricter \w-only subset. // Opener tails like "```c++" (info string with `+`/`-`) or "``` " // (trailing whitespace) are valid renderer openers waiting for a `\n`. // If the parser flushes them as text and then sees a literal `` // on the next chunk, that artifact would incorrectly enter artifact mode. const cases: Array<{ name: string; chunks: [string, string] }> = [ { name: 'plus suffix', chunks: ['Header.\n```c++', '\ndemo\n```\n'], }, { name: 'dash suffix', chunks: ['Header.\n```ts-', '\ndemo\n```\n'], }, { name: 'trailing space', chunks: ['Header.\n``` ', '\ndemo\n```\n'], }, ]; for (const { name, chunks } of cases) { const parser = createArtifactParser(); const events: ArtifactEvent[] = []; for (const c of chunks) for (const e of parser.feed(c)) events.push(e); for (const e of parser.flush()) events.push(e); expect(events.find((e) => e.type === 'artifact:start'), name).toBeUndefined(); } }); it('parses a real artifact between paragraphs that each carry a stray backtick', () => { // Inline code is paragraph-local in the renderer; an unbalanced backtick // in one paragraph must not bridge across a blank line to pair with a // backtick in a later paragraph and swallow a real in // between (mrcfps's 2026-05-11 repro). const events = collect( [ 'intro `', '', 'demo', '', 'closing `', ].join('\n'), ); expect(events.find((e) => e.type === 'artifact:start')).toBeDefined(); expect(events.find((e) => e.type === 'artifact:end')).toBeDefined(); }); it('does not enter artifact mode when bridged by stray backticks across HR-shaped lines', () => { // Renderer's paragraph loop does not break on HR (runtime/markdown.tsx:95-104), // so `intro \`\n---\n\n---\nclosing \`` is one paragraph // and the backticks pair to cover the literal recitation. mrcfps's // 2026-05-11 05:46 follow-up — the skip-range walker must not split on HR. const events = collect( [ 'intro `', '---', 'demo', '---', 'closing `', ].join('\n'), ); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); }); it('does not enter artifact mode for or other prefix-shared identifiers', () => { // `` are not protocol tags and must survive as // literal text on both the parser and the stripper sides. const events = collect('prefix demo suffix'); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); const text = events .filter((e): e is Extract => e.type === 'text') .map((e) => e.delta) .join(''); expect(text).toBe('prefix demo suffix'); }); it('does not enter artifact mode when a fenced tag arrives across multiple chunks', () => { const parser = createArtifactParser(); const chunks = [ 'Example:\n```html\n\n

Demo

\n\n```\nAfter the fence, more prose.', ]; const events: ArtifactEvent[] = []; for (const c of chunks) for (const e of parser.feed(c)) events.push(e); for (const e of parser.flush()) events.push(e); expect(events.find((e) => e.type === 'artifact:start')).toBeUndefined(); const text = events .filter((e): e is Extract => e.type === 'text') .map((e) => e.delta) .join(''); expect(text).toContain('After the fence, more prose.'); }); });