- 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>
161 lines
5.6 KiB
TypeScript
161 lines
5.6 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 { NotebookRepository } from '../../src/main/repository/NotebookRepository.js';
|
|
|
|
describe('NotebookRepository', () => {
|
|
let db: Database.Database;
|
|
let repo: NotebookRepository;
|
|
beforeEach(() => {
|
|
db = new Database(':memory:');
|
|
db.pragma('foreign_keys = ON');
|
|
runMigrations(db);
|
|
repo = new NotebookRepository(db);
|
|
});
|
|
afterEach(() => { db.close(); });
|
|
|
|
it('list: 기본 notebook 1개 + noteCount 0', () => {
|
|
const all = repo.list();
|
|
expect(all).toHaveLength(1);
|
|
expect(all[0]!.name).toBe('기본');
|
|
expect(all[0]!.noteCount).toBe(0);
|
|
});
|
|
|
|
it('create: 새 notebook 추가', () => {
|
|
const nb = repo.create({ name: '회사', color: '#0a4b80' });
|
|
expect(nb.name).toBe('회사');
|
|
expect(nb.color).toBe('#0a4b80');
|
|
expect(repo.list()).toHaveLength(2);
|
|
});
|
|
|
|
it('create: 같은 이름 두 번이면 throw', () => {
|
|
repo.create({ name: '회사' });
|
|
expect(() => repo.create({ name: '회사' })).toThrow();
|
|
});
|
|
|
|
it('rename: 이름 변경', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
repo.rename(nb.id, '워크');
|
|
const after = repo.findById(nb.id);
|
|
expect(after?.name).toBe('워크');
|
|
});
|
|
|
|
it('delete: 메모 없으면 OK', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
const r = repo.delete(nb.id);
|
|
expect(r.ok).toBe(true);
|
|
expect(repo.findById(nb.id)).toBeNull();
|
|
});
|
|
|
|
it('delete: 메모 있으면 RESTRICT — ok:false', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
const ts = '2026-05-14T00:00:00Z';
|
|
db.prepare(
|
|
`INSERT INTO notes(id, raw_text, ai_status, created_at, updated_at, status, notebook_id)
|
|
VALUES('n1','t','pending',?,?,'active',?)`
|
|
).run(ts, ts, nb.id);
|
|
const r = repo.delete(nb.id);
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) expect(r.reason).toBe('has_notes');
|
|
});
|
|
|
|
it('noteCount: status="active" 만 카운트 (completed/trashed 제외)', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
const ts = '2026-05-14T00:00:00Z';
|
|
const insert = db.prepare(
|
|
`INSERT INTO notes(id, raw_text, ai_status, created_at, updated_at, status, notebook_id)
|
|
VALUES(?,?,?,?,?,?,?)`
|
|
);
|
|
insert.run('n1','t','done',ts,ts,'active',nb.id);
|
|
insert.run('n2','t','done',ts,ts,'completed',nb.id);
|
|
insert.run('n3','t','done',ts,ts,'trashed',nb.id);
|
|
const found = repo.findById(nb.id);
|
|
expect(found?.noteCount).toBe(1);
|
|
});
|
|
|
|
it('moveNote: notebook_id 갱신', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
const ts = '2026-05-14T00:00:00Z';
|
|
const defaultId = repo.list().find((n) => n.name === '기본')!.id;
|
|
db.prepare(
|
|
`INSERT INTO notes(id, raw_text, ai_status, created_at, updated_at, status, notebook_id)
|
|
VALUES('n1','t','pending',?,?,'active',?)`
|
|
).run(ts, ts, defaultId);
|
|
repo.moveNote('n1', nb.id);
|
|
const r = db.prepare(`SELECT notebook_id FROM notes WHERE id='n1'`).get() as { notebook_id: string };
|
|
expect(r.notebook_id).toBe(nb.id);
|
|
});
|
|
|
|
it('setColor: 색 변경', () => {
|
|
const nb = repo.create({ name: '회사', color: '#000' });
|
|
repo.setColor(nb.id, '#fff');
|
|
expect(repo.findById(nb.id)?.color).toBe('#fff');
|
|
});
|
|
|
|
it('delete: 존재하지 않는 id → ok:false reason="not_found"', () => {
|
|
const r = repo.delete('does-not-exist');
|
|
expect(r.ok).toBe(false);
|
|
if (!r.ok) expect(r.reason).toBe('not_found');
|
|
});
|
|
|
|
it('findByName: 이름으로 조회', () => {
|
|
const nb = repo.create({ name: '회사' });
|
|
const found = repo.findByName('회사');
|
|
expect(found?.id).toBe(nb.id);
|
|
});
|
|
|
|
it('findByName: case-insensitive', () => {
|
|
repo.create({ name: 'Work' });
|
|
expect(repo.findByName('work')?.name).toBe('Work');
|
|
});
|
|
|
|
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;
|
|
});
|
|
});
|