import { describe, expect, it } from 'vitest' import { applySttSettingsUpdate, applyTtsSettingsUpdate, isTtsResponseFormat } from './piagentui-speech-settings.js' function stt(update: Record, existing = {}) { return applySttSettingsUpdate(existing, update) } function tts(update: Record, existing = {}) { return applyTtsSettingsUpdate(existing, update) } describe('applySttSettingsUpdate', () => { it('returns the existing settings untouched for an empty update', () => { const existing = { enabled: true, language: 'pt' } expect(stt({}, existing)).toEqual({ ok: true, next: existing }) }) it('applies a valid update of every field', () => { const result = stt({ enabled: true, providerKind: 'openai', mode: 'realtime', baseUrl: 'https://api.example.com/v1///', apiKeyRequired: true, transcriptionEndpoint: '/audio/transcriptions', transcriptionModel: ' whisper-1 ', language: ' en ', insertMode: 'replace', apiKey: ' secret ', }) expect(result).toEqual({ ok: true, next: { enabled: true, providerKind: 'openai', mode: 'realtime', baseUrl: 'https://api.example.com/v1', apiKeyRequired: true, transcriptionEndpoint: '/audio/transcriptions', transcriptionModel: 'whisper-1', language: 'en', insertMode: 'replace', apiKey: 'secret', }, }) }) it('does not mutate the existing settings', () => { const existing = { enabled: false } stt({ enabled: true }, existing) expect(existing).toEqual({ enabled: false }) }) it.each([ ['providerKind', { providerKind: 'other' }], ['mode', { mode: 'stream' }], ['baseUrl', { baseUrl: 'ftp://host' }], ['baseUrl', { baseUrl: 42 }], ['transcriptionEndpoint', { transcriptionEndpoint: 'audio' }], ['transcriptionModel', { transcriptionModel: ' ' }], ['language', { language: 5 }], ['insertMode', { insertMode: 'prepend' }], ])('rejects an invalid %s', (field, update) => { expect(stt(update)).toEqual({ ok: false, error: `Invalid ${field}` }) }) it('coerces enabled/apiKeyRequired to booleans', () => { expect(stt({ enabled: 'yes', apiKeyRequired: 1 })).toEqual({ ok: true, next: { enabled: false, apiKeyRequired: false }, }) }) it('clears language with null or empty string', () => { expect(stt({ language: null }, { language: 'pt' })).toEqual({ ok: true, next: {} }) expect(stt({ language: '' }, { language: 'pt' })).toEqual({ ok: true, next: {} }) }) it('handles apiKey: null clears, blank string keeps, non-string keeps', () => { expect(stt({ apiKey: null }, { apiKey: 'old' })).toEqual({ ok: true, next: {} }) expect(stt({ apiKey: ' ' }, { apiKey: 'old' })).toEqual({ ok: true, next: { apiKey: 'old' } }) expect(stt({ apiKey: 12 }, { apiKey: 'old' })).toEqual({ ok: true, next: { apiKey: 'old' } }) }) }) describe('applyTtsSettingsUpdate', () => { it('returns the existing settings untouched for an empty update', () => { const existing = { voice: 'alloy' } expect(tts({}, existing)).toEqual({ ok: true, next: existing }) }) it('applies a valid update of every field', () => { const result = tts({ enabled: true, providerKind: 'openai-compatible', baseUrl: 'http://localhost:8880/v1/', apiKeyRequired: true, speechEndpoint: '/audio/speech', model: ' tts-1-hd ', voice: ' nova ', responseFormat: 'wav', speed: 1.5, apiKey: ' k ', }) expect(result).toEqual({ ok: true, next: { enabled: true, providerKind: 'openai-compatible', baseUrl: 'http://localhost:8880/v1', apiKeyRequired: true, speechEndpoint: '/audio/speech', model: 'tts-1-hd', voice: 'nova', responseFormat: 'wav', speed: 1.5, apiKey: 'k', }, }) }) it.each([ ['providerKind', { providerKind: 'x' }], ['baseUrl', { baseUrl: 'not a url' }], ['speechEndpoint', { speechEndpoint: 'speech' }], ['model', { model: '' }], ['voice', { voice: 3 }], ['responseFormat', { responseFormat: 'ogg' }], ['speed', { speed: '1' }], ['speed', { speed: 0.1 }], ['speed', { speed: 4.5 }], ])('rejects an invalid %s', (field, update) => { expect(tts(update)).toEqual({ ok: false, error: `Invalid ${field}` }) }) it('accepts speed at both bounds', () => { expect(tts({ speed: 0.25 })).toEqual({ ok: true, next: { speed: 0.25 } }) expect(tts({ speed: 4 })).toEqual({ ok: true, next: { speed: 4 } }) }) it('handles apiKey: null clears, blank string keeps', () => { expect(tts({ apiKey: null }, { apiKey: 'old' })).toEqual({ ok: true, next: {} }) expect(tts({ apiKey: ' ' }, { apiKey: 'old' })).toEqual({ ok: true, next: { apiKey: 'old' } }) }) }) describe('isTtsResponseFormat', () => { it('accepts supported formats only', () => { for (const f of ['mp3', 'opus', 'aac', 'flac', 'wav', 'pcm']) expect(isTtsResponseFormat(f)).toBe(true) expect(isTtsResponseFormat('ogg')).toBe(false) expect(isTtsResponseFormat(undefined)).toBe(false) }) })