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(); }); }); describe('CaptureService telemetry emit', () => { let db: Database.Database; let repo: NoteRepository; let store: MediaStore; let tmp: string; let events: Array<{ kind: string; payload: { noteId: string; rawTextLength: number; hasMedia: boolean } }>; beforeEach(() => { db = new Database(':memory:'); runMigrations(db); repo = new NoteRepository(db); tmp = mkdtempSync(join(tmpdir(), 'inkling-capture-')); store = new MediaStore(tmp); events = []; }); it('emits capture event with noteId/rawTextLength/hasMedia', async () => { const svc = new CaptureService(repo, store, { enqueue: async () => {}, celebrate: () => {}, telemetry: { emit: async (ev) => { events.push(ev as typeof events[number]); } } }); await svc.submit({ text: '안녕하세요', images: [] }); expect(events).toHaveLength(1); expect(events[0]!.kind).toBe('capture'); expect(events[0]!.payload.rawTextLength).toBe('안녕하세요'.length); expect(events[0]!.payload.hasMedia).toBe(false); expect(typeof events[0]!.payload.noteId).toBe('string'); }); it('emits hasMedia=true when images present', async () => { const svc = new CaptureService(repo, store, { enqueue: async () => {}, celebrate: () => {}, telemetry: { emit: async (ev) => { events.push(ev as typeof events[number]); } } }); const img = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]).buffer; await svc.submit({ text: '이미지 메모', images: [img] }); expect(events).toHaveLength(1); expect(events[0]!.payload.hasMedia).toBe(true); }); it('does NOT emit when telemetry dep absent (backward compat)', async () => { const svc = new CaptureService(repo, store, { enqueue: async () => {}, celebrate: () => {} }); const result = await svc.submit({ text: 'no telem', images: [] }); expect(typeof result.noteId).toBe('string'); expect(events).toHaveLength(0); // events array stays empty since no telemetry was wired }); });