generated from john/python-template
Baseline was 207 diagnostics. Two real bugs were hiding in the noise: - tools/run_destructive_tests.py imported ctypes.wintypes at module scope, which raises on non-Windows, and called fcntl unconditionally. The Windows and POSIX implementations now live under a module-level sys.platform split. - tests/ui/test_sources_page.py constructed Source(...) without document_id. Structural fixes, not suppressions: - New src/transcription/db/loading.py owns the SQLModel-field to QueryableAttribute reinterpretation via orm_attribute()/selectinload()/ defer(). This removed 42 "# pyright: ignore[reportArgumentType]" comments across documents/jobs/people/sources. Its docstring records that selectinload(A.b, B.c) is NOT equivalent to the chained form: varargs applies the selectin strategy only to the last path element, which under lazy="raise" raises InvalidRequestError at render time. - db/session.py transaction_scope no longer accepts or yields AsyncSessionTransaction. No caller ever passed one, sessionmaker.begin() yields an AsyncSession, and the dead branch was latently buggy because services call .exec(). Cleared 7 workflows.py diagnostics. - services/registry.py RegistryService is bound by a new RegistryEntry Protocol instead of bare SQLModel, so the shared implementation can read id/label/normalized_label/is_active. Cleared 9 diagnostics. - Column expressions in sources.py/jobs.py/test_store.py wrap in sqlmodel col(), the idiom already used in registry.py. - read_source_navigation wraps its literal tuple bounds in literal(). - normalization.py narrows with isinstance(image, TiffImageFile) rather than comparing image.format, since tag_v2 is TIFF-only. - linked_people.render uses @ui.refreshable_method, the NiceGUI API for bound methods. - The OpenRouter capturing client re-raises ResponseNotRead when the response stream is not async rather than mis-wrapping it. Tooling gate: - New .pre-commit-config.yaml runs ruff check and ty check as blocking hooks. No pre-commit config previously existed. Negative-tested: injecting a type error fails both hooks. - The last two "# pyright: ignore" comments (config.py) are removed; ty does not honor pyright directives. One "# ty: ignore" remains, in tests/test_prompts.py, where the test deliberately assigns to a frozen field to assert ValidationError. - asyncio_default_fixture_loop_scope is pinned to "function" so pytest-asyncio behavior does not shift on upgrade. Verification: ruff check clean, ty check reports 0 diagnostics, 292 passed and 4 skipped, pre-commit passes and demonstrably fails on a regression, and tools/run_destructive_tests.py runs on Windows. Co-authored-by: Copilot App <[email protected]>
179 lines
6.8 KiB
Python
179 lines
6.8 KiB
Python
"""Tests for transcription.config — settings loading, provider defaults, and paths."""
|
|
|
|
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 parse_cli_settings
|
|
|
|
|
|
def _make_settings(**overrides: Any) -> Settings:
|
|
"""Build a Settings instance with a dummy API key unless overridden."""
|
|
defaults: dict[str, Any] = {"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)
|
|
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
|