Files
inkling/tests/unit/NoteRevisions.test.ts
altair823 7541d3c9e4 feat(v0210): NoteRepository revision API + NoteRevision type + InboxApi 시그니처
- updateRawText: raw_text 갱신 + user revision INSERT (atomic)
- listRevisions: edited_at DESC 순 hydrate
- restoreRevision: 옛 raw_text 를 새 user revision 으로 복원 (chain 보존)
- shared/types: NoteRevision + InboxApi 3 메서드 (updateRawText/listRevisions/restoreRevision)
- preload: 3 IPC stub 추가 (inbox:update-raw-text / inbox:list-revisions / inbox:restore-revision)
2026-05-09 20:41:17 +09:00

111 lines
4.5 KiB
TypeScript

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';
describe('NoteRepository — note_revisions', () => {
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(); });
describe('updateRawText', () => {
it('notes.raw_text 갱신 + 새 user revision INSERT (single transaction)', () => {
const { id } = repo.create({ rawText: 'v1' });
const t = new Date('2026-05-10T00:00:00Z');
repo.updateRawText(id, 'v2', t);
const note = db.prepare(`SELECT raw_text, updated_at FROM notes WHERE id=?`).get(id) as {
raw_text: string;
updated_at: string;
};
expect(note.raw_text).toBe('v2');
expect(note.updated_at).toBe('2026-05-10T00:00:00.000Z');
const revs = db
.prepare(`SELECT raw_text, edited_by, edited_at FROM note_revisions WHERE note_id=? ORDER BY rev_id ASC`)
.all(id) as Array<{ raw_text: string; edited_by: string; edited_at: string }>;
expect(revs).toHaveLength(2); // capture + user
expect(revs.at(0)!.edited_by).toBe('capture');
expect(revs.at(0)!.raw_text).toBe('v1');
expect(revs.at(1)!.edited_by).toBe('user');
expect(revs.at(1)!.raw_text).toBe('v2');
expect(revs.at(1)!.edited_at).toBe('2026-05-10T00:00:00.000Z');
});
it('atomic: 두 번 호출 시 두 revision 모두 누적 (chain history)', () => {
const { id } = repo.create({ rawText: 'v1' });
repo.updateRawText(id, 'v2', new Date('2026-05-10T00:00:00Z'));
repo.updateRawText(id, 'v3', new Date('2026-05-11T00:00:00Z'));
const revs = db
.prepare(`SELECT raw_text FROM note_revisions WHERE note_id=? ORDER BY rev_id ASC`)
.all(id) as Array<{ raw_text: string }>;
expect(revs.map((r) => r.raw_text)).toEqual(['v1', 'v2', 'v3']);
});
});
describe('listRevisions', () => {
it('DESC 순서 + edited_by + camelCase hydrate', () => {
const { id } = repo.create({ rawText: 'v1' });
repo.updateRawText(id, 'v2', new Date('2026-05-10T00:00:00Z'));
repo.updateRawText(id, 'v3', new Date('2026-05-11T00:00:00Z'));
const revs = repo.listRevisions(id);
expect(revs).toHaveLength(3);
expect(revs.at(0)!.rawText).toBe('v3');
expect(revs.at(0)!.editedBy).toBe('user');
expect(revs.at(1)!.rawText).toBe('v2');
expect(revs.at(1)!.editedBy).toBe('user');
expect(revs.at(2)!.rawText).toBe('v1');
expect(revs.at(2)!.editedBy).toBe('capture');
expect(typeof revs.at(0)!.revId).toBe('number');
expect(revs.at(0)!.noteId).toBe(id);
expect(revs.at(0)!.editedAt).toBe('2026-05-11T00:00:00.000Z');
});
});
describe('restoreRevision', () => {
it('옛 raw_text 를 새 user revision 으로 INSERT + notes.raw_text 갱신', () => {
const { id } = repo.create({ rawText: 'v1' });
repo.updateRawText(id, 'v2', new Date('2026-05-10T00:00:00Z'));
repo.updateRawText(id, 'v3', new Date('2026-05-11T00:00:00Z'));
const revs = repo.listRevisions(id);
const v1 = revs.find((r) => r.rawText === 'v1');
expect(v1).toBeDefined();
repo.restoreRevision(id, v1!.revId, new Date('2026-05-12T00:00:00Z'));
const note = db.prepare(`SELECT raw_text FROM notes WHERE id=?`).get(id) as { raw_text: string };
expect(note.raw_text).toBe('v1');
const after = repo.listRevisions(id);
expect(after).toHaveLength(4); // v1(capture) + v2 + v3 + v1 restored (user)
expect(after.at(0)!.rawText).toBe('v1');
expect(after.at(0)!.editedBy).toBe('user');
expect(after.at(0)!.editedAt).toBe('2026-05-12T00:00:00.000Z');
});
it('존재하지 않는 revId 는 throw', () => {
const { id } = repo.create({ rawText: 'v1' });
expect(() => repo.restoreRevision(id, 999_999, new Date())).toThrow(/not found/);
});
});
describe('AiWorker source 회귀', () => {
it('updateRawText 후 findById 가 latest raw_text 반환 (옛 revision 미노출)', () => {
const { id } = repo.create({ rawText: 'v1' });
repo.updateRawText(id, 'v2 corrected', new Date('2026-05-10T00:00:00Z'));
const note = repo.findById(id);
expect(note?.rawText).toBe('v2 corrected');
});
});
});