import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { rm } from 'fs/promises'; import type { TranscriptResponse } from 'youtube-transcript'; const { mockExecFile, mockFetchTranscript } = vi.hoisted(() => ({ mockExecFile: vi.fn(), mockFetchTranscript: vi.fn() })); const TEST_DATA_DIR = `/tmp/tonemark-downloader-test-${Date.now()}`; vi.stubEnv('DATA_DIR', TEST_DATA_DIR); vi.mock('child_process', () => ({ execFile: mockExecFile })); vi.mock('youtube-transcript', () => ({ fetchTranscript: mockFetchTranscript })); import { downloadYouTube, transcriptEntriesToSegments, saveUploadedFile, getUploadPath, cleanupUploadDir } from '$lib/server/downloader.js'; beforeEach(() => { vi.clearAllMocks(); mockExecFile.mockImplementation((...args: unknown[]) => { const cb = args.at(-1) as (...callbackArgs: unknown[]) => void; cb(null, JSON.stringify({ title: 'Fetched Title' }), ''); }); }); afterEach(async () => { await rm(TEST_DATA_DIR, { recursive: true, force: true }).catch(() => {}); }); describe('transcriptEntriesToSegments', () => { it('converts millisecond transcript offsets into second-based segments', () => { const entries: TranscriptResponse[] = [ { text: 'Hello everyone.', offset: 15240, duration: 4240, lang: 'en' }, { text: 'Um, welcome to this talk.', offset: 16600, duration: 5080, lang: 'en' } ]; expect(transcriptEntriesToSegments(entries)).toEqual([ { index: 0, start: 15.24, end: 19.48, text: 'Hello everyone.', words: [] }, { index: 1, start: 16.6, end: 21.68, text: 'Um, welcome to this talk.', words: [] } ]); }); it('preserves second-based transcript offsets and drops empty text', () => { const entries: TranscriptResponse[] = [ { text: ' ', offset: 0, duration: 1.5, lang: 'en' }, { text: 'Clean caption cue', offset: 91.08, duration: 3.72, lang: 'en' } ]; expect(transcriptEntriesToSegments(entries)).toEqual([ { index: 0, start: 91.08, end: 94.8, text: 'Clean caption cue', words: [] } ]); }); }); describe('saveUploadedFile', () => { it('saves file to persistent DATA_DIR/uploads/{jobId}/ location', async () => { const buffer = Buffer.from('fake audio content'); const filePath = await saveUploadedFile(buffer, 'test-recording.webm', 'upload-test-1'); // Must be under DATA_DIR/uploads/, NOT under TMP_DIR/downloads/ expect(filePath).toContain('/uploads/upload-test-1/test-recording.webm'); expect(filePath).not.toContain('/downloads/'); // File must exist on disk const { accessSync } = await import('fs'); expect(() => accessSync(filePath)).not.toThrow(); // Content must match const { readFileSync } = await import('fs'); expect(readFileSync(filePath)).toEqual(buffer); // cleanup await cleanupUploadDir('upload-test-1'); }); it('getUploadPath returns correct path without creating file', async () => { const p = getUploadPath('some-job', 'audio.mp3'); expect(p).toContain('/uploads/some-job/audio.mp3'); expect(p).not.toContain('/downloads/'); }); it('cleanupUploadDir removes the job upload directory', async () => { const buffer = Buffer.from('data'); await saveUploadedFile(buffer, 'temp-file.txt', 'cleanup-test'); const p = getUploadPath('cleanup-test', 'temp-file.txt'); const { accessSync } = await import('fs'); expect(() => accessSync(p)).not.toThrow(); await cleanupUploadDir('cleanup-test'); expect(() => accessSync(p)).toThrow(); }); it('cleanupUploadDir does not throw for non-existent dir', async () => { await expect(cleanupUploadDir('non-existent-job')).resolves.toBeUndefined(); }); it('each job gets an isolated upload directory', async () => { const p1 = await saveUploadedFile(Buffer.from('a'), 'a.wav', 'job-iso-1'); const p2 = await saveUploadedFile(Buffer.from('b'), 'b.wav', 'job-iso-2'); expect(p1).toContain('/uploads/job-iso-1/a.wav'); expect(p2).toContain('/uploads/job-iso-2/b.wav'); await cleanupUploadDir('job-iso-1'); await cleanupUploadDir('job-iso-2'); }); }); describe('downloadYouTube', () => { it('uses fetched transcript entries directly for caption jobs', async () => { mockFetchTranscript.mockResolvedValue([ { text: 'Hello everyone.', offset: 15240, duration: 4240, lang: 'en' }, { text: 'Um, welcome to this talk.', offset: 16600, duration: 5080, lang: 'en' } ] satisfies TranscriptResponse[]); const result = await downloadYouTube('https://youtube.com/watch?v=qdh_x-uRs9g', 'job-1'); expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=qdh_x-uRs9g', { lang: 'en' }); expect(result).toMatchObject({ type: 'captions', segments: [ { index: 0, start: 15.24, end: 19.48, text: 'Hello everyone.', words: [] }, { index: 1, start: 16.6, end: 21.68, text: 'Um, welcome to this talk.', words: [] } ] }); }); });