diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index 0ed8fe2..7f76045 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -780,6 +780,11 @@ export class NoteRepository { * 'capture' 첫 revision INSERT (createdAt = edited_at). 미수행 시 first user * edit 직후 import 시점 본문이 history 에서 사라지는 회귀 (final review 발견). * + * v0.2.11 Cut D — INSERT/fork 시 tags 추가 후 rebuildFtsTagsForNote(finalId) + * 호출 — m007 trigger 가 빈 tags='' 로 FTS row 만들고, note_tags INSERT 만으로는 + * notes_fts.tags 갱신 안 됨. 미수행 시 import 한 노트가 tag keyword 검색에서 + * 매칭 안 되는 회귀 (final review 발견). + * * deletedAt merge (v0.2.3 #4, spec §8.2): source/dest 중 IS NOT NULL 우선 * (삭제 보존). skip 케이스에서 source NN + dest NULL 일 때만 dest 갱신. * insert/fork 는 source 의 deletedAt 그대로 보존. @@ -850,6 +855,8 @@ export class NoteRepository { if (t.source === 'ai') linkAi.run(finalId, row.id); else linkUser.run(finalId, row.id); } + // v0.2.11 Cut D — note_tags 변경 후 notes_fts.tags 동기화 (single write path). + this.rebuildFtsTagsForNote(finalId); } }); tx(); diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 6a59bab..82002ff 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -1103,4 +1103,57 @@ describe('NoteRepository — notes_fts tags sync (v0.2.11 Cut D)', () => { .get(id) as { tags: string }; expect(row.tags.split(' ').sort()).toEqual(['new1', 'new2']); }); + + it('importNote insert path: notes_fts.tags 가 csv 로 sync (final review fix)', () => { + const repo = new NoteRepository(db); + const r = repo.importNote({ + id: '00000000-0000-0000-0000-000000000010', + rawText: 'imported with tags', + createdAt: '2026-04-01T00:00:00Z', + updatedAt: '2026-04-01T00:00:00Z', + aiTitle: 'imported title', + aiSummary: 'imported summary', + titleEditedByUser: false, + summaryEditedByUser: false, + aiProvider: 'p', + aiGeneratedAt: '2026-04-01T00:00:00Z', + userIntent: null, + intentPromptedAt: null, + tags: [ + { name: '기획', source: 'ai' }, + { name: '회의', source: 'user' } + ] + }); + expect(r.status).toBe('inserted'); + const row = db + .prepare(`SELECT tags FROM notes_fts WHERE note_id=?`) + .get(r.id) as { tags: string }; + expect(row.tags.split(' ').sort()).toEqual(['기획', '회의']); + }); + + it('importNote fork path: forked 노트의 notes_fts.tags 동기 (final review fix)', () => { + const repo = new NoteRepository(db); + const existing = repo.create({ rawText: 'v1' }); + const r = repo.importNote({ + id: existing.id, + rawText: 'imported v2 with tags', + createdAt: '2026-04-01T00:00:00Z', + updatedAt: '2026-04-01T00:00:00Z', + aiTitle: null, + aiSummary: null, + titleEditedByUser: false, + summaryEditedByUser: false, + aiProvider: null, + aiGeneratedAt: null, + userIntent: null, + intentPromptedAt: null, + tags: [{ name: '결재', source: 'user' }] + }); + expect(r.status).toBe('forked'); + expect(r.id).not.toBe(existing.id); + const row = db + .prepare(`SELECT tags FROM notes_fts WHERE note_id=?`) + .get(r.id) as { tags: string }; + expect(row.tags).toBe('결재'); + }); });