import { describe, expect, it } from 'vitest'; import type { PPTElement } from '@openmaic/dsl'; import type { StatelessChatRequest } from '@/lib/types/chat'; import { ElementReferenceValidationError, classifyMediaReference, normalizeElementHtml, resolveSlideElementReference, } from '@/lib/chat/pi/element-reference'; const base = { id: 'element-1', left: 10, top: 20, width: 300, height: 120, rotate: 5, }; function makeBody(element: PPTElement): StatelessChatRequest { return { messages: [], storeState: { stage: null, scenes: [ { id: 'scene-1', stageId: 'stage-1', title: 'Evidence page', order: 2, type: 'slide', content: { type: 'slide', canvas: { elements: [element] }, }, }, ], currentSceneId: 'scene-1', mode: 'playback', whiteboardOpen: false, }, config: { agentIds: ['default-1'] }, apiKey: '', elementReference: { kind: 'slide_element', sceneId: 'scene-1', elementId: 'element-1', }, } as unknown as StatelessChatRequest; } function resolve(element: PPTElement) { const result = resolveSlideElementReference(makeBody(element)); expect(result).toBeDefined(); return result!; } describe('slide element reference projector', () => { it('projects all 10 canonical element types without inventing line geometry', () => { const elements: PPTElement[] = [ { ...base, type: 'text', content: '

Hello world

', defaultFontName: 'Arial', defaultColor: '#000', textType: 'content', }, { ...base, type: 'latex', latex: 'x^2+y^2', align: 'center' }, { ...base, type: 'table', outline: {}, colWidths: [0.5, 0.5], rowHeights: [30], cellMinHeight: 20, data: [[{ id: 'cell-1', text: 'A', colspan: 1, rowspan: 1 }]], theme: { color: '#fff', rowHeader: true, rowFooter: false, colHeader: false, colFooter: false, }, }, { ...base, type: 'chart', chartType: 'bar', data: { labels: ['A'], legends: ['Series'], series: [[1]] }, themeColors: ['#000'], options: { stack: true }, }, { ...base, type: 'code', language: 'ts', fileName: 'main.ts', lines: [{ id: 'L1', content: 'let x = 1' }], }, { ...base, type: 'shape', viewBox: [100, 100], path: 'SECRET_PATH', fixedRatio: false, fill: '#fff', text: { content: '

shape label

', defaultFontName: 'Arial', defaultColor: '#000', align: 'middle', }, pathFormula: 'triangle', }, { id: base.id, left: base.left, top: base.top, width: base.width, type: 'line', start: [0, 0], end: [10, 10], style: 'dotted', color: '#000', points: ['', 'arrow'], }, { ...base, type: 'image', src: 'https://secret.test/image.png', fixedRatio: true, imageType: 'pageFigure', }, { ...base, type: 'video', src: 'blob:secret-video', mediaRef: 'video-ref-secret', poster: 'data:image/png;base64,SECRET', autoplay: true, ext: 'mp4', }, { ...base, type: 'audio', src: 'audio-ref-secret', fixedRatio: true, color: '#000', loop: true, autoplay: false, ext: 'mp3', }, ] as PPTElement[]; const packets = elements.map((element) => resolve(element).evidence); expect(packets.map((packet) => packet.elementType)).toEqual([ 'text', 'latex', 'table', 'chart', 'code', 'shape', 'line', 'image', 'video', 'audio', ]); expect(packets[0]).toMatchObject({ content: { text: 'Hello world', textType: 'content' } }); expect(packets[2]).toMatchObject({ content: { rows: [[{ id: 'cell-1', text: 'A' }]] } }); expect(packets[5]).not.toHaveProperty('content.path'); expect(packets[6].geometry).toEqual({ left: 10, top: 20, width: 300 }); }); it('uses deterministic Unicode and item bounds with truthful metadata', () => { const text = resolve({ ...base, type: 'text', content: `

${'😀'.repeat(12_001)}

`, defaultFontName: 'Arial', defaultColor: '#000', name: '名'.repeat(257), }); expect( Array.from( (text.evidence as Extract).content.text, ), ).toHaveLength(12_000); expect(text.evidence.truncatedFields).toEqual( expect.arrayContaining(['elementName', 'content.text']), ); const chart = resolve({ ...base, type: 'chart', chartType: 'line', data: { labels: Array.from({ length: 101 }, (_, index) => `label-${index}`), legends: Array.from({ length: 21 }, (_, index) => `legend-${index}`), series: Array.from({ length: 21 }, () => Array.from({ length: 101 }, (_, index) => index)), }, themeColors: [], }); expect(chart.evidence.omittedItems).toMatchObject({ 'content.labels': 1, 'content.legends': 1, 'content.series': 1, 'content.series[0]': 1, }); const code = resolve({ ...base, type: 'code', language: 'ts', lines: Array.from({ length: 201 }, (_, index) => ({ id: `L${index}`, content: 'x'.repeat(512), })), }); const codeEvidence = code.evidence as Extract; expect(codeEvidence.content.lines).toHaveLength(24); expect(codeEvidence.omittedItems['content.lines']).toBe(177); expect( codeEvidence.content.lines.reduce((sum, line) => sum + Array.from(line.content).length, 0), ).toBe(12_000); }); it('classifies media without exposing raw references or substrings', () => { expect(classifyMediaReference(undefined)).toEqual({ kind: 'absent' }); expect(classifyMediaReference('data:secret-marker')).toEqual({ kind: 'embedded' }); expect(classifyMediaReference('blob:secret-marker')).toEqual({ kind: 'local' }); expect(classifyMediaReference('https://secret-marker.test')).toEqual({ kind: 'external' }); expect(classifyMediaReference('opaque-secret-marker')).toEqual({ kind: 'reference' }); const packet = resolve({ ...base, type: 'video', src: 'https://src-secret-marker.test/video', mediaRef: 'opaque-media-secret-marker', poster: 'data:image/png;base64,poster-secret-marker', autoplay: false, }); const serialized = JSON.stringify(packet.evidence); expect(serialized).not.toContain('src-secret-marker'); expect(serialized).not.toContain('media-secret-marker'); expect(serialized).not.toContain('poster-secret-marker'); }); it('projects renderer-tolerated legacy Charts with missing data arrays as empty evidence', () => { const chart = { ...base, type: 'chart', chartType: 'line', themeColors: [], }; const missingData = resolve(chart as unknown as PPTElement).evidence as Extract< ReturnType['evidence'], { elementType: 'chart' } >; expect(missingData.content).toMatchObject({ labels: [], legends: [], series: [] }); const missingLabels = resolve({ ...chart, data: { legends: ['Series'], series: [[180, 88]] }, } as unknown as PPTElement).evidence as typeof missingData; expect(missingLabels.content).toMatchObject({ labels: [], legends: ['Series'], series: [[180, 88]], }); const missingSeries = resolve({ ...chart, data: { labels: ['First'], legends: ['Series'] }, } as unknown as PPTElement).evidence as typeof missingData; expect(missingSeries.content).toMatchObject({ labels: ['First'], legends: ['Series'], series: [], }); }); it('strictly rejects malformed, stale, duplicate, non-slide, and unsupported references', () => { const element = { ...base, type: 'text', content: 'hello', defaultFontName: 'Arial', defaultColor: '#000', } as PPTElement; const extraKey = makeBody(element) as StatelessChatRequest & { elementReference: Record; }; extraKey.elementReference.extra = 'browser-content'; expect(() => resolveSlideElementReference(extraKey)).toThrow(ElementReferenceValidationError); const stale = makeBody(element); stale.elementReference!.elementId = 'missing'; expect(() => resolveSlideElementReference(stale)).toThrow(/exactly one element/); const duplicate = makeBody(element); duplicate.storeState.scenes.push(duplicate.storeState.scenes[0]); expect(() => resolveSlideElementReference(duplicate)).toThrow(/exactly one Scene/); const nonSlide = makeBody(element); nonSlide.storeState.scenes[0] = { ...nonSlide.storeState.scenes[0], type: 'quiz', content: { type: 'quiz', questions: [] }, } as never; expect(() => resolveSlideElementReference(nonSlide)).toThrow(/slide Scene/); const unsupported = makeBody({ ...element, type: 'future-element' } as never); expect(() => resolveSlideElementReference(unsupported)).toThrow(/unsupported element type/); }); it('normalizes HTML by replacing tag boundaries and collapsing whitespace', () => { expect(normalizeElementHtml('

A

B C

')).toBe('A B C'); }); it('omits script and style blocks instead of exposing their source as visible text', () => { expect( normalizeElementHtml( '

Visible

After

', ), ).toBe('Visible After'); }); });