import { describe, it, expect, beforeEach, vi } from 'vitest'; import Database from 'better-sqlite3'; import { runMigrations } from '@main/db/migrations/index.js'; import { NoteRepository } from '@main/repository/NoteRepository.js'; import { AiWorker } from '@main/ai/AiWorker.js'; import type { InferenceProvider } from '@main/ai/InferenceProvider.js'; import type { AiResponse } from '@main/ai/schema.js'; function makeProvider(overrides: Partial = {}): InferenceProvider { return { name: 'mock', generate: vi.fn(async (): Promise => ({ title: '제목', summary: 'a\nb\nc', tags: ['tag'] })), healthCheck: vi.fn(async () => ({ ok: true })), ...overrides } as InferenceProvider; } describe('AiWorker', () => { let db: Database.Database; let repo: NoteRepository; beforeEach(() => { db = new Database(':memory:'); runMigrations(db); repo = new NoteRepository(db); }); it('processes a pending job and marks done', async () => { const { id } = repo.create({ rawText: 'x' }); const updates: string[] = []; const w = new AiWorker(repo, makeProvider(), { backoffsMs: [0, 0, 0], onUpdate: (note) => updates.push(note.aiStatus) }); await w.enqueue(id); await w.drain(); expect(repo.findById(id)?.aiStatus).toBe('done'); expect(updates).toContain('done'); }); it('retries 3 times then marks failed', async () => { const { id } = repo.create({ rawText: 'x' }); const provider = makeProvider({ generate: vi.fn(async () => { throw new Error('boom'); }) }); const w = new AiWorker(repo, provider, { backoffsMs: [0, 0, 0] }); await w.enqueue(id); await w.drain(); const note = repo.findById(id)!; expect(note.aiStatus).toBe('failed'); expect(note.aiError).toContain('boom'); expect(provider.generate).toHaveBeenCalledTimes(3); }); it('loadFromDb re-queues all pending', async () => { const a = repo.create({ rawText: 'a' }).id; const b = repo.create({ rawText: 'b' }).id; const w = new AiWorker(repo, makeProvider(), { backoffsMs: [0, 0, 0] }); await w.loadFromDb(); await w.drain(); expect(repo.findById(a)?.aiStatus).toBe('done'); expect(repo.findById(b)?.aiStatus).toBe('done'); }); it('processes sequentially (concurrency 1)', async () => { const ids = [repo.create({ rawText: 'a' }).id, repo.create({ rawText: 'b' }).id]; let running = 0; let max = 0; const provider = makeProvider({ generate: vi.fn(async () => { running++; max = Math.max(max, running); await new Promise((r) => setTimeout(r, 10)); running--; return { title: '제목', summary: 'a\nb\nc', tags: [] }; }) }); const w = new AiWorker(repo, provider, { backoffsMs: [0, 0, 0] }); for (const id of ids) await w.enqueue(id); await w.drain(); expect(max).toBe(1); }); });