"""Tests for transcription.config — settings loading, provider defaults, and paths.""" from pathlib import Path import pytest from pydantic import ValidationError from transcription.config import Provider from transcription.config import Settings from transcription.config import parse_cli_settings def _make_settings(**overrides) -> Settings: """Build a Settings instance with a dummy API key unless overridden.""" defaults = {"openrouter_api_key": "test-key-abc123", "provider_models": None} defaults.update(overrides) return Settings(**defaults) class TestSettingsLoading: """Verify Settings construction and required-field validation.""" def test_loads_from_env(self, monkeypatch): """Settings constructs when OPENROUTER_API_KEY is provided.""" monkeypatch.setenv("OPENROUTER_API_KEY", "test-key-xyz") settings = Settings() assert settings.openrouter_api_key.get_secret_value() == "test-key-xyz" def test_requires_api_key(self, monkeypatch): """Settings raises ValidationError when OPENROUTER_API_KEY is missing.""" monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) with pytest.raises(ValidationError): Settings(_env_file=None) def test_ignores_process_cli_arguments(self, monkeypatch): """Ordinary settings construction does not consume tooling arguments.""" monkeypatch.setattr("sys.argv", ["pytest", "--rootdir=/tmp/project"]) settings = _make_settings() assert settings.port == 8000 def test_explicit_cli_parser_reads_arguments(self): """The executable settings boundary accepts application CLI flags.""" settings = parse_cli_settings( [ "--openrouter-api-key", "test-key", "--port", "8123", "--reload", ] ) assert settings.openrouter_api_key.get_secret_value() == "test-key" assert settings.port == 8123 assert settings.reload is True class TestProviderSettings: """Verify provider enum defaults and validation.""" def test_defaults_to_openrouter(self): """Default provider is openrouter when not explicitly set.""" settings = _make_settings() assert settings.provider == Provider.OPENROUTER assert settings.provider == "openrouter" def test_rejects_invalid_value(self): """Setting PROVIDER to an invalid value raises ValidationError.""" with pytest.raises(ValidationError): _make_settings(provider="not-a-provider") def test_optional_provider_header_fields_default_to_none(self): """openrouter_http_referer and openrouter_app_title are None when unset.""" settings = _make_settings() assert settings.openrouter_http_referer is None assert settings.openrouter_app_title is None @pytest.mark.parametrize( ("field", "value"), [ ("transcription_temperature", -0.1), ("transcription_temperature", 2.1), ("transcription_top_p", -0.1), ("transcription_top_p", 1.1), ], ) def test_rejects_sampling_values_outside_provider_ranges(self, field, value): with pytest.raises(ValidationError): _make_settings(**{field: value}) def test_rejects_prompt_paths_outside_prompt_directory(self): with pytest.raises(ValidationError): _make_settings(default_prompt_name="../secret.md") def test_settings_are_immutable_runtime_snapshots(self): settings = _make_settings() with pytest.raises(ValidationError): settings.port = 9000 def test_provider_model_accepts_env_default(self, monkeypatch): """provider_model is sourced when provided through environment configuration.""" monkeypatch.setenv("PROVIDER_MODEL", "google/gemini-2.5-flash") settings = Settings(openrouter_api_key="test-key-abc123") assert settings.provider_model == "google/gemini-2.5-flash" def test_provider_models_defaults_to_default_model(self): settings = _make_settings(provider_model="vendor/default") assert settings.provider_models == ("vendor/default",) def test_provider_models_are_default_first_trimmed_and_deduplicated(self): settings = _make_settings( provider_model=" vendor/default ", provider_models=["vendor/alternate", "vendor/default", " vendor/other "], ) assert settings.provider_models == ("vendor/default", "vendor/alternate", "vendor/other") def test_provider_models_rejects_empty_list(self): with pytest.raises(ValidationError): _make_settings(provider_models=[]) def test_provider_models_loads_json_from_environment(self, monkeypatch): monkeypatch.setenv("PROVIDER_MODEL", "vendor/default") monkeypatch.setenv("PROVIDER_MODELS", '["vendor/alternate","vendor/default"]') settings = Settings(openrouter_api_key="test-key-abc123") assert settings.provider_models == ("vendor/default", "vendor/alternate") class TestPathSettings: """Verify filesystem path field types.""" def test_path_fields_are_path_objects(self): """upload_dir and prompt_dir are Path instances.""" settings = _make_settings() assert isinstance(settings.upload_dir, Path) assert isinstance(settings.prompt_dir, Path) class TestWorkerReliabilitySettings: """Verify worker retry settings defaults.""" def test_worker_retry_defaults(self): """worker retry settings default to no retries.""" settings = _make_settings() assert settings.worker_max_retries == 0 def test_provider_timeout_is_not_capped_at_twenty_seconds(): """HIGH-03: vision transcription regularly runs past the old le=20.0 ceiling.""" settings = Settings(openrouter_api_key="test-key", worker_provider_timeout_seconds=300.0) assert settings.worker_provider_timeout_seconds == 300.0 def test_provider_timeout_must_still_be_positive(): with pytest.raises(ValidationError): Settings(openrouter_api_key="test-key", worker_provider_timeout_seconds=0.0) def test_openrouter_client_timeout_tracks_the_configured_budget(): """HIGH-03: httpx defaults every phase to 5s, silently capping the provider call.""" from transcription.providers.openrouter import OpenRouterTranscriptionProvider settings = Settings(openrouter_api_key="test-key", worker_provider_timeout_seconds=123.0) provider = OpenRouterTranscriptionProvider(settings=settings) timeout = provider._capturing_client._client.timeout assert timeout.read == 123.0 assert timeout.write == 123.0 assert timeout.pool == 123.0 assert timeout.connect == 10.0