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>
This commit is contained in:
@@ -112,4 +112,49 @@ describe('NotebookRepository', () => {
|
||||
it('findByName: 없으면 null', () => {
|
||||
expect(repo.findByName('없음')).toBeNull();
|
||||
});
|
||||
|
||||
it('list: sort_order ASC 순서로 반환', () => {
|
||||
const a = repo.create({ name: 'A' }); // sort_order = 1 (기본=0)
|
||||
const b = repo.create({ name: 'B' }); // sort_order = 2
|
||||
const all = repo.list();
|
||||
expect(all[0]!.name).toBe('기본');
|
||||
expect(all[1]!.id).toBe(a.id);
|
||||
expect(all[2]!.id).toBe(b.id);
|
||||
});
|
||||
|
||||
it('reorder: B.up → B/기본/A 순서로 swap', () => {
|
||||
const a = repo.create({ name: 'A' }); // sort_order=1
|
||||
const b = repo.create({ name: 'B' }); // sort_order=2
|
||||
// 초기: 기본(0), A(1), B(2)
|
||||
const r = repo.reorder(b.id, 'up');
|
||||
expect(r.ok).toBe(true);
|
||||
const names = repo.list().map((n) => n.name);
|
||||
expect(names).toEqual(['기본', 'B', 'A']);
|
||||
});
|
||||
|
||||
it('reorder: 첫 번째 notebook up → ok:false', () => {
|
||||
const defaultId = repo.list()[0]!.id;
|
||||
const r = repo.reorder(defaultId, 'up');
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('reorder: 마지막 notebook down → ok:false', () => {
|
||||
const c = repo.create({ name: 'C' }); // sort_order=1
|
||||
const r = repo.reorder(c.id, 'down');
|
||||
expect(r.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('reorder: B.down → 기본/A/C/B 순서', () => {
|
||||
const a = repo.create({ name: 'A' }); // sort_order=1
|
||||
const b = repo.create({ name: 'B' }); // sort_order=2
|
||||
const c = repo.create({ name: 'C' }); // sort_order=3
|
||||
// 초기: 기본(0), A(1), B(2), C(3)
|
||||
const r = repo.reorder(b.id, 'down');
|
||||
expect(r.ok).toBe(true);
|
||||
const names = repo.list().map((n) => n.name);
|
||||
expect(names).toEqual(['기본', 'A', 'C', 'B']);
|
||||
// a, c 순서 안 변함 확인
|
||||
expect(names.indexOf('A')).toBeLessThan(names.indexOf('C'));
|
||||
void a; void c;
|
||||
});
|
||||
});
|
||||
|
||||
28
tests/unit/m009.test.ts
Normal file
28
tests/unit/m009.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrations } from '../../src/main/db/migrations/index.js';
|
||||
|
||||
describe('m009 notebook sort_order migration', () => {
|
||||
let db: Database.Database;
|
||||
beforeEach(() => { db = new Database(':memory:'); db.pragma('foreign_keys = ON'); });
|
||||
afterEach(() => { db.close(); });
|
||||
|
||||
it('fresh DB: default notebook 의 sort_order = 0', () => {
|
||||
runMigrations(db);
|
||||
const r = db.prepare(`SELECT sort_order FROM notebooks`).get() as { sort_order: number };
|
||||
expect(r.sort_order).toBe(0);
|
||||
});
|
||||
|
||||
it('새 notebook insert 시 DEFAULT 0 (caller 가 max+1 책임)', () => {
|
||||
runMigrations(db);
|
||||
db.prepare(`INSERT INTO notebooks(id,name,created_at,updated_at) VALUES('nb-x','회사','t','t')`).run();
|
||||
const r = db.prepare(`SELECT sort_order FROM notebooks WHERE id='nb-x'`).get() as { sort_order: number };
|
||||
expect(r.sort_order).toBe(0);
|
||||
});
|
||||
|
||||
it('user_version reaches 9 after all migrations', () => {
|
||||
runMigrations(db);
|
||||
const r = db.prepare('PRAGMA user_version').get() as { user_version: number };
|
||||
expect(r.user_version).toBe(9);
|
||||
});
|
||||
});
|
||||
@@ -51,11 +51,11 @@ describe('migration v3 — soft delete columns', () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it('user_version reaches latest (8)', () => {
|
||||
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(8);
|
||||
expect(row.user_version).toBe(9);
|
||||
db.close();
|
||||
});
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ function makeRepo() {
|
||||
setColor: vi.fn(),
|
||||
delete: vi.fn(() => ({ ok: true })),
|
||||
moveNote: vi.fn(),
|
||||
findById: vi.fn(() => null)
|
||||
findById: vi.fn(() => null),
|
||||
reorder: vi.fn(() => ({ ok: true }))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -96,4 +97,23 @@ describe('notebookApi IPC', () => {
|
||||
expect(repo.setColor).toHaveBeenCalledWith('id1', '#fff');
|
||||
expect(r).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('notebook:reorder — repo.reorder 호출 + ok:true 전달', async () => {
|
||||
const repo = makeRepo();
|
||||
repo.reorder.mockReturnValue({ ok: true } as never);
|
||||
registerNotebookApi({ repo: repo as never });
|
||||
const h = getHandler('notebook:reorder');
|
||||
const r = await h({}, 'nb-1', 'up');
|
||||
expect(repo.reorder).toHaveBeenCalledWith('nb-1', 'up');
|
||||
expect(r).toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('notebook:reorder — 첫 번째 항목 up 시 ok:false 전달', async () => {
|
||||
const repo = makeRepo();
|
||||
repo.reorder.mockReturnValue({ ok: false } as never);
|
||||
registerNotebookApi({ repo: repo as never });
|
||||
const h = getHandler('notebook:reorder');
|
||||
const r = await h({}, 'nb-first', 'up');
|
||||
expect(r).toEqual({ ok: false });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user