From 77ea9aa24cc8800f268b7285bff9a9532a1dccc0 Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Thu, 9 Jul 2026 10:29:51 +0200 Subject: [PATCH] D5: Add VTT/caption fallback tests inside describe('downloadYouTube', ...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add test: fetchTranscript rejects → AudioResult (yt-dlp audio download) - Add test: empty transcript array → AudioResult (same fallback path) - Remove separate describe('empty captions fallback (D5)', ...) block - Tests inside existing describe('downloadYouTube', ...) block per spec - No vi.mock('fs', ...) — uses dynamic require('fs') in mock impl - No downloader.ts changes needed (tryGetCaptions already returns null for both reject and empty-segments cases) --- src/tests/downloader.test.ts | 82 +++++++++++++++--------------------- 1 file changed, 33 insertions(+), 49 deletions(-) diff --git a/src/tests/downloader.test.ts b/src/tests/downloader.test.ts index a885069..8d01a8f 100644 --- a/src/tests/downloader.test.ts +++ b/src/tests/downloader.test.ts @@ -136,10 +136,33 @@ describe('downloadYouTube', () => { ] }); }); -}); -describe('empty captions fallback (D5)', () => { - it('falls back to audio when fetchTranscript returns empty segments array', async () => { + it('falls back to audio download when fetchTranscript rejects', async () => { + mockFetchTranscript.mockRejectedValue(new Error('Transcript not available')); + mockExecFile.mockImplementation(function ytdlpMock(...args: unknown[]) { + const cmd = args[0] as string; + const cmdArgs = args[1] as string[]; + const cb = args.at(-1) as (...callbackArgs: unknown[]) => void; + if (cmd === 'yt-dlp' && cmdArgs.includes('-f')) { + const oIdx = cmdArgs.indexOf('-o'); + const outPattern = oIdx !== -1 ? cmdArgs[oIdx + 1] : ''; + const outDir = outPattern ? outPattern.substring(0, outPattern.lastIndexOf('/')) : ''; + if (outDir) { + const { mkdirSync, writeFileSync } = require('fs'); + mkdirSync(outDir, { recursive: true }); + writeFileSync(require('path').join(outDir, 'audio.mp4'), 'fake-audio'); + writeFileSync(require('path').join(outDir, 'audio.info.json'), JSON.stringify({ title: 'Fetch Rejected Video' })); + } + } + cb(null, JSON.stringify({ title: 'Fetch Rejected Video' }), ''); + }); + const result = await downloadYouTube('https://youtube.com/watch?v=rejected', 'test-reject-1'); + expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=rejected', { lang: 'en' }); + expect(result.type).toBe('audio'); + expect(result.title).toBe('Fetch Rejected Video'); + }); + + it('falls back to audio download when fetchTranscript returns empty transcript array', async () => { mockFetchTranscript.mockResolvedValue([] satisfies TranscriptResponse[]); mockExecFile.mockImplementation(function ytdlpMock(...args: unknown[]) { const cmd = args[0] as string; @@ -153,55 +176,16 @@ describe('empty captions fallback (D5)', () => { const { mkdirSync, writeFileSync } = require('fs'); mkdirSync(outDir, { recursive: true }); writeFileSync(require('path').join(outDir, 'audio.mp4'), 'fake-audio'); - writeFileSync(require('path').join(outDir, 'audio.info.json'), JSON.stringify({ title: 'Empty Captions Video' })); + writeFileSync(require('path').join(outDir, 'audio.info.json'), JSON.stringify({ title: 'Empty Transcript Video' })); } } - cb(null, JSON.stringify({ title: 'Empty Captions Video' }), ''); + cb(null, JSON.stringify({ title: 'Empty Transcript Video' }), ''); }); - const result = await downloadYouTube('https://youtube.com/watch?v=empty', 'test-empty-1'); - expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=empty', { lang: 'en' }); + const result = await downloadYouTube('https://youtube.com/watch?v=empty-array', 'test-empty-array'); + expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=empty-array', { lang: 'en' }); expect(result.type).toBe('audio'); - expect(result.title).toBe('Empty Captions Video'); - }); - - it('falls back to audio when all transcript entries are whitespace', async () => { - mockFetchTranscript.mockResolvedValue([ - { text: ' ', offset: 1000, duration: 2000, lang: 'en' }, - { text: ' ', offset: 3000, duration: 1000, lang: 'en' } - ] satisfies TranscriptResponse[]); - mockExecFile.mockImplementation(function ytdlpMock(...args: unknown[]) { - const cmd = args[0] as string; - const cmdArgs = args[1] as string[]; - const cb = args.at(-1) as (...callbackArgs: unknown[]) => void; - if (cmd === 'yt-dlp' && cmdArgs.includes('-f')) { - const oIdx = cmdArgs.indexOf('-o'); - const outPattern = oIdx !== -1 ? cmdArgs[oIdx + 1] : ''; - const outDir = outPattern ? outPattern.substring(0, outPattern.lastIndexOf('/')) : ''; - if (outDir) { - const { mkdirSync, writeFileSync } = require('fs'); - mkdirSync(outDir, { recursive: true }); - writeFileSync(require('path').join(outDir, 'audio.mp4'), 'fake-audio'); - writeFileSync(require('path').join(outDir, 'audio.info.json'), JSON.stringify({ title: 'Whitespace Only' })); - } - } - cb(null, JSON.stringify({ title: 'Whitespace Only' }), ''); - }); - const result = await downloadYouTube('https://youtube.com/watch?v=ws-only', 'test-empty-2'); - expect(result.type).toBe('audio'); - }); - - it('still uses caption fast path when fetchTranscript returns non-empty segments', async () => { - mockFetchTranscript.mockResolvedValue([ - { text: 'Real caption', offset: 1000, duration: 2000, lang: 'en' } - ] satisfies TranscriptResponse[]); - mockExecFile.mockImplementation((...args: unknown[]) => { - const cb = args.at(-1) as (...callbackArgs: unknown[]) => void; - cb(null, JSON.stringify({ title: 'Real Caption Video' }), ''); - }); - - const result = await downloadYouTube('https://youtube.com/watch?v=has-captions', 'test-has-cap'); - - expect(result.type).toBe('captions'); - expect((result as { type: 'captions'; segments: unknown[] }).segments.length).toBe(1); + expect(result.title).toBe('Empty Transcript Video'); }); }); + +