""" Typed configuration models for the Voice Assistant. Purpose: Define and validate the configuration structures used by the application and dynamic provider loading system. Responsibilities: - Define the application configuration for STT, Agent, and TTS. - Define the provider catalog structure. - Represent provider-specific options without coupling the core configuration model to a specific vendor or implementation. - Describe provider capabilities for discovery and future tooling. Non-responsibilities: - Loading YAML files. - Reading environment variables or secrets. - Selecting or instantiating provider implementations. - Executing STT, Agent, or TTS functionality. Architecture: YAML | v Config Loader | v AppConfig / ProviderCatalog | v Provider Loader | v STTAdapter / AgentAdapter / TTSAdapter """ import pytest import yaml from voice_assistant.config.loader import ( load_config, load_provider_catalog, ) def test_load_development_config(): config = load_config() assert config.stt.provider == "nvidia" assert config.stt.options["server"] == "127.0.0.1:9000" assert config.stt.options["language_code"] == "pt-BR" assert config.stt.options["automatic_punctuation"] is True assert config.tts.provider == "voxcpm" assert config.tts.options["server"] == "http://127.0.0.1:8000" assert config.tts.options["model"] == "openbmb/VoxCPM2" assert config.tts.options["voice"] == "default" assert config.agent.provider == "mock" def test_load_config_missing_file(tmp_path): with pytest.raises( FileNotFoundError, match="Configuration file not found", ): load_config( environment="development", config_dir=tmp_path, ) def test_load_config_empty_file(tmp_path): config_file = tmp_path / "development.yaml" config_file.write_text( "", encoding="utf-8", ) with pytest.raises( ValueError, match="Configuration file is empty", ): load_config( environment="development", config_dir=tmp_path, ) def test_load_config_root_must_be_mapping(tmp_path): config_file = tmp_path / "development.yaml" config_file.write_text( "- item1\n- item2\n", encoding="utf-8", ) with pytest.raises( ValueError, match="Configuration root must be a mapping", ): load_config( environment="development", config_dir=tmp_path, ) def test_load_config_validates_required_fields(tmp_path): config_file = tmp_path / "development.yaml" config_file.write_text( yaml.safe_dump( { "stt": { "provider": "nvidia", "server": "127.0.0.1:9000", "language": "pt-BR", }, "tts": { "provider": "voxcpm", "server": "http://127.0.0.1:8000", "model": "openbmb/VoxCPM2", "voice": "default", }, # agent intentionally missing } ), encoding="utf-8", ) with pytest.raises( Exception, match="agent", ): load_config( environment="development", config_dir=tmp_path, ) def test_load_provider_catalog(): catalog = load_provider_catalog() assert "nvidia" in catalog.providers["stt"] assert "voxcpm" in catalog.providers["tts"] assert "mock" in catalog.providers["agent"] voxcpm = catalog.providers["tts"]["voxcpm"] assert voxcpm.name == "VoxCPM" assert ( voxcpm.implementation == "voice_assistant.tts.voxcpm:VoxCPMTTSAdapter" ) assert voxcpm.capabilities == { "realtime": True, "streaming": True, } def test_load_provider_catalog_missing_file(tmp_path): with pytest.raises( FileNotFoundError, match="Provider catalog not found", ): load_provider_catalog( config_dir=tmp_path, ) def test_load_config_uses_development_app_env( tmp_path, monkeypatch, ): development = tmp_path / "development.yaml" development.write_text( """ stt: provider: nvidia options: server: "dev-asr" tts: provider: voxcpm options: server: "dev-tts" model: "dev-model" voice: "default" agent: provider: mock options: {} """, encoding="utf-8", ) monkeypatch.setenv( "APP_ENV", "development", ) config = load_config( config_dir=tmp_path, ) assert config.stt.options["server"] == "dev-asr" assert config.tts.options["server"] == "dev-tts" assert config.tts.options["model"] == "dev-model" def test_load_config_rejects_unknown_environment( tmp_path, ): with pytest.raises( FileNotFoundError, match="Configuration file not found", ): load_config( environment="does-not-exist", config_dir=tmp_path, ) def test_load_config_uses_staging_app_env( tmp_path, monkeypatch, ): config_file = tmp_path / "staging.yaml" config_file.write_text( """ stt: provider: nvidia options: server: "staging-asr:9000" tts: provider: voxcpm options: server: "http://staging-tts:8000" model: "staging-model" voice: "staging-voice" agent: provider: mock options: {} """, encoding="utf-8", ) monkeypatch.setenv( "APP_ENV", "staging", ) config = load_config( config_dir=tmp_path, ) assert config.stt.provider == "nvidia" assert ( config.stt.options["server"] == "staging-asr:9000" ) assert config.tts.provider == "voxcpm" assert ( config.tts.options["server"] == "http://staging-tts:8000" ) assert config.tts.options["model"] == "staging-model" assert config.tts.options["voice"] == "staging-voice" assert config.agent.provider == "mock"