Files
transcription/tests/test_config.py
T
2026-08-23 18:30:54 -05:00

198 lines
7.6 KiB
Python

"""Tests for transcription.config — settings loading, provider defaults, and paths."""
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any
import pytest
from pydantic import ValidationError
from transcription.config import Provider
from transcription.config import Settings
from transcription.config import configure_logging
from transcription.config import parse_cli_settings
def _make_settings(**overrides: Any) -> Settings:
"""Build a Settings instance with a dummy API key, isolated from any local .env."""
defaults: dict[str, Any] = {"openrouter_api_key": "test-key-abc123", "provider_models": None}
defaults.update(overrides)
return Settings(_env_file=None, **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)
assert isinstance(settings.log_dir, Path)
assert isinstance(settings.database_backup_dir, Path)
def test_configure_logging_writes_rotating_file_logs_to_configured_directory(tmp_path):
log_dir = tmp_path / "logs"
settings = _make_settings(log_dir=log_dir, log_file_name="app.log")
configure_logging(settings)
root_handlers = logging.getLogger().handlers
file_handlers = [handler for handler in root_handlers if isinstance(handler, RotatingFileHandler)]
assert file_handlers
assert Path(file_handlers[0].baseFilename) == log_dir / "app.log"
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
assert settings.worker_stale_job_seconds == 30.0
assert settings.worker_retry_backoff_seconds == 1.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)
assert provider._capturing_client is not None
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