/** * Speech Configuration Tests * * Issue #401: Tests for speech services environment variable validation * Tests cover STT, TTS (default, premium, fallback), and speech limits configuration. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { isSttEnabled, isTtsEnabled, isTtsPremiumEnabled, isTtsFallbackEnabled, validateSpeechConfig, getSpeechConfig, type SpeechConfig, } from "./speech.config"; describe("speech.config", () => { const originalEnv = { ...process.env }; beforeEach(() => { // Clear all speech-related env vars before each test delete process.env.STT_ENABLED; delete process.env.STT_BASE_URL; delete process.env.STT_MODEL; delete process.env.STT_LANGUAGE; delete process.env.TTS_ENABLED; delete process.env.TTS_DEFAULT_URL; delete process.env.TTS_DEFAULT_VOICE; delete process.env.TTS_DEFAULT_FORMAT; delete process.env.TTS_PREMIUM_ENABLED; delete process.env.TTS_PREMIUM_URL; delete process.env.TTS_FALLBACK_ENABLED; delete process.env.TTS_FALLBACK_URL; delete process.env.SPEECH_MAX_UPLOAD_SIZE; delete process.env.SPEECH_MAX_DURATION_SECONDS; delete process.env.SPEECH_MAX_TEXT_LENGTH; }); afterEach(() => { process.env = { ...originalEnv }; }); // ========================================== // STT enabled check // ========================================== describe("isSttEnabled", () => { it("should return false when STT_ENABLED is not set", () => { expect(isSttEnabled()).toBe(false); }); it("should return false when STT_ENABLED is 'false'", () => { process.env.STT_ENABLED = "false"; expect(isSttEnabled()).toBe(false); }); it("should return false when STT_ENABLED is '0'", () => { process.env.STT_ENABLED = "0"; expect(isSttEnabled()).toBe(false); }); it("should return false when STT_ENABLED is empty string", () => { process.env.STT_ENABLED = ""; expect(isSttEnabled()).toBe(false); }); it("should return true when STT_ENABLED is 'true'", () => { process.env.STT_ENABLED = "true"; expect(isSttEnabled()).toBe(true); }); it("should return true when STT_ENABLED is '1'", () => { process.env.STT_ENABLED = "1"; expect(isSttEnabled()).toBe(true); }); }); // ========================================== // TTS enabled check // ========================================== describe("isTtsEnabled", () => { it("should return false when TTS_ENABLED is not set", () => { expect(isTtsEnabled()).toBe(false); }); it("should return false when TTS_ENABLED is 'false'", () => { process.env.TTS_ENABLED = "false"; expect(isTtsEnabled()).toBe(false); }); it("should return true when TTS_ENABLED is 'true'", () => { process.env.TTS_ENABLED = "true"; expect(isTtsEnabled()).toBe(true); }); it("should return true when TTS_ENABLED is '1'", () => { process.env.TTS_ENABLED = "1"; expect(isTtsEnabled()).toBe(true); }); }); // ========================================== // TTS premium enabled check // ========================================== describe("isTtsPremiumEnabled", () => { it("should return false when TTS_PREMIUM_ENABLED is not set", () => { expect(isTtsPremiumEnabled()).toBe(false); }); it("should return false when TTS_PREMIUM_ENABLED is 'false'", () => { process.env.TTS_PREMIUM_ENABLED = "false"; expect(isTtsPremiumEnabled()).toBe(false); }); it("should return true when TTS_PREMIUM_ENABLED is 'true'", () => { process.env.TTS_PREMIUM_ENABLED = "true"; expect(isTtsPremiumEnabled()).toBe(true); }); }); // ========================================== // TTS fallback enabled check // ========================================== describe("isTtsFallbackEnabled", () => { it("should return false when TTS_FALLBACK_ENABLED is not set", () => { expect(isTtsFallbackEnabled()).toBe(false); }); it("should return false when TTS_FALLBACK_ENABLED is 'false'", () => { process.env.TTS_FALLBACK_ENABLED = "false"; expect(isTtsFallbackEnabled()).toBe(false); }); it("should return true when TTS_FALLBACK_ENABLED is 'true'", () => { process.env.TTS_FALLBACK_ENABLED = "true"; expect(isTtsFallbackEnabled()).toBe(true); }); }); // ========================================== // validateSpeechConfig // ========================================== describe("validateSpeechConfig", () => { describe("when all services are disabled", () => { it("should not throw when no speech services are enabled", () => { expect(() => validateSpeechConfig()).not.toThrow(); }); it("should not throw when services are explicitly disabled", () => { process.env.STT_ENABLED = "false"; process.env.TTS_ENABLED = "false"; process.env.TTS_PREMIUM_ENABLED = "false"; process.env.TTS_FALLBACK_ENABLED = "false"; expect(() => validateSpeechConfig()).not.toThrow(); }); }); describe("STT validation", () => { beforeEach(() => { process.env.STT_ENABLED = "true"; }); it("should throw when STT is enabled but STT_BASE_URL is missing", () => { expect(() => validateSpeechConfig()).toThrow("STT_BASE_URL"); expect(() => validateSpeechConfig()).toThrow( "STT is enabled (STT_ENABLED=true) but required environment variables are missing" ); }); it("should throw when STT_BASE_URL is empty string", () => { process.env.STT_BASE_URL = ""; expect(() => validateSpeechConfig()).toThrow("STT_BASE_URL"); }); it("should throw when STT_BASE_URL is whitespace only", () => { process.env.STT_BASE_URL = " "; expect(() => validateSpeechConfig()).toThrow("STT_BASE_URL"); }); it("should not throw when STT is enabled and STT_BASE_URL is set", () => { process.env.STT_BASE_URL = "http://speaches:8000/v1"; expect(() => validateSpeechConfig()).not.toThrow(); }); it("should suggest disabling STT in error message", () => { expect(() => validateSpeechConfig()).toThrow("STT_ENABLED=false"); }); }); describe("TTS default validation", () => { beforeEach(() => { process.env.TTS_ENABLED = "true"; }); it("should throw when TTS is enabled but TTS_DEFAULT_URL is missing", () => { expect(() => validateSpeechConfig()).toThrow("TTS_DEFAULT_URL"); expect(() => validateSpeechConfig()).toThrow( "TTS is enabled (TTS_ENABLED=true) but required environment variables are missing" ); }); it("should throw when TTS_DEFAULT_URL is empty string", () => { process.env.TTS_DEFAULT_URL = ""; expect(() => validateSpeechConfig()).toThrow("TTS_DEFAULT_URL"); }); it("should not throw when TTS is enabled and TTS_DEFAULT_URL is set", () => { process.env.TTS_DEFAULT_URL = "http://kokoro-tts:8880/v1"; expect(() => validateSpeechConfig()).not.toThrow(); }); it("should suggest disabling TTS in error message", () => { expect(() => validateSpeechConfig()).toThrow("TTS_ENABLED=false"); }); }); describe("TTS premium validation", () => { beforeEach(() => { process.env.TTS_PREMIUM_ENABLED = "true"; }); it("should throw when TTS premium is enabled but TTS_PREMIUM_URL is missing", () => { expect(() => validateSpeechConfig()).toThrow("TTS_PREMIUM_URL"); expect(() => validateSpeechConfig()).toThrow( "TTS premium is enabled (TTS_PREMIUM_ENABLED=true) but required environment variables are missing" ); }); it("should throw when TTS_PREMIUM_URL is empty string", () => { process.env.TTS_PREMIUM_URL = ""; expect(() => validateSpeechConfig()).toThrow("TTS_PREMIUM_URL"); }); it("should not throw when TTS premium is enabled and TTS_PREMIUM_URL is set", () => { process.env.TTS_PREMIUM_URL = "http://chatterbox-tts:8881/v1"; expect(() => validateSpeechConfig()).not.toThrow(); }); it("should suggest disabling TTS premium in error message", () => { expect(() => validateSpeechConfig()).toThrow("TTS_PREMIUM_ENABLED=false"); }); }); describe("TTS fallback validation", () => { beforeEach(() => { process.env.TTS_FALLBACK_ENABLED = "true"; }); it("should throw when TTS fallback is enabled but TTS_FALLBACK_URL is missing", () => { expect(() => validateSpeechConfig()).toThrow("TTS_FALLBACK_URL"); expect(() => validateSpeechConfig()).toThrow( "TTS fallback is enabled (TTS_FALLBACK_ENABLED=true) but required environment variables are missing" ); }); it("should throw when TTS_FALLBACK_URL is empty string", () => { process.env.TTS_FALLBACK_URL = ""; expect(() => validateSpeechConfig()).toThrow("TTS_FALLBACK_URL"); }); it("should not throw when TTS fallback is enabled and TTS_FALLBACK_URL is set", () => { process.env.TTS_FALLBACK_URL = "http://openedai-speech:8000/v1"; expect(() => validateSpeechConfig()).not.toThrow(); }); it("should suggest disabling TTS fallback in error message", () => { expect(() => validateSpeechConfig()).toThrow("TTS_FALLBACK_ENABLED=false"); }); }); describe("multiple services enabled simultaneously", () => { it("should validate all enabled services", () => { process.env.STT_ENABLED = "true"; process.env.TTS_ENABLED = "true"; // Missing both STT_BASE_URL and TTS_DEFAULT_URL expect(() => validateSpeechConfig()).toThrow("STT_BASE_URL"); }); it("should pass when all enabled services are properly configured", () => { process.env.STT_ENABLED = "true"; process.env.STT_BASE_URL = "http://speaches:8000/v1"; process.env.TTS_ENABLED = "true"; process.env.TTS_DEFAULT_URL = "http://kokoro-tts:8880/v1"; process.env.TTS_PREMIUM_ENABLED = "true"; process.env.TTS_PREMIUM_URL = "http://chatterbox-tts:8881/v1"; process.env.TTS_FALLBACK_ENABLED = "true"; process.env.TTS_FALLBACK_URL = "http://openedai-speech:8000/v1"; expect(() => validateSpeechConfig()).not.toThrow(); }); }); describe("limits validation", () => { it("should throw when SPEECH_MAX_UPLOAD_SIZE is not a valid number", () => { process.env.SPEECH_MAX_UPLOAD_SIZE = "not-a-number"; expect(() => validateSpeechConfig()).toThrow("SPEECH_MAX_UPLOAD_SIZE"); expect(() => validateSpeechConfig()).toThrow("must be a positive integer"); }); it("should throw when SPEECH_MAX_UPLOAD_SIZE is negative", () => { process.env.SPEECH_MAX_UPLOAD_SIZE = "-100"; expect(() => validateSpeechConfig()).toThrow("SPEECH_MAX_UPLOAD_SIZE"); }); it("should throw when SPEECH_MAX_UPLOAD_SIZE is zero", () => { process.env.SPEECH_MAX_UPLOAD_SIZE = "0"; expect(() => validateSpeechConfig()).toThrow("SPEECH_MAX_UPLOAD_SIZE"); }); it("should throw when SPEECH_MAX_DURATION_SECONDS is not a valid number", () => { process.env.SPEECH_MAX_DURATION_SECONDS = "abc"; expect(() => validateSpeechConfig()).toThrow("SPEECH_MAX_DURATION_SECONDS"); }); it("should throw when SPEECH_MAX_TEXT_LENGTH is not a valid number", () => { process.env.SPEECH_MAX_TEXT_LENGTH = "xyz"; expect(() => validateSpeechConfig()).toThrow("SPEECH_MAX_TEXT_LENGTH"); }); it("should not throw when limits are valid positive integers", () => { process.env.SPEECH_MAX_UPLOAD_SIZE = "50000000"; process.env.SPEECH_MAX_DURATION_SECONDS = "1200"; process.env.SPEECH_MAX_TEXT_LENGTH = "8192"; expect(() => validateSpeechConfig()).not.toThrow(); }); it("should not throw when limits are not set (uses defaults)", () => { expect(() => validateSpeechConfig()).not.toThrow(); }); }); }); // ========================================== // getSpeechConfig // ========================================== describe("getSpeechConfig", () => { it("should return default values when no env vars are set", () => { const config = getSpeechConfig(); expect(config.stt.enabled).toBe(false); expect(config.stt.baseUrl).toBe("http://speaches:8000/v1"); expect(config.stt.model).toBe("Systran/faster-whisper-large-v3-turbo"); expect(config.stt.language).toBe("en"); expect(config.tts.default.enabled).toBe(false); expect(config.tts.default.url).toBe("http://kokoro-tts:8880/v1"); expect(config.tts.default.voice).toBe("af_heart"); expect(config.tts.default.format).toBe("mp3"); expect(config.tts.premium.enabled).toBe(false); expect(config.tts.premium.url).toBe("http://chatterbox-tts:8881/v1"); expect(config.tts.fallback.enabled).toBe(false); expect(config.tts.fallback.url).toBe("http://openedai-speech:8000/v1"); expect(config.limits.maxUploadSize).toBe(25000000); expect(config.limits.maxDurationSeconds).toBe(600); expect(config.limits.maxTextLength).toBe(4096); }); it("should use custom env var values when set", () => { process.env.STT_ENABLED = "true"; process.env.STT_BASE_URL = "http://custom-stt:9000/v1"; process.env.STT_MODEL = "custom-model"; process.env.STT_LANGUAGE = "fr"; process.env.TTS_ENABLED = "true"; process.env.TTS_DEFAULT_URL = "http://custom-tts:9001/v1"; process.env.TTS_DEFAULT_VOICE = "custom_voice"; process.env.TTS_DEFAULT_FORMAT = "wav"; process.env.TTS_PREMIUM_ENABLED = "true"; process.env.TTS_PREMIUM_URL = "http://custom-premium:9002/v1"; process.env.TTS_FALLBACK_ENABLED = "true"; process.env.TTS_FALLBACK_URL = "http://custom-fallback:9003/v1"; process.env.SPEECH_MAX_UPLOAD_SIZE = "50000000"; process.env.SPEECH_MAX_DURATION_SECONDS = "1200"; process.env.SPEECH_MAX_TEXT_LENGTH = "8192"; const config = getSpeechConfig(); expect(config.stt.enabled).toBe(true); expect(config.stt.baseUrl).toBe("http://custom-stt:9000/v1"); expect(config.stt.model).toBe("custom-model"); expect(config.stt.language).toBe("fr"); expect(config.tts.default.enabled).toBe(true); expect(config.tts.default.url).toBe("http://custom-tts:9001/v1"); expect(config.tts.default.voice).toBe("custom_voice"); expect(config.tts.default.format).toBe("wav"); expect(config.tts.premium.enabled).toBe(true); expect(config.tts.premium.url).toBe("http://custom-premium:9002/v1"); expect(config.tts.fallback.enabled).toBe(true); expect(config.tts.fallback.url).toBe("http://custom-fallback:9003/v1"); expect(config.limits.maxUploadSize).toBe(50000000); expect(config.limits.maxDurationSeconds).toBe(1200); expect(config.limits.maxTextLength).toBe(8192); }); it("should return typed SpeechConfig object", () => { const config: SpeechConfig = getSpeechConfig(); // Verify structure matches the SpeechConfig type expect(config).toHaveProperty("stt"); expect(config).toHaveProperty("tts"); expect(config).toHaveProperty("limits"); expect(config.tts).toHaveProperty("default"); expect(config.tts).toHaveProperty("premium"); expect(config.tts).toHaveProperty("fallback"); }); it("should handle partial env var overrides", () => { process.env.STT_ENABLED = "true"; process.env.STT_BASE_URL = "http://custom-stt:9000/v1"; // STT_MODEL and STT_LANGUAGE not set, should use defaults const config = getSpeechConfig(); expect(config.stt.enabled).toBe(true); expect(config.stt.baseUrl).toBe("http://custom-stt:9000/v1"); expect(config.stt.model).toBe("Systran/faster-whisper-large-v3-turbo"); expect(config.stt.language).toBe("en"); }); it("should parse numeric limits correctly", () => { process.env.SPEECH_MAX_UPLOAD_SIZE = "10000000"; const config = getSpeechConfig(); expect(typeof config.limits.maxUploadSize).toBe("number"); expect(config.limits.maxUploadSize).toBe(10000000); }); }); // ========================================== // registerAs integration // ========================================== describe("speechConfig (registerAs factory)", () => { it("should be importable as a config namespace factory", async () => { const { speechConfig } = await import("./speech.config"); expect(speechConfig).toBeDefined(); expect(speechConfig.KEY).toBe("CONFIGURATION(speech)"); }); it("should return config object when called", async () => { const { speechConfig } = await import("./speech.config"); const config = speechConfig() as SpeechConfig; expect(config).toHaveProperty("stt"); expect(config).toHaveProperty("tts"); expect(config).toHaveProperty("limits"); }); }); });