import { describe, expect, it } from 'vitest'; import { splitStreamingArtifact, stripArtifact, stripRecoveredHtmlFallbackForDisplay, summarizeArtifactsForTranscript, } from '../../src/artifacts/strip'; const completeHtml = 'X

X

'; describe('stripArtifact', () => { it('removes a real artifact tag and its body from prose', () => { const out = stripArtifact( 'Header.\n\n

x

\n
\nFooter.', ); expect(out).toBe('Header.\n\nFooter.'); }); it('preserves an artifact tag wrapped in inline backticks', () => { const input = 'Wrap output as `demo` to ship it.'; expect(stripArtifact(input)).toBe(input); }); it('preserves an artifact tag inside a fenced code block', () => { const input = [ 'Example:', '```html', '', '

Demo

', '
', '```', 'After.', ].join('\n'); expect(stripArtifact(input)).toBe(input); }); it('preserves a tag wrapped in double backticks', () => { const input = 'Use ```` here.'; expect(stripArtifact(input)).toBe(input); }); it('returns content unchanged when no artifact open tag is present', () => { const input = 'Just prose, no markup.'; expect(stripArtifact(input)).toBe(input); }); it('does not truncate when an open tag has no matching close', () => { // A bare orphan open without a close should not nuke the rest of the // message (the previous implementation sliced to end-of-string). const input = 'Trailing prose with no closer.'; expect(stripArtifact(input)).toBe(input); }); it('preserves a tag inside a fenced literal whose closing fence has no trailing newline', () => { // Renderer treats an unclosed-at-EOF fence as a code block extending to // end of input. The stripper must do the same, otherwise a literal // recitation tucked into a code example at the bottom of a chat reply // gets eaten. const input = '```js\nconst s = `demo`;\n```'; expect(stripArtifact(input)).toBe(input); }); it('strips a real artifact that follows an indented pseudo-fence (renderer sees no fence)', () => { // The renderer's open-fence regex disallows leading spaces; an indented // " ```html" line is rendered as a paragraph, not a fence. The // on the next line is therefore a real protocol tag and // must be stripped — not preserved as fictional fenced content. const input = [ ' ```html', '', '

x

', '
', 'Tail.', ].join('\n'); const out = stripArtifact(input); expect(out).not.toContain(' { // Inline code spans are paragraph-local in the renderer; an unbalanced // backtick in one paragraph must not bridge across a blank line to pair // with a backtick in another paragraph and accidentally classify a real // between them as inline code (mrcfps's 2026-05-11 repro). const input = [ 'intro `', '', 'demo', '', 'closing `', ].join('\n'); const out = stripArtifact(input); expect(out).not.toContain(' { // runtime/markdown.tsx:95-104's paragraph-accumulation loop only breaks on // blank / fence / heading / ul / ol — it does NOT break on HR. So a buffer // shaped `intro \`` / `---` / `` / `---` / `closing \`` // is one paragraph in the renderer, and the two stray backticks pair to // cover the literal artifact recitation. The skip-range walker must mirror // that (mrcfps's 2026-05-11 05:46 follow-up). const input = [ 'intro `', '---', 'demo', '---', 'closing `', ].join('\n'); expect(stripArtifact(input)).toBe(input); }); it('does not strip or other prefix-shared identifiers', () => { // The streaming parser only treats `` gets silently truncated. const input = 'prefix demo suffix'; expect(stripArtifact(input)).toBe(input); }); it('preserves a tag inside a fenced block that contains a `\`\`\`html` line in its body', () => { // The renderer closes a fence only on a bare "```\\s*$" — a "```html" // line inside an open fence is kept as code content. The stripper must // match that semantics or it will exit the fence early and eat a // literal recitation that follows. const input = [ '```js', 'const a = `

x

`;', '```html', 'demo', '```', ].join('\n'); expect(stripArtifact(input)).toBe(input); }); }); describe('splitStreamingArtifact', () => { it('surfaces an open-but-unclosed html artifact as a live box', () => { const input = 'Building it.\n\n

Hi'; const { head, live } = splitStreamingArtifact(input); expect(head).toBe('Building it.'); expect(live).not.toBeNull(); expect(live?.artifactType).toBe('text/html'); expect(live?.title).toBe('Landing'); expect(live?.identifier).toBe('page'); expect(live?.content).toBe('\n

Hi'); }); it('defers a completed artifact to stripArtifact (no live box)', () => { const input = 'Done.\n\n

x

\n
'; const { head, live } = splitStreamingArtifact(input); expect(head).toBe(input); expect(live).toBeNull(); }); it('returns no live box for a non-text/html artifact type', () => { const input = 'Generating.\n'; const { head, live } = splitStreamingArtifact(input); expect(head).toBe(input); expect(live).toBeNull(); }); it('treats an unknown/omitted type as code-eligible', () => { const input = 'Writing.\n\nsome text'; const { live } = splitStreamingArtifact(input); expect(live).not.toBeNull(); expect(live?.artifactType).toBe(''); }); it('ignores a literal recited inside a code fence', () => { const input = [ 'Example:', '```html', '', '

Demo

', ].join('\n'); const { head, live } = splitStreamingArtifact(input); expect(head).toBe(input); expect(live).toBeNull(); }); it('shows an empty live box when the open tag attributes are still streaming', () => { const input = 'Almost there.\nsummary only', ].join('\n'); const stripped = stripArtifact(original); expect(stripRecoveredHtmlFallbackForDisplay(stripped, original)).toBe('Done — saved as an artifact.'); }); it('leaves partial snippets unchanged', () => { const input = '```html\n

Snippet

\n```'; expect(stripRecoveredHtmlFallbackForDisplay(input)).toBe(input); }); it('leaves multiple complete html fences unchanged instead of guessing', () => { const input = ['```html', completeHtml, '```', '```html', completeHtml, '```'].join('\n'); expect(stripRecoveredHtmlFallbackForDisplay(input)).toBe(input); }); }); describe('summarizeArtifactsForTranscript', () => { it('summarizes persisted css, svg, and markdown artifacts', () => { const input = [ '', 'body { color: red; }', '', '', '', '', '', '# Brief', '', ].join('\n'); const out = summarizeArtifactsForTranscript(input, [ { name: 'theme.css' }, { name: 'logo.svg' }, { name: 'brief.md' }, ]); expect(out).toContain('project file "theme.css"'); expect(out).toContain('project file "logo.svg"'); expect(out).toContain('project file "brief.md"'); expect(out).not.toContain('body { color: red; }'); expect(out).not.toContain(''); expect(out).not.toContain('# Brief'); }); });