import { buildDictionaryCsv, buildDocx, buildMeetingExport, buildMeetingMarkdown, buildPdf, type MeetingExportInput, type PortableDictionaryRow, } from '../src/features/data-portability'; const OWNER = '4f642f87-2d12-4dcf-9aa1-f4427d4078a8'; function input(): MeetingExportInput { return { meeting: { id: '292a4f9d-288e-4277-aab6-b07132a80c69', user_id: OWNER, team_id: null, title: '모바일 출시 점검', status: 'completed', started_at: '2026-08-21T00:00:00.000Z', ended_at: '2026-08-21T01:00:00.000Z', duration_ms: 3_600_000, raw_transcript: null, edited_transcript: null, minutes_markdown: '## 요약\n\n원자적 복원을 검증했다.', minutes_json: null, stt_model: 'whisper-large-v3', llm_model: 'gpt-5', stt_latency_ms: 100, llm_latency_ms: 200, error_message: null, audio_storage_key: null, created_at: '2026-08-21T00:00:00.000Z', updated_at: '2026-08-21T01:00:00.000Z', }, transcripts: [ { id: '2d47b3b6-a0dc-4ce4-8cd4-37b32ef3a29c', meeting_id: '292a4f9d-288e-4277-aab6-b07132a80c69', segment_index: 0, timestamp_ms: 5_000, duration_ms: 1_000, text: '전체 실패는 롤백합니다.', speaker: '화자 1', edited: false, created_at: '2026-08-21T00:00:05.000Z', updated_at: '2026-08-21T00:00:05.000Z', }, ], memos: [ { id: '8ebac53d-6bb6-4da6-973a-9d4e12e93d3c', meeting_id: '292a4f9d-288e-4277-aab6-b07132a80c69', user_id: OWNER, content: '체크섬 | 재검증', timestamp_ms: 3_000, created_at: '2026-08-21T00:00:03.000Z', }, ], documents: [], }; } function ascii(bytes: Uint8Array): string { return String.fromCharCode(...bytes.slice(0, 32)); } describe('meeting and tabular exports', () => { it('builds Markdown with transcript, memo escaping, and metadata', () => { const markdown = buildMeetingMarkdown(input()); expect(markdown).toContain('# Meeting Minutes — 모바일 출시 점검'); expect(markdown).toContain('체크섬 \\| 재검증'); expect(markdown).toContain('[00:05] 화자 1: 전체 실패는 롤백합니다.'); }); it('creates a structurally valid PDF container with Korean CID text', () => { const pdf = buildPdf(buildMeetingMarkdown(input())); const complete = String.fromCharCode(...pdf); expect(ascii(pdf)).toMatch(/^%PDF-1\.4/); expect(complete).toContain('/UniKS-UCS2-H'); expect(complete).toContain('%%EOF'); }); it('creates an uncompressed OOXML DOCX ZIP containing Unicode document XML', () => { const docx = buildDocx( buildMeetingMarkdown(input()), '모바일 출시 점검', new Date('2026-08-21T00:00:00.000Z'), ); const complete = String.fromCharCode(...docx); expect(ascii(docx)).toMatch(/^PK\x03\x04/); expect(complete).toContain('word/document.xml'); expect(complete).toContain('[Content_Types].xml'); expect(complete).toContain('PK\x05\x06'); }); it('returns all four real meeting output formats', () => { expect(buildMeetingExport(input(), 'md').mimeType).toBe('text/markdown'); expect(buildMeetingExport(input(), 'txt').mimeType).toBe('text/plain'); expect(buildMeetingExport(input(), 'pdf').bytes?.slice(0, 5)).toEqual( Uint8Array.from([37, 80, 68, 70, 45]), ); expect(buildMeetingExport(input(), 'docx').bytes?.slice(0, 2)).toEqual( Uint8Array.from([80, 75]), ); }); it('neutralizes spreadsheet formulas while preserving RFC4180 fields', () => { const row: PortableDictionaryRow = { id: '7e050e11-c90c-4e95-8581-d37ad687d836', user_id: OWNER, word: '=HYPERLINK("https://attacker.invalid")', pronunciation: '쉼표, 따옴표 "확인"', category: 'user', usage_count: 0, last_used_at: null, created_at: '2026-08-21T00:00:00.000Z', updated_at: '2026-08-21T00:00:00.000Z', }; const csv = buildDictionaryCsv([row]); expect(csv).toContain("'=HYPERLINK"); expect(csv).toContain('"쉼표, 따옴표 ""확인"""'); }); });