Files
inkling/tests/unit/migrations.test.ts
th-kim0823 eca91a1e7c feat(notebook): m009 sort_order 컬럼 + reorder 메서드 + IPC notebook:reorder
- m009 마이그레이션: notebooks.sort_order INTEGER 컬럼 추가, 기존 rows created_at 순으로 backfill
- NotebookRepository.list ORDER BY sort_order ASC, name ASC 로 변경
- NotebookRepository.create 신규 노트북 sort_order = max+1 자동 할당
- NotebookRepository.reorder(id, direction) — swap transaction 으로 atomic 순서 변경
- IPC notebook:reorder 핸들러 등록, preload/shared types pass-through
- 테스트 45개 추가 (m009, reorder 케이스 4, list ORDER BY, IPC 핸들러 2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:06:44 +09:00

120 lines
4.6 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import Database from 'better-sqlite3';
import { runMigrations } from '@main/db/migrations/index.js';
describe('migrations', () => {
it('creates schema with intent + edited columns', () => {
const db = new Database(':memory:');
runMigrations(db);
const cols = db.prepare(`PRAGMA table_info(notes)`).all().map((r: any) => r.name);
expect(cols).toEqual(
expect.arrayContaining([
'id', 'raw_text', 'ai_title', 'ai_summary', 'ai_status', 'ai_error',
'ai_provider', 'ai_generated_at',
'title_edited_by_user', 'summary_edited_by_user',
'user_intent', 'intent_prompted_at',
'created_at', 'updated_at'
])
);
db.close();
});
it('is idempotent', () => {
const db = new Database(':memory:');
runMigrations(db);
const before = (db.prepare('PRAGMA user_version').get() as any).user_version;
runMigrations(db);
const after = (db.prepare('PRAGMA user_version').get() as any).user_version;
expect(after).toBe(before);
db.close();
});
});
describe('migration v3 — soft delete columns', () => {
it('adds deleted_at, last_recalled_at, recall_dismissed_at to notes', () => {
const db = new Database(':memory:');
runMigrations(db);
const cols = db.prepare(`PRAGMA table_info(notes)`).all().map((r: any) => r.name);
expect(cols).toEqual(
expect.arrayContaining(['deleted_at', 'last_recalled_at', 'recall_dismissed_at'])
);
db.close();
});
it('creates idx_notes_deleted_at index', () => {
const db = new Database(':memory:');
runMigrations(db);
const indexes = db
.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='notes'`)
.all() as Array<{ name: string }>;
expect(indexes.map((i) => i.name)).toContain('idx_notes_deleted_at');
db.close();
});
it('user_version reaches latest (9)', () => {
const db = new Database(':memory:');
runMigrations(db);
const row = db.prepare('PRAGMA user_version').get() as { user_version: number };
expect(row.user_version).toBe(9);
db.close();
});
it('all 3 new columns default to NULL', () => {
const db = new Database(':memory:');
runMigrations(db);
db.prepare(
`INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at)
VALUES ('n1', 't', 'pending', '2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z')`
).run();
const row = db.prepare('SELECT deleted_at, last_recalled_at, recall_dismissed_at FROM notes WHERE id=?').get('n1') as any;
expect(row.deleted_at).toBeNull();
expect(row.last_recalled_at).toBeNull();
expect(row.recall_dismissed_at).toBeNull();
db.close();
});
});
describe('migration v5 — ai_status disabled enum', () => {
it("CHECK constraint accepts 'disabled'", () => {
const db = new Database(':memory:');
runMigrations(db);
expect(() => {
db.prepare(
`INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at)
VALUES ('d1', 't', 'disabled', '2026-05-01T00:00:00Z', '2026-05-01T00:00:00Z')`
).run();
}).not.toThrow();
db.close();
});
it('preserves existing notes (status, due_date, deleted_at, recall fields)', () => {
// m004 까지만 적용된 상태에서 데이터 insert 후 m005 까지 마이그레이션 → 데이터 보존 확인.
// runMigrations 가 user_version 으로 idempotent 라 한 번에 5 까지 가지만,
// 본 테스트는 single runMigrations 후 m004 시점에 가까운 row 를 넣고 cols 확인.
const db = new Database(':memory:');
runMigrations(db);
db.prepare(
`INSERT INTO notes (id, raw_text, ai_status, created_at, updated_at, status, due_date, deleted_at)
VALUES ('p1', 'old', 'done', '2026-04-01T00:00:00Z', '2026-04-01T00:00:00Z', 'archived', '2026-05-10', NULL)`
).run();
const row = db.prepare('SELECT status, due_date, ai_status FROM notes WHERE id=?').get('p1') as any;
expect(row.status).toBe('archived');
expect(row.due_date).toBe('2026-05-10');
expect(row.ai_status).toBe('done');
db.close();
});
it('preserves idx_notes_ai_status + idx_notes_created_at + idx_notes_deleted_at', () => {
const db = new Database(':memory:');
runMigrations(db);
const indexes = db
.prepare(`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='notes'`)
.all() as Array<{ name: string }>;
const names = indexes.map((i) => i.name);
expect(names).toContain('idx_notes_ai_status');
expect(names).toContain('idx_notes_created_at');
expect(names).toContain('idx_notes_deleted_at');
db.close();
});
});