import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { vi } from 'vitest'; import { mkdtemp, rm } from 'fs/promises'; import { join } from 'path'; import { tmpdir } from 'os'; // Set DATA_DIR before db module is imported (module-level DB init) const TEST_DATA_DIR = join(tmpdir(), 'whisper-pwa-db-test-' + Date.now()); vi.stubEnv('DATA_DIR', TEST_DATA_DIR); import { createJob, getJob, listJobs, updateJob, setJobStatus, savePushSubscription, getAllSubscriptions, deletePushSubscription } from '$lib/server/db.js'; afterAll(async () => { await rm(TEST_DATA_DIR, { recursive: true, force: true }); }); // ── createJob / getJob ──────────────────────────────────────────────────────── describe('createJob', () => { it('creates a job and returns it with a UUID', () => { const job = createJob('https://youtu.be/abc', 'My Video', 'auto'); expect(job.id).toMatch(/^[0-9a-f-]{36}$/); expect(job.source).toBe('https://youtu.be/abc'); expect(job.title).toBe('My Video'); expect(job.audioMode).toBe('auto'); }); it('defaults status to pending', () => { const job = createJob('src', 'title', 'standard'); expect(job.status).toBe('pending'); }); it('defaults progress to 0', () => { const job = createJob('src', 'title', 'none'); expect(job.progress).toBe(0); }); it('assigns unique IDs to each job', () => { const a = createJob('src', 'A', 'auto'); const b = createJob('src', 'B', 'auto'); expect(a.id).not.toBe(b.id); }); }); describe('getJob', () => { it('returns null for an unknown id', () => { expect(getJob('00000000-0000-0000-0000-000000000000')).toBeNull(); }); it('returns the correct job by id', () => { const created = createJob('https://youtu.be/xyz', 'Find Me', 'aggressive'); const found = getJob(created.id); expect(found).not.toBeNull(); expect(found!.id).toBe(created.id); expect(found!.title).toBe('Find Me'); }); }); // ── updateJob ───────────────────────────────────────────────────────────────── describe('updateJob', () => { it('updates only specified fields, preserving others', () => { const job = createJob('src', 'Original Title', 'auto'); updateJob({ id: job.id, title: 'New Title' }); const updated = getJob(job.id)!; expect(updated.title).toBe('New Title'); expect(updated.source).toBe('src'); // unchanged expect(updated.audioMode).toBe('auto'); // unchanged }); it('stores whisperJobId', () => { const job = createJob('src', 'title', 'auto'); updateJob({ id: job.id, whisperJobId: 'whisper-uuid-123' }); expect(getJob(job.id)!.whisperJobId).toBe('whisper-uuid-123'); }); it('stores meanVolume', () => { const job = createJob('src', 'title', 'auto'); updateJob({ id: job.id, meanVolume: -20.5 }); expect(getJob(job.id)!.meanVolume).toBe(-20.5); }); it('stores segmentsJson', () => { const job = createJob('src', 'title', 'auto'); const json = JSON.stringify([{ index: 0, start: 0, end: 5, text: 'Hello', words: [] }]); updateJob({ id: job.id, segmentsJson: json }); expect(getJob(job.id)!.segmentsJson).toBe(json); }); it('stores error message', () => { const job = createJob('src', 'title', 'auto'); updateJob({ id: job.id, status: 'failed', error: 'something went wrong' }); const updated = getJob(job.id)!; expect(updated.status).toBe('failed'); expect(updated.error).toBe('something went wrong'); }); it('does nothing for an unknown id', () => { expect(() => updateJob({ id: 'no-such-id', title: 'Ghost' })).not.toThrow(); }); }); // ── setJobStatus ────────────────────────────────────────────────────────────── describe('setJobStatus', () => { it('updates status without touching other fields', () => { const job = createJob('src', 'Status Test', 'auto'); setJobStatus(job.id, 'downloading', 5); const updated = getJob(job.id)!; expect(updated.status).toBe('downloading'); expect(updated.progress).toBe(5); expect(updated.title).toBe('Status Test'); // unchanged }); it('transitions through all valid statuses', () => { const job = createJob('src', 'title', 'auto'); const statuses = ['downloading', 'preparing', 'transcribing', 'processing', 'done'] as const; for (const status of statuses) { setJobStatus(job.id, status, 50); expect(getJob(job.id)!.status).toBe(status); } }); it('defaults progress to 0 if not supplied', () => { const job = createJob('src', 'title', 'auto'); setJobStatus(job.id, 'preparing'); expect(getJob(job.id)!.progress).toBe(0); }); }); // ── listJobs ────────────────────────────────────────────────────────────────── describe('listJobs', () => { it('returns jobs in descending creation order', () => { const a = createJob('src', 'Alpha', 'auto'); const b = createJob('src', 'Beta', 'auto'); const jobs = listJobs(); const ids = jobs.map((j) => j.id); expect(ids.indexOf(b.id)).toBeLessThan(ids.indexOf(a.id)); }); it('returns an array (possibly empty)', () => { expect(Array.isArray(listJobs())).toBe(true); }); }); // ── Push subscriptions ──────────────────────────────────────────────────────── describe('push subscriptions', () => { const sub1 = { endpoint: 'https://push.example.com/abc', p256dh: 'p256dh-value-1', auth: 'auth-1' }; const sub2 = { endpoint: 'https://push.example.com/xyz', p256dh: 'p256dh-value-2', auth: 'auth-2' }; it('saves and retrieves a subscription', () => { savePushSubscription(sub1); const subs = getAllSubscriptions(); const found = subs.find((s) => s.endpoint === sub1.endpoint); expect(found).toBeDefined(); expect(found!.p256dh).toBe(sub1.p256dh); expect(found!.auth).toBe(sub1.auth); }); it('upserts on duplicate endpoint (updates keys)', () => { savePushSubscription(sub1); const updated = { ...sub1, p256dh: 'new-p256dh', auth: 'new-auth' }; savePushSubscription(updated); const subs = getAllSubscriptions().filter((s) => s.endpoint === sub1.endpoint); expect(subs).toHaveLength(1); expect(subs[0].p256dh).toBe('new-p256dh'); }); it('deletes by endpoint', () => { savePushSubscription(sub2); deletePushSubscription(sub2.endpoint); const found = getAllSubscriptions().find((s) => s.endpoint === sub2.endpoint); expect(found).toBeUndefined(); }); it('stores multiple subscriptions independently', () => { savePushSubscription(sub1); savePushSubscription(sub2); const subs = getAllSubscriptions(); const endpoints = subs.map((s) => s.endpoint); expect(endpoints).toContain(sub1.endpoint); expect(endpoints).toContain(sub2.endpoint); }); });