D5: Add VTT/caption fallback tests inside describe('downloadYouTube', ...)

- 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)
This commit is contained in:
Giancarmine Salucci
2026-07-09 10:29:51 +02:00
parent d9891e2107
commit 77ea9aa24c
+33 -49
View File
@@ -136,10 +136,33 @@ describe('downloadYouTube', () => {
] ]
}); });
}); });
});
describe('empty captions fallback (D5)', () => { it('falls back to audio download when fetchTranscript rejects', async () => {
it('falls back to audio when fetchTranscript returns empty segments array', 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[]); mockFetchTranscript.mockResolvedValue([] satisfies TranscriptResponse[]);
mockExecFile.mockImplementation(function ytdlpMock(...args: unknown[]) { mockExecFile.mockImplementation(function ytdlpMock(...args: unknown[]) {
const cmd = args[0] as string; const cmd = args[0] as string;
@@ -153,55 +176,16 @@ describe('empty captions fallback (D5)', () => {
const { mkdirSync, writeFileSync } = require('fs'); const { mkdirSync, writeFileSync } = require('fs');
mkdirSync(outDir, { recursive: true }); mkdirSync(outDir, { recursive: true });
writeFileSync(require('path').join(outDir, 'audio.mp4'), 'fake-audio'); 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'); const result = await downloadYouTube('https://youtube.com/watch?v=empty-array', 'test-empty-array');
expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=empty', { lang: 'en' }); expect(mockFetchTranscript).toHaveBeenCalledWith('https://youtube.com/watch?v=empty-array', { lang: 'en' });
expect(result.type).toBe('audio'); expect(result.type).toBe('audio');
expect(result.title).toBe('Empty Captions Video'); expect(result.title).toBe('Empty Transcript 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);
}); });
}); });