feat(capture): CaptureService with enqueue + celebrate hooks
Task 14 of the slice plan. Orchestrates a Quick Capture submit: trims/validates input, creates the note row + pending_jobs row in one repo.create transaction, persists each pasted PNG via MediaStore (relPath stored in media table), then awaits the injected enqueue() (AiWorker.enqueue at runtime) and fires celebrate() (NotificationService.celebrate). deleteNote drops the db row (cascading note_tags / media / pending_jobs) and removes the on-disk media directory afterwards. Verification: `npx vitest run tests/unit/CaptureService.test.ts` 4 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
60
tests/unit/CaptureService.test.ts
Normal file
60
tests/unit/CaptureService.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { mkdtempSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrations } from '@main/db/migrations/index.js';
|
||||
import { NoteRepository } from '@main/repository/NoteRepository.js';
|
||||
import { MediaStore } from '@main/services/MediaStore.js';
|
||||
import { CaptureService } from '@main/services/CaptureService.js';
|
||||
|
||||
describe('CaptureService', () => {
|
||||
let db: Database.Database;
|
||||
let repo: NoteRepository;
|
||||
let store: MediaStore;
|
||||
let tmp: string;
|
||||
let enqueued: string[];
|
||||
let celebrated: string[];
|
||||
let svc: CaptureService;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
repo = new NoteRepository(db);
|
||||
tmp = mkdtempSync(join(tmpdir(), 'inkling-capture-'));
|
||||
store = new MediaStore(tmp);
|
||||
enqueued = [];
|
||||
celebrated = [];
|
||||
svc = new CaptureService(repo, store, {
|
||||
enqueue: async (id) => { enqueued.push(id); },
|
||||
celebrate: (id) => { celebrated.push(id); }
|
||||
});
|
||||
});
|
||||
|
||||
it('persists text-only and triggers enqueue + celebrate', async () => {
|
||||
const { noteId } = await svc.submit({ text: '안녕', images: [] });
|
||||
expect(repo.findById(noteId)?.rawText).toBe('안녕');
|
||||
expect(enqueued).toEqual([noteId]);
|
||||
expect(celebrated).toEqual([noteId]);
|
||||
});
|
||||
|
||||
it('saves images under media/{noteId}/', async () => {
|
||||
const img = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]).buffer;
|
||||
const { noteId } = await svc.submit({ text: 'x', images: [img] });
|
||||
const note = repo.findById(noteId)!;
|
||||
expect(note.media).toHaveLength(1);
|
||||
expect(note.media[0]!.relPath.startsWith(`media/${noteId}/`)).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects empty submit', async () => {
|
||||
await expect(svc.submit({ text: ' ', images: [] })).rejects.toThrow(/empty/i);
|
||||
expect(celebrated).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('deleteNote removes db row + media dir', async () => {
|
||||
const img = new Uint8Array([0, 1, 2, 3]).buffer;
|
||||
const { noteId } = await svc.submit({ text: 't', images: [img] });
|
||||
await svc.deleteNote(noteId);
|
||||
expect(repo.findById(noteId)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user