import { mkdtemp, mkdir, readFile, rm, symlink } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { decideHtmlArtifactWrite, deriveHtmlSlug, deriveHtmlTitle, deriveMarkdownSlug, detectHtmlArtifact, HTML_ARTIFACT_MAX_BYTES, writeHtmlArtifact, writeMarkdownArtifact, } from '../artifacts.js'; import { PathScopeError } from '../path_guard.js'; describe('deriveMarkdownSlug', () => { it('uses the first # heading when present', () => { expect(deriveMarkdownSlug('# Hello World\n\nbody')).toBe('hello-world'); }); it('falls back to first 6 words', () => { const s = deriveMarkdownSlug('the quick brown fox jumps over the lazy dog'); expect(s).toBe('the-quick-brown-fox-jumps-over'); }); it('returns "artifact" for empty input', () => { expect(deriveMarkdownSlug('')).toBe('artifact'); }); it('caps at 60 chars and lowercases', () => { const long = '# ' + 'A'.repeat(200); const s = deriveMarkdownSlug(long); expect(s.length).toBeLessThanOrEqual(60); expect(s).toMatch(/^[a-z0-9-]+$/); }); it('strips trailing punctuation', () => { expect(deriveMarkdownSlug('# Hello, World!!!')).toBe('hello-world'); }); }); describe('deriveHtmlSlug', () => { it('prefers payload.title when set', () => { expect( deriveHtmlSlug({ html_content: '', title: 'My Title' }), ).toBe('my-title'); }); it('falls back to tag', () => { expect( deriveHtmlSlug({ html_content: '<html><head><title>Page Title', title: null, }), ).toBe('page-title'); }); it('falls back to first

when no ', () => { expect( deriveHtmlSlug({ html_content: '<html><body><h1>Heading One</h1></body></html>', title: null, }), ).toBe('heading-one'); }); it('falls back to inner text words', () => { expect( deriveHtmlSlug({ html_content: '<div>one two three four five six seven</div>', title: null, }), ).toBe('one-two-three-four-five-six'); }); }); describe('deriveHtmlTitle', () => { it('returns <title> content', () => { expect(deriveHtmlTitle('<html><head><title>T')).toBe('T'); }); it('falls back to

', () => { expect(deriveHtmlTitle('

H

')).toBe('H'); }); it('falls back to first 80 chars of inner text', () => { const html = '
' + 'x '.repeat(100) + '
'; const t = deriveHtmlTitle(html); expect(t).not.toBeNull(); expect(t!.length).toBeLessThanOrEqual(80); }); it('returns null for empty html', () => { expect(deriveHtmlTitle('')).toBeNull(); }); }); describe('detectHtmlArtifact', () => { it('detects prefix case-insensitively', () => { const html = 'x'; expect(detectHtmlArtifact(html)).toBe(html); }); it('strips leading/trailing whitespace before matching', () => { const html = '\n\n\n\n'; expect(detectHtmlArtifact(html)).toBe(html.trim()); }); it('detects fenced ```html block wrapping entire message', () => { const wrapped = '```html\n\n\n```'; expect(detectHtmlArtifact(wrapped)).toContain(''); }); it('rejects plain markdown', () => { expect(detectHtmlArtifact('# heading\n\nsome text')).toBeNull(); }); it('rejects message with prose before the doctype', () => { expect( detectHtmlArtifact('Here you go: '), ).toBeNull(); }); it('rejects empty input', () => { expect(detectHtmlArtifact('')).toBeNull(); expect(detectHtmlArtifact(' \n ')).toBeNull(); }); it('rejects fenced block without doctype/', () => { expect(detectHtmlArtifact('```html\n
x
\n```')).toBeNull(); }); it('accepts fenced block containing tag (no doctype)', () => { const r = detectHtmlArtifact('```html\nx\n```'); expect(r).toContain(''); }); }); describe('writeMarkdownArtifact / writeHtmlArtifact', () => { let projectRoot: string; beforeEach(async () => { projectRoot = await mkdtemp(join(tmpdir(), 'artifacts-test-')); }); afterEach(async () => { await rm(projectRoot, { recursive: true, force: true }); }); it('writes a markdown artifact under .boocode/artifacts/', async () => { const result = await writeMarkdownArtifact( { content: '# Hello\n\nbody' }, { projectId: 'pid', projectRoot }, ); expect(result.path).toMatch(/\.boocode\/artifacts\/hello-\d+\.md$/); expect(result.url).toMatch(/^\/api\/projects\/pid\/artifacts\/hello-\d+\.md$/); const written = await readFile(result.path, 'utf8'); expect(written).toBe('# Hello\n\nbody'); }); it('writes an html artifact', async () => { const result = await writeHtmlArtifact( { html_content: 'X', char_count: 56, title: 'X', }, { projectId: 'pid', projectRoot }, ); expect(result.path).toMatch(/\.boocode\/artifacts\/x-\d+\.html$/); const written = await readFile(result.path, 'utf8'); expect(written).toContain(''); }); it('creates the artifacts directory if absent', async () => { // Confirm the writer mkdir-recursive's the artifacts dir on first call. const result = await writeMarkdownArtifact( { content: '# T' }, { projectId: 'pid', projectRoot }, ); expect(result.path).toContain('.boocode/artifacts'); }); }); describe('1MB cap behavior', () => { it('reports the correct byte threshold', () => { expect(HTML_ARTIFACT_MAX_BYTES).toBe(1_048_576); }); it('exceeds threshold for oversize payload', () => { const oversize = '' + 'A'.repeat(HTML_ARTIFACT_MAX_BYTES); expect(Buffer.byteLength(oversize, 'utf8')).toBeGreaterThan( HTML_ARTIFACT_MAX_BYTES, ); }); it('detectHtmlArtifact still returns content above the cap (cap is checked by caller)', () => { // Detection is content-shape; the cap check lives in finalizeCompletion // (error-handler.ts). This test pins that contract: the helper does not // silently drop oversize payloads on the floor. const big = '' + 'x'.repeat(2_000_000); expect(detectHtmlArtifact(big)).not.toBeNull(); }); }); describe('decideHtmlArtifactWrite', () => { // Pure helper extracted from finalizeCompletion's cap-skip branch. Pins // the warn-and-skip decision without mocking the full InferenceContext. it('returns write=true for payloads under the cap', () => { const html = ''; const decision = decideHtmlArtifactWrite(html); expect(decision.write).toBe(true); expect(decision.byteLen).toBe(Buffer.byteLength(html, 'utf8')); }); it('returns write=false with cap_exceeded reason for oversize payloads', () => { const big = '' + 'x'.repeat(HTML_ARTIFACT_MAX_BYTES); const decision = decideHtmlArtifactWrite(big); expect(decision.write).toBe(false); if (!decision.write) { expect(decision.reason).toBe('cap_exceeded'); expect(decision.byteLen).toBeGreaterThan(HTML_ARTIFACT_MAX_BYTES); } }); it('accepts payload exactly at the cap (boundary)', () => { // byteLen === cap should write; only strictly greater skips. const exact = 'x'.repeat(HTML_ARTIFACT_MAX_BYTES); const decision = decideHtmlArtifactWrite(exact); expect(decision.write).toBe(true); expect(decision.byteLen).toBe(HTML_ARTIFACT_MAX_BYTES); }); }); describe('symlink escape protection', () => { // Closes the gap where `.boocode/artifacts` is a symlink pointing // outside the project root. The lexical prefix check on the resolved // candidate path passes (it's under projectRoot textually), but the // post-mkdir realpath verification must catch the escape. let projectRoot: string; let outside: string; beforeEach(async () => { projectRoot = await mkdtemp(join(tmpdir(), 'artifacts-symlink-root-')); outside = await mkdtemp(join(tmpdir(), 'artifacts-symlink-outside-')); }); afterEach(async () => { await rm(projectRoot, { recursive: true, force: true }); await rm(outside, { recursive: true, force: true }); }); it('throws PathScopeError when .boocode/artifacts is a symlink to outside the project', async () => { // Create .boocode dir, then make `artifacts` a symlink pointing outside. await mkdir(join(projectRoot, '.boocode'), { recursive: true }); await symlink(outside, join(projectRoot, '.boocode', 'artifacts')); await expect( writeMarkdownArtifact( { content: '# Hello' }, { projectId: 'pid', projectRoot }, ), ).rejects.toBeInstanceOf(PathScopeError); }); });