Files
tonemark/src/tests/db.test.ts
Giancarmine Salucci 13a96b6efa
Some checks failed
Build & Push Docker Image / build-and-push (push) Failing after 11s
Initial commit: Tonemark PWA
Tonemark is a SvelteKit PWA for transcribing YouTube videos, audio
and video files, and microphone recordings using a local Whisper backend.

Features:
- Dark glassmorphic UI with electric-lime accent (5 switchable themes)
- Rail nav (desktop) / tab bar (mobile) layout
- Drop zone, YouTube URL input, and live audio recording inputs
- Audio mode waveform cards (none / standard / aggressive / auto)
- Real-time transcription progress with animated waveform
- Job queue with SSE streaming updates
- Push notifications on job completion
- PWA with native SvelteKit service worker
- SRT / TXT / MD / JSON transcript downloads

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-06 16:41:25 +02:00

196 lines
7.0 KiB
TypeScript

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);
});
});