import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import Database from 'better-sqlite3'; import { runMigrations } from '../../src/main/db/migrations/index.js'; import { NoteRepository } from '../../src/main/repository/NoteRepository.js'; const baseInput = { id: '00000000-0000-0000-0000-000000000001', rawText: 'sync 본문', createdAt: '2026-05-09T00:00:00Z', updatedAt: '2026-05-10T00:00:00Z', aiTitle: 'sync 제목', aiSummary: 'sync 요약', titleEditedByUser: false, summaryEditedByUser: false, aiProvider: 'p', aiGeneratedAt: '2026-05-10T00:00:00Z', userIntent: null, intentPromptedAt: null, tags: [{ name: '동기', source: 'user' as const }], status: 'active' as const, statusChangedAt: null, moveReason: null, dueDate: null, dueDateEditedByUser: false }; describe('NoteRepository.upsertFromSync', () => { let db: Database.Database; let repo: NoteRepository; beforeEach(() => { db = new Database(':memory:'); db.pragma('foreign_keys = ON'); runMigrations(db); repo = new NoteRepository(db); }); afterEach(() => { db.close(); }); it('id 없음 → INSERT (status=inserted) + capture revision + tags FTS sync', () => { const r = repo.upsertFromSync(baseInput); expect(r.status).toBe('inserted'); expect(r.id).toBe(baseInput.id); const note = repo.findById(baseInput.id); expect(note?.rawText).toBe('sync 본문'); expect(note?.aiTitle).toBe('sync 제목'); const revs = repo.listRevisions(baseInput.id); expect(revs).toHaveLength(1); expect(revs[0]!.editedBy).toBe('capture'); const fts = db.prepare(`SELECT tags FROM notes_fts WHERE note_id=?`).get(baseInput.id) as { tags: string }; expect(fts.tags).toBe('동기'); }); it('id 있음 + raw_text 동일 + source 더 최신 → metadata 갱신 (status=updated)', () => { const created = repo.create({ rawText: 'sync 본문' }, new Date('2026-05-09T00:00:00Z')); repo.updateAiResult(created.id, { title: '옛 제목', summary: '옛 요약', tags: ['old'], provider: 'p' }); db.prepare(`UPDATE notes SET updated_at=? WHERE id=?`).run('2026-05-08T00:00:00Z', created.id); const r = repo.upsertFromSync({ ...baseInput, id: created.id }); expect(r.status).toBe('updated'); const note = repo.findById(created.id); expect(note?.aiTitle).toBe('sync 제목'); expect(note?.tags.map((t) => t.name)).toEqual(['동기']); const fts = db.prepare(`SELECT tags FROM notes_fts WHERE note_id=?`).get(created.id) as { tags: string }; expect(fts.tags).toBe('동기'); }); it('id 있음 + raw_text 동일 + source 더 옛 → skip (status=skipped)', () => { const created = repo.create({ rawText: 'sync 본문' }); repo.updateAiResult(created.id, { title: '신선한 제목', summary: 'fresh', tags: ['x'], provider: 'p' }); db.prepare(`UPDATE notes SET updated_at=? WHERE id=?`).run('2026-05-12T00:00:00Z', created.id); const r = repo.upsertFromSync({ ...baseInput, id: created.id, updatedAt: '2026-05-10T00:00:00Z' }); expect(r.status).toBe('skipped'); const note = repo.findById(created.id); expect(note?.aiTitle).toBe('신선한 제목'); }); it('id 있음 + raw_text 다름 + source 더 최신 → updateRawText (status=updated) + new user revision', () => { const created = repo.create({ rawText: 'old text' }, new Date('2026-05-09T00:00:00Z')); db.prepare(`UPDATE notes SET updated_at=? WHERE id=?`).run('2026-05-08T00:00:00Z', created.id); const r = repo.upsertFromSync({ ...baseInput, id: created.id, rawText: 'new sync text' }); expect(r.status).toBe('updated'); const note = repo.findById(created.id); expect(note?.rawText).toBe('new sync text'); const revs = repo.listRevisions(created.id); expect(revs).toHaveLength(2); // capture (old) + user (new) expect(revs[0]!.editedBy).toBe('user'); expect(revs[0]!.rawText).toBe('new sync text'); }); it('id 있음 + raw_text 다름 + source 더 옛 → skip', () => { const created = repo.create({ rawText: 'local fresh' }); db.prepare(`UPDATE notes SET updated_at=? WHERE id=?`).run('2026-05-15T00:00:00Z', created.id); const r = repo.upsertFromSync({ ...baseInput, id: created.id, rawText: 'old sync text', updatedAt: '2026-05-10T00:00:00Z' }); expect(r.status).toBe('skipped'); const note = repo.findById(created.id); expect(note?.rawText).toBe('local fresh'); }); });