Files
inkling/tests/unit/importFormat.test.ts

275 lines
9.3 KiB
TypeScript

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();
});
});