You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
144 lines
5.1 KiB
144 lines
5.1 KiB
import json |
|
from pathlib import Path |
|
|
|
import pytest |
|
|
|
from voice_transcriptor.models import AppSettings |
|
from voice_transcriptor.services.settings import DEFAULT_CONTEXT, SettingsError, SettingsRepository |
|
|
|
|
|
def test_load_missing_file_returns_documented_defaults(monkeypatch, tmp_path: Path) -> None: |
|
documents = tmp_path / "Documents" |
|
documents.mkdir() |
|
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) |
|
|
|
settings, warning = SettingsRepository(tmp_path / "settings.json").load() |
|
|
|
assert settings == AppSettings("gpt-transcribe", "pt-BR", documents) |
|
assert "AWS, Kubernetes, OpenAI" in settings.context_vocabulary |
|
assert warning is None |
|
|
|
|
|
def test_load_missing_file_uses_home_when_documents_is_unavailable( |
|
monkeypatch, tmp_path: Path |
|
) -> None: |
|
monkeypatch.setattr(Path, "home", classmethod(lambda cls: tmp_path)) |
|
|
|
settings, warning = SettingsRepository(tmp_path / "settings.json").load() |
|
|
|
assert settings.output_directory == tmp_path |
|
assert warning is None |
|
|
|
|
|
def test_save_and_load_round_trip_utf8_settings(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
expected = AppSettings("gpt-4o-transcribe", "pt-BR", tmp_path / "Transcrições") |
|
repository = SettingsRepository(path) |
|
|
|
repository.save(expected) |
|
actual, warning = repository.load() |
|
|
|
assert actual == expected |
|
assert warning is None |
|
assert json.loads(path.read_text(encoding="utf-8")) == { |
|
"model": "gpt-4o-transcribe", |
|
"language": "pt-BR", |
|
"output_directory": str(tmp_path / "Transcrições"), |
|
"chunk_duration_seconds": 900, |
|
"chunk_overlap_seconds": 15, |
|
"retain_temporary_files": False, |
|
"context_vocabulary": DEFAULT_CONTEXT, |
|
} |
|
|
|
|
|
def test_load_malformed_file_recovers_defaults_with_nonfatal_warning(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
path.write_text("not json", encoding="utf-8") |
|
|
|
settings, warning = SettingsRepository(path).load() |
|
|
|
assert settings.model == "gpt-transcribe" |
|
assert warning is not None |
|
assert "settings" in warning.lower() |
|
|
|
|
|
def test_save_replaces_existing_file_atomically(monkeypatch, tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
path.write_text("old", encoding="utf-8") |
|
calls: list[tuple[Path, Path]] = [] |
|
original_replace = Path.replace |
|
|
|
def recording_replace(source: Path, destination: Path) -> Path: |
|
calls.append((source, destination)) |
|
return original_replace(source, destination) |
|
|
|
monkeypatch.setattr(Path, "replace", recording_replace) |
|
|
|
SettingsRepository(path).save(AppSettings("model", "pt-BR", tmp_path / "out")) |
|
|
|
assert calls |
|
source, destination = calls[0] |
|
assert source.parent == path.parent |
|
assert destination == path |
|
assert json.loads(path.read_text(encoding="utf-8"))["model"] == "model" |
|
|
|
|
|
def test_save_preserves_settings_error_when_replace_and_cleanup_fail( |
|
monkeypatch, tmp_path: Path |
|
) -> None: |
|
path = tmp_path / "settings.json" |
|
replacement_failure = OSError("target is locked") |
|
|
|
def failing_replace(source: Path, destination: Path) -> Path: |
|
raise replacement_failure |
|
|
|
def failing_unlink(path: Path, missing_ok: bool = False) -> None: |
|
raise OSError("temporary file is locked") |
|
|
|
monkeypatch.setattr(Path, "replace", failing_replace) |
|
monkeypatch.setattr(Path, "unlink", failing_unlink) |
|
|
|
with pytest.raises(SettingsError) as caught: |
|
SettingsRepository(path).save(AppSettings("model", "pt-BR", tmp_path / "out")) |
|
|
|
assert caught.value.__cause__ is replacement_failure |
|
|
|
|
|
def test_saved_settings_never_include_api_key_fields(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
|
|
SettingsRepository(path).save(AppSettings("model", "pt-BR", tmp_path / "out")) |
|
|
|
serialized = path.read_text(encoding="utf-8").lower() |
|
assert "api" not in serialized |
|
assert "key" not in serialized |
|
|
|
|
|
def test_preprocessing_settings_round_trip(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
expected = AppSettings("model", "pt-BR", tmp_path, 1200, 20, True) |
|
repository = SettingsRepository(path) |
|
repository.save(expected) |
|
actual, warning = repository.load() |
|
assert actual == expected |
|
assert warning is None |
|
|
|
|
|
def test_legacy_settings_receive_preprocessing_defaults(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
path.write_text(json.dumps({"model": "model", "language": "pt-BR", "output_directory": str(tmp_path)}), encoding="utf-8") |
|
settings, warning = SettingsRepository(path).load() |
|
assert (settings.chunk_duration_seconds, settings.chunk_overlap_seconds, settings.retain_temporary_files) == (900, 15, False) |
|
assert settings.context_vocabulary == DEFAULT_CONTEXT |
|
assert warning is None |
|
|
|
|
|
def test_context_vocabulary_round_trips_as_non_secret_setting(tmp_path: Path) -> None: |
|
path = tmp_path / "settings.json" |
|
expected = AppSettings("future-transcribe", "pt-BR", tmp_path, context_vocabulary="Brasília, Pix") |
|
|
|
SettingsRepository(path).save(expected) |
|
actual, warning = SettingsRepository(path).load() |
|
|
|
assert actual == expected |
|
assert warning is None
|
|
|