import { describe, it, expect } from 'vitest'; import { composeMarkdown, type ExportNote } from '@main/services/exportFormat.js'; import { parseExportNote } from '@main/services/importFormat.js'; const baseNote: ExportNote = { id: '014a3b9c-1234-7890-abcd-000000000001', createdAt: '2026-04-25T14:23:11.000Z', updatedAt: '2026-04-25T14:24:02.000Z', rawText: '회고 메모 본문', aiTitle: '주간 회고 PR 리뷰', aiSummary: '회고 양식 통일을 위한 메모.', titleEditedByUser: false, summaryEditedByUser: false, aiProvider: 'local-ollama/gemma4:e4b', aiGeneratedAt: '2026-04-25T14:23:34.000Z', userIntent: null, intentPromptedAt: null, status: 'active', statusChangedAt: null, moveReason: null, dueDate: null, dueDateEditedByUser: false, tags: [{ name: 'pr', source: 'ai' }, { name: 'review', source: 'user' }], media: [] }; describe('parseExportNote — round-trip with composeMarkdown', () => { it('round-trips the base note', () => { const md = composeMarkdown(baseNote); const parsed = parseExportNote(md); expect(parsed.id).toBe(baseNote.id); expect(parsed.createdAt).toBe(baseNote.createdAt); expect(parsed.updatedAt).toBe(baseNote.updatedAt); expect(parsed.rawText).toBe(baseNote.rawText); expect(parsed.aiTitle).toBe(baseNote.aiTitle); expect(parsed.aiSummary).toBe(baseNote.aiSummary); expect(parsed.aiProvider).toBe(baseNote.aiProvider); expect(parsed.aiGeneratedAt).toBe(baseNote.aiGeneratedAt); expect(parsed.titleEditedByUser).toBe(false); expect(parsed.summaryEditedByUser).toBe(false); expect(parsed.tags).toEqual([ { name: 'pr', source: 'ai' }, { name: 'review', source: 'user' } ]); expect(parsed.images).toEqual([]); expect(parsed.exportVersion).toBe(1); }); it('round-trips a note with media', () => { const note: ExportNote = { ...baseNote, media: [ { rel: 'media/014a3b9c__1.png', mime: 'image/png', bytes: 1234 }, { rel: 'media/014a3b9c__2.jpg', mime: 'image/jpeg', bytes: 5678 } ] }; const md = composeMarkdown(note); const parsed = parseExportNote(md); expect(parsed.images).toEqual([ { rel: 'media/014a3b9c__1.png', mime: 'image/png', bytes: 1234 }, { rel: 'media/014a3b9c__2.jpg', mime: 'image/jpeg', bytes: 5678 } ]); expect(parsed.rawText).toBe(note.rawText); }); }); describe('parseExportNote — frontmatter scalar variants', () => { it('parses plain scalar', () => { const md = composeMarkdown({ ...baseNote, aiTitle: '주간 회고' }); const parsed = parseExportNote(md); expect(parsed.aiTitle).toBe('주간 회고'); }); it('parses single-quoted with embedded apostrophe (`` `` escape)', () => { const note: ExportNote = { ...baseNote, aiTitle: "it's a: title" }; const md = composeMarkdown(note); // Should be emitted as: title: 'it''s a: title' expect(md).toContain("title: 'it''s a: title'"); const parsed = parseExportNote(md); expect(parsed.aiTitle).toBe("it's a: title"); }); it('parses block scalar `|-` for multiline summary', () => { const note: ExportNote = { ...baseNote, aiSummary: 'line1\nline2\nline3' }; const md = composeMarkdown(note); expect(md).toContain('summary: |-'); const parsed = parseExportNote(md); expect(parsed.aiSummary).toBe('line1\nline2\nline3'); }); }); describe('parseExportNote — list parsing', () => { it('parses tags inline flow', () => { const md = composeMarkdown({ ...baseNote, tags: [ { name: 'foo', source: 'ai' }, { name: 'bar baz', source: 'user' } ] }); const parsed = parseExportNote(md); expect(parsed.tags).toEqual([ { name: 'foo', source: 'ai' }, { name: 'bar baz', source: 'user' } ]); }); it('parses images list with mime + bytes', () => { const md = composeMarkdown({ ...baseNote, media: [{ rel: 'media/014a3b9c__1.png', mime: 'image/png', bytes: 9876 }] }); const parsed = parseExportNote(md); expect(parsed.images).toEqual([ { rel: 'media/014a3b9c__1.png', mime: 'image/png', bytes: 9876 } ]); }); }); describe('parseExportNote — body extraction', () => { it('extracts rawText with summary present', () => { const md = composeMarkdown({ ...baseNote, rawText: '본문\n두 번째 줄' }); const parsed = parseExportNote(md); expect(parsed.rawText).toBe('본문\n두 번째 줄'); }); it('extracts rawText with summary absent', () => { const md = composeMarkdown({ ...baseNote, aiSummary: null, rawText: '요약 없는 본문' }); const parsed = parseExportNote(md); expect(parsed.rawText).toBe('요약 없는 본문'); }); it('extracts rawText with no images', () => { const md = composeMarkdown({ ...baseNote, rawText: '이미지 없음', media: [] }); const parsed = parseExportNote(md); expect(parsed.rawText).toBe('이미지 없음'); }); it('preserves `>` mid-line in rawText (not parsed as blockquote)', () => { const md = composeMarkdown({ ...baseNote, rawText: '값 a > b 라는 부등호' }); const parsed = parseExportNote(md); expect(parsed.rawText).toBe('값 a > b 라는 부등호'); }); it('preserves `# ` mid-line in rawText (not parsed as heading)', () => { const md = composeMarkdown({ ...baseNote, rawText: '예시: see issue #1 어쩌고 # 가운데 해시' }); const parsed = parseExportNote(md); expect(parsed.rawText).toBe('예시: see issue #1 어쩌고 # 가운데 해시'); }); }); describe('parseExportNote — provenance', () => { it('recovers titleEditedByUser from title_source: user', () => { const md = composeMarkdown({ ...baseNote, titleEditedByUser: true }); const parsed = parseExportNote(md); expect(parsed.titleEditedByUser).toBe(true); }); it('recovers summaryEditedByUser from summary_source: user', () => { const md = composeMarkdown({ ...baseNote, summaryEditedByUser: true }); const parsed = parseExportNote(md); expect(parsed.summaryEditedByUser).toBe(true); }); it('exposes exportVersion = 1', () => { const md = composeMarkdown(baseNote); const parsed = parseExportNote(md); expect(parsed.exportVersion).toBe(1); }); }); describe('parseExportNote — status/dueDate/moveReason round-trip (v0.3.0 Cut E)', () => { it('round-trips status=active (default)', () => { const md = composeMarkdown(baseNote); const parsed = parseExportNote(md); expect(parsed.status).toBe('active'); expect(parsed.statusChangedAt).toBeNull(); expect(parsed.moveReason).toBeNull(); expect(parsed.dueDate).toBeNull(); expect(parsed.dueDateEditedByUser).toBe(false); }); it('round-trips status=archived with statusChangedAt and moveReason', () => { const note: ExportNote = { ...baseNote, status: 'archived', statusChangedAt: '2026-05-01T10:00:00Z', moveReason: 'project done' }; const md = composeMarkdown(note); const parsed = parseExportNote(md); expect(parsed.status).toBe('archived'); expect(parsed.statusChangedAt).toBe('2026-05-01T10:00:00Z'); expect(parsed.moveReason).toBe('project done'); }); it('round-trips dueDate with dueDateEditedByUser=true', () => { const note: ExportNote = { ...baseNote, dueDate: '2026-06-15', dueDateEditedByUser: true }; const md = composeMarkdown(note); const parsed = parseExportNote(md); expect(parsed.dueDate).toBe('2026-06-15'); expect(parsed.dueDateEditedByUser).toBe(true); }); it('round-trips dueDate with dueDateEditedByUser=false (ai source)', () => { const note: ExportNote = { ...baseNote, dueDate: '2026-07-01', dueDateEditedByUser: false }; const md = composeMarkdown(note); const parsed = parseExportNote(md); expect(parsed.dueDate).toBe('2026-07-01'); expect(parsed.dueDateEditedByUser).toBe(false); }); it('defaults to status=active for older exports without status field', () => { // Simulate a pre-Cut E export that has no status line const md = `---\nid: 014a3b9c-1234-7890-abcd-000000000001\ncreated_at: 2026-04-25T14:23:11.000Z\nupdated_at: 2026-04-25T14:24:02.000Z\ntitle: t\ntitle_source: ai\nsummary: s\nsummary_source: ai\ninkling_export_version: 1\n---\n\n# t\n\n> s\n\nbody\n`; const parsed = parseExportNote(md); expect(parsed.status).toBe('active'); expect(parsed.dueDate).toBeNull(); expect(parsed.moveReason).toBeNull(); expect(parsed.dueDateEditedByUser).toBe(false); }); }); describe('parseExportNote — edge cases', () => { it('preserves user_intent when present', () => { const md = composeMarkdown({ ...baseNote, userIntent: '팀에서 회고 양식 통일', intentPromptedAt: '2026-04-25T14:24:02.000Z' }); const parsed = parseExportNote(md); expect(parsed.userIntent).toBe('팀에서 회고 양식 통일'); expect(parsed.intentPromptedAt).toBe('2026-04-25T14:24:02.000Z'); }); it('returns null aiTitle / aiSummary when omitted', () => { const md = composeMarkdown({ ...baseNote, aiTitle: null, aiSummary: null }); const parsed = parseExportNote(md); expect(parsed.aiTitle).toBeNull(); expect(parsed.aiSummary).toBeNull(); }); it('throws when input lacks frontmatter delimiter', () => { expect(() => parseExportNote('hello world')).toThrow(); }); });