125 lines
4.1 KiB
TypeScript
125 lines
4.1 KiB
TypeScript
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('"쉼표, 따옴표 ""확인"""');
|
|
});
|
|
});
|