fix(v0211): importNote 가 rebuildFtsTagsForNote 호출 (final review fix)
final code review 발견: F5 import path 가 note_tags INSERT 후 notes_fts.tags 갱신 안 해서 import 한 노트의 tag 가 keyword 검색에서 매칭 안 되는 회귀. Cut C 의 importNote capture revision 누락 패턴과 동일 — single write path 정책 (Cut D 도입) 의 강제 검사 누락. importNote transaction 끝에서 호출하도록 fix + 회귀 test 2건 (insert path / fork path) 추가. NoteRepository 안 note_tags INSERT path 는 updateAiResult / updateUserAiFields / importNote 3곳, 셋 다 rebuildFtsTagsForNote 호출 보장 — invariant 회복.
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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('결재');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user