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.

130 lines
4.5 KiB

import json
from pathlib import Path
import pytest
from voice_transcriptor.models import AppSettings
from voice_transcriptor.services.settings import 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-4o-transcribe", "pt-BR", documents)
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,
}
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-4o-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 warning is None