import { describe, expect, it } from 'vitest'; import { AUDIO_PRESETS, BUILT_IN_PRESETS, MemoryUserPresetPersistence, UserPresetRegistry, VIDEO_PRESETS, migrateUserPreset, parseUserPresetJson, presetAvailability, presetEncodingArguments, serializeUserPreset, validateUserPreset, type UserExportPreset, } from '../../src/presets'; const USER_PRESET: UserExportPreset = { schemaVersion: 1, id: 'my-podcast', name: 'My podcast', description: 'Speech export', kind: 'audio', container: 'mp3', fileExtension: 'mp3', audio: { codec: 'libmp3lame', bitrateKbps: 128, sampleRate: 44_100, channels: 2, }, metadataPolicy: 'copy', chapterPolicy: 'remove', subtitlePolicy: 'none', }; describe('built-in preset registry', () => { it('contains immutable, uniquely identified video and audio definitions', () => { expect(VIDEO_PRESETS.length).toBeGreaterThanOrEqual(6); expect(AUDIO_PRESETS.length).toBeGreaterThanOrEqual(6); expect(new Set(BUILT_IN_PRESETS.map((preset) => preset.id)).size).toBe( BUILT_IN_PRESETS.length ); expect(Object.isFrozen(BUILT_IN_PRESETS[0])).toBe(true); expect(Object.isFrozen(BUILT_IN_PRESETS[0]?.settingsSummary)).toBe(true); }); it('disables a preset with exact missing capability reasons', () => { const preset = BUILT_IN_PRESETS.find( (candidate) => candidate.id === 'webm-vp9-opus' )!; const unavailable = presetAvailability(preset, { muxers: new Set(['webm']), encoders: new Set(['libvpx-vp9']), decoders: new Set(), filters: new Set(), }); expect(unavailable).toEqual({ status: 'unavailable', reasons: ['Missing encoders: libopus'], }); expect( presetAvailability(preset, { muxers: new Set(['webm']), encoders: new Set(['libvpx-vp9', 'libopus']), decoders: new Set(), filters: new Set(), }) ).toEqual({ status: 'available', reasons: [] }); expect( presetAvailability( preset, { muxers: new Set(['webm']), encoders: new Set(['libvpx-vp9', 'libopus']), decoders: new Set(), filters: new Set(), }, { filters: ['drawtext'] } ) ).toEqual({ status: 'degraded', reasons: ['Optional filters unavailable: drawtext'], }); }); it('turns allowlisted settings into structured encoder arguments', () => { expect(presetEncodingArguments(USER_PRESET)).toEqual([ '-vn', '-c:a', 'libmp3lame', '-b:a', '128k', '-ar', '44100', '-ac', '2', ]); }); }); describe('user preset validation and persistence', () => { it('round-trips a versioned preset as JSON', () => { const json = serializeUserPreset(USER_PRESET); expect(parseUserPresetJson(json)).toEqual(USER_PRESET); expect(validateUserPreset(USER_PRESET)).toMatchObject({ valid: true }); }); it('migrates an allowlisted legacy schema without accepting raw fields', () => { expect( migrateUserPreset({ schemaVersion: 0, id: 'legacy', name: 'Legacy', kind: 'audio', format: 'ipod', audio: { codec: 'aac', bitrateKbps: 128 }, includeChapters: true, }) ).toEqual({ schemaVersion: 1, id: 'legacy', name: 'Legacy', kind: 'audio', container: 'ipod', fileExtension: 'm4a', audio: { codec: 'aac', bitrateKbps: 128 }, metadataPolicy: 'copy', chapterPolicy: 'keep', subtitlePolicy: 'none', }); expect(() => migrateUserPreset({ schemaVersion: 0, id: 'legacy', name: 'Legacy', kind: 'audio', format: 'mp3', args: ['-i', 'remote'], }) ).toThrow(/unsupported field/iu); }); it.each([ ['args', ['-i', 'https://example.invalid/a']], ['filter', 'movie=https://example.invalid'], ['inputPath', '/etc/passwd'], ['protocol', 'https'], ])('rejects imported arbitrary %s fields', (field, value) => { expect( validateUserPreset({ ...USER_PRESET, [field]: value }) ).toMatchObject({ valid: false, errors: [expect.stringContaining(`"${field}"`)], }); }); it('rejects unknown codecs and unsafe identifiers', () => { expect( validateUserPreset({ ...USER_PRESET, id: '../../unsafe', audio: { codec: 'shell', bitrateKbps: 128 }, }) ).toMatchObject({ valid: false }); }); it.each([ [ 'MP4 H.264/AAC video', { ...USER_PRESET, kind: 'video', container: 'mp4', fileExtension: 'mp4', video: { codec: 'libx264', qualityMode: 'balanced', crf: 23 }, audio: { codec: 'aac', bitrateKbps: 160 }, chapterPolicy: 'keep', subtitlePolicy: 'convert', fastStart: true, }, ], [ 'WebM VP9/Opus video', { ...USER_PRESET, kind: 'video', container: 'webm', fileExtension: 'webm', video: { codec: 'libvpx-vp9', qualityMode: 'smaller-file', crf: 32 }, audio: { codec: 'libopus', bitrateKbps: 128 }, chapterPolicy: 'keep', subtitlePolicy: 'convert', }, ], [ 'Matroska VP8/FLAC video', { ...USER_PRESET, kind: 'video', container: 'matroska', fileExtension: 'mkv', video: { codec: 'libvpx', qualityMode: 'compatibility', crf: 18 }, audio: { codec: 'flac' }, chapterPolicy: 'keep', subtitlePolicy: 'copy-compatible', }, ], [ 'animated WebP derivative', { ...USER_PRESET, kind: 'derivative', container: 'webp', fileExtension: 'webp', video: { codec: 'libwebp_anim', qualityMode: 'balanced', crf: 60, }, audio: undefined, metadataPolicy: 'remove', chapterPolicy: 'remove', subtitlePolicy: 'none', }, ], [ 'GIF derivative', { ...USER_PRESET, kind: 'derivative', container: 'gif', fileExtension: 'gif', video: { codec: 'gif', qualityMode: 'compatibility' }, audio: undefined, metadataPolicy: 'remove', chapterPolicy: 'remove', subtitlePolicy: 'none', }, ], ['MP3 audio', USER_PRESET], [ 'M4A/AAC audio', { ...USER_PRESET, container: 'ipod', fileExtension: 'm4a', audio: { codec: 'aac', bitrateKbps: 160 }, fastStart: true, }, ], [ 'Ogg/Vorbis audio', { ...USER_PRESET, container: 'ogg', fileExtension: 'ogg', audio: { codec: 'libvorbis', bitrateKbps: 160 }, }, ], [ 'Opus audio', { ...USER_PRESET, container: 'opus', fileExtension: 'opus', audio: { codec: 'libopus', bitrateKbps: 128 }, }, ], [ 'WAV/PCM audio', { ...USER_PRESET, container: 'wav', fileExtension: 'wav', audio: { codec: 'pcm_s16le' }, }, ], [ 'FLAC audio', { ...USER_PRESET, container: 'flac', fileExtension: 'flac', audio: { codec: 'flac' }, }, ], ])('accepts the reviewed %s combination', (_label, preset) => { expect(validateUserPreset(preset)).toMatchObject({ valid: true, errors: [], }); }); it.each([ [ 'an unknown container', { ...USER_PRESET, container: 'avi', fileExtension: 'avi' }, /container "avi".*allowlist/iu, ], [ 'a mismatched extension', { ...USER_PRESET, container: 'ipod', fileExtension: 'mp4', audio: { codec: 'aac' }, }, /fileExtension must be "m4a"/u, ], [ 'the wrong media kind', { ...USER_PRESET, kind: 'audio', container: 'webm', fileExtension: 'webm', video: undefined, audio: { codec: 'libopus' }, }, /container "webm".*kind "audio"/u, ], [ 'an incompatible video codec', { ...USER_PRESET, kind: 'video', container: 'mp4', fileExtension: 'mp4', video: { codec: 'libvpx-vp9', qualityMode: 'balanced' }, audio: { codec: 'aac' }, }, /video codec "libvpx-vp9".*container "mp4"/u, ], [ 'an incompatible audio codec', { ...USER_PRESET, kind: 'video', container: 'webm', fileExtension: 'webm', video: { codec: 'libvpx', qualityMode: 'balanced' }, audio: { codec: 'aac' }, }, /audio codec "aac".*container "webm"/u, ], [ 'audio in an image derivative', { ...USER_PRESET, kind: 'derivative', container: 'gif', fileExtension: 'gif', video: { codec: 'gif', qualityMode: 'compatibility' }, }, /container "gif" does not support audio settings/u, ], [ 'a video container without video', { ...USER_PRESET, kind: 'video', container: 'mp4', fileExtension: 'mp4', audio: { codec: 'aac' }, video: undefined, }, /container "mp4" requires video settings/u, ], [ 'fast-start outside MP4/M4A', { ...USER_PRESET, fastStart: true }, /fastStart is only supported.*not "mp3"/u, ], ])('rejects %s with a clear compatibility error', (_label, preset, error) => { const result = validateUserPreset(preset); expect(result.valid).toBe(false); expect(result.errors.join('; ')).toMatch(error); }); it.each([ ['metadataPolicy', 'edit', /metadataPolicy "edit".*Quick presets/u], ['chapterPolicy', 'replace', /chapterPolicy "replace".*Quick presets/u], [ 'subtitlePolicy', 'burn-in', /subtitlePolicy "burn-in".*explicit subtitle source/u, ], ])( 'rejects the unsupported Quick preset policy %s=%s', (field, value, error) => { const result = validateUserPreset({ ...USER_PRESET, [field]: value, }); expect(result.valid).toBe(false); expect(result.errors.join('; ')).toMatch(error); } ); it('applies relational compatibility checks to imported JSON', () => { expect(() => parseUserPresetJson( JSON.stringify({ ...USER_PRESET, kind: 'video', container: 'mp4', fileExtension: 'mp4', video: { codec: 'libvpx-vp9', qualityMode: 'balanced' }, audio: { codec: 'aac' }, }) ) ).toThrow(/video codec "libvpx-vp9".*container "mp4"/iu); }); it('supports create, clone, rename, export, import, delete and reset', async () => { const persistence = new MemoryUserPresetPersistence(); const registry = new UserPresetRegistry(persistence); await registry.initialize(); await registry.save(USER_PRESET); const clone = await registry.clone('my-podcast', 'my-podcast-copy', 'Copy'); expect(clone.name).toBe('Copy'); await registry.rename(clone.id, 'Renamed'); expect(registry.get(clone.id)?.name).toBe('Renamed'); const exported = registry.export('my-podcast'); await registry.delete('my-podcast'); expect(registry.get('my-podcast')).toBeUndefined(); await registry.import(exported); expect(registry.get('my-podcast')).toEqual(USER_PRESET); await registry.reset(); expect(registry.list()).toEqual([]); const reloaded = new UserPresetRegistry(persistence); await reloaded.initialize(); expect(reloaded.list()).toEqual([]); }); });