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