fix(v0210): importNote 가 capture revision 을 함께 INSERT (final review fix)
final code review 발견: F5 import 후 first user edit 시 import 시점 본문이 note_revisions 에 없어 history 에서 사라지는 회귀. importNote transaction 안 INSERT 추가 (createdAt = edited_at). 부수 작업: ImportNoteInput / importNote 의 "raw_text invariant guard" 주석을 v0.2.10 의 'fork-on-id-collision (sync determinism)' 정확한 의미로 갱신. 테스트 +2 — insert path / fork path 모두 capture revision 검증.
This commit is contained in:
@@ -22,7 +22,10 @@ export interface NewMediaRow {
|
||||
|
||||
export interface ImportNoteInput {
|
||||
/** Proposed id from the export file. May be replaced if it collides with
|
||||
* an existing row whose `raw_text` differs (raw_text invariant guard). */
|
||||
* an existing row whose `raw_text` differs — fork-on-conflict so a single
|
||||
* id never resolves to two distinct historical baselines (v0.2.10 Cut C
|
||||
* changed `raw_text 불변` policy → `raw_text 가변` + revision history; the
|
||||
* baseline distinction is now preserved per-id, edit history per-note). */
|
||||
id: string;
|
||||
rawText: string;
|
||||
createdAt: string;
|
||||
@@ -681,11 +684,17 @@ export class NoteRepository {
|
||||
|
||||
/**
|
||||
* Import a note from an external source (F5 export tree).
|
||||
* Conflict policy:
|
||||
* Conflict policy (fork-on-id-collision):
|
||||
* - id missing in DB → INSERT (status: 'inserted')
|
||||
* - id present + raw_text identical → no-op (status: 'skipped')
|
||||
* - id present + raw_text differs → INSERT under fresh uuidv7
|
||||
* to preserve the raw_text-immutable invariant (status: 'forked')
|
||||
* - id present + raw_text differs → INSERT under fresh uuidv7 so the same id
|
||||
* never points at two different baselines (status: 'forked'). v0.2.10 Cut C
|
||||
* relaxed the `raw_text 불변` policy → `raw_text 가변 + note_revisions 보존`,
|
||||
* but per-id baseline distinction is still required for sync determinism.
|
||||
*
|
||||
* v0.2.10 Cut C — INSERT/fork 시 동일 transaction 안에서 note_revisions 에
|
||||
* 'capture' 첫 revision INSERT (createdAt = edited_at). 미수행 시 first user
|
||||
* edit 직후 import 시점 본문이 history 에서 사라지는 회귀 (final review 발견).
|
||||
*
|
||||
* deletedAt merge (v0.2.3 #4, spec §8.2): source/dest 중 IS NOT NULL 우선
|
||||
* (삭제 보존). skip 케이스에서 source NN + dest NULL 일 때만 dest 갱신.
|
||||
@@ -736,6 +745,12 @@ export class NoteRepository {
|
||||
input.createdAt,
|
||||
input.updatedAt
|
||||
);
|
||||
this.db
|
||||
.prepare(
|
||||
`INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by)
|
||||
VALUES (?, ?, ?, 'capture')`
|
||||
)
|
||||
.run(finalId, input.rawText, input.createdAt);
|
||||
if (input.tags.length > 0) {
|
||||
const getOrInsertTag = this.db.prepare(
|
||||
`INSERT INTO tags(name) VALUES(?) ON CONFLICT(name) DO UPDATE SET name=name RETURNING id`
|
||||
|
||||
@@ -107,4 +107,65 @@ describe('NoteRepository — note_revisions', () => {
|
||||
expect(note?.rawText).toBe('v2 corrected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('importNote — capture revision 생성 (final review 보강)', () => {
|
||||
it('insert path: imported note 가 capture revision (createdAt = edited_at) 을 함께 갖는다', () => {
|
||||
const r = repo.importNote({
|
||||
id: '00000000-0000-0000-0000-000000000001',
|
||||
rawText: 'imported text',
|
||||
createdAt: '2026-04-01T00:00:00Z',
|
||||
updatedAt: '2026-04-02T00:00:00Z',
|
||||
aiTitle: 't',
|
||||
aiSummary: 's',
|
||||
titleEditedByUser: false,
|
||||
summaryEditedByUser: false,
|
||||
aiProvider: 'p',
|
||||
aiGeneratedAt: '2026-04-02T00:00:00Z',
|
||||
userIntent: null,
|
||||
intentPromptedAt: null,
|
||||
tags: []
|
||||
});
|
||||
expect(r.status).toBe('inserted');
|
||||
|
||||
const revs = repo.listRevisions(r.id);
|
||||
expect(revs).toHaveLength(1);
|
||||
expect(revs[0]!.rawText).toBe('imported text');
|
||||
expect(revs[0]!.editedBy).toBe('capture');
|
||||
expect(revs[0]!.editedAt).toBe('2026-04-01T00:00:00Z');
|
||||
});
|
||||
|
||||
it('fork path: id 충돌 시 fresh uuidv7 + 새 capture revision (옛 노트 revision 보존)', () => {
|
||||
// 기존 노트 (capture 'v1' revision 자동 생성됨)
|
||||
const existing = repo.create({ rawText: 'v1' });
|
||||
// 동일 id 로 다른 raw_text 를 import → fork
|
||||
const r = repo.importNote({
|
||||
id: existing.id,
|
||||
rawText: 'imported v2',
|
||||
createdAt: '2026-04-01T00:00:00Z',
|
||||
updatedAt: '2026-04-02T00:00:00Z',
|
||||
aiTitle: null,
|
||||
aiSummary: null,
|
||||
titleEditedByUser: false,
|
||||
summaryEditedByUser: false,
|
||||
aiProvider: null,
|
||||
aiGeneratedAt: null,
|
||||
userIntent: null,
|
||||
intentPromptedAt: null,
|
||||
tags: []
|
||||
});
|
||||
expect(r.status).toBe('forked');
|
||||
expect(r.id).not.toBe(existing.id);
|
||||
|
||||
// forked 노트에 capture revision
|
||||
const forkRevs = repo.listRevisions(r.id);
|
||||
expect(forkRevs).toHaveLength(1);
|
||||
expect(forkRevs[0]!.rawText).toBe('imported v2');
|
||||
expect(forkRevs[0]!.editedBy).toBe('capture');
|
||||
|
||||
// 기존 노트의 revision 은 그대로 보존
|
||||
const existingRevs = repo.listRevisions(existing.id);
|
||||
expect(existingRevs).toHaveLength(1);
|
||||
expect(existingRevs[0]!.rawText).toBe('v1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user