from __future__ import annotations import pytest from transcription.config import Settings from transcription.errors import ErrorCategory from transcription.services.prompts import PromptStore from transcription.services.prompts import PromptStoreError @pytest.fixture def prompt_store(tmp_path): prompt_dir = tmp_path / "prompts" prompt_dir.mkdir() (prompt_dir / "transcribe_document.md").write_text("Original prompt\n", encoding="utf-8") (prompt_dir / "notes.txt").write_text("Not a prompt\n", encoding="utf-8") settings = Settings(openrouter_api_key="test-key", prompt_dir=prompt_dir) return PromptStore(settings=settings), prompt_dir def test_list_and_read_existing_markdown_prompts(prompt_store): store, _ = prompt_store prompts = store.list_prompts() assert [item.name for item in prompts] == ["transcribe_document.md"] assert prompts[0].is_default is True assert prompts[0].has_backup is False assert store.read_prompt("transcribe_document.md") == "Original prompt\n" def test_write_rotates_single_backup_and_recovery_swaps_versions(prompt_store): store, prompt_dir = prompt_store prompt_path = prompt_dir / "transcribe_document.md" backup_path = prompt_dir / "transcribe_document.md.bak" store.write_prompt(prompt_path.name, "Second prompt") assert prompt_path.read_text(encoding="utf-8") == "Second prompt\n" assert backup_path.read_text(encoding="utf-8") == "Original prompt\n" store.write_prompt(prompt_path.name, "Third prompt") assert prompt_path.read_text(encoding="utf-8") == "Third prompt\n" assert backup_path.read_text(encoding="utf-8") == "Second prompt\n" assert store.list_prompts()[0].has_backup is True store.recover_prompt(prompt_path.name) assert prompt_path.read_text(encoding="utf-8") == "Second prompt\n" assert backup_path.read_text(encoding="utf-8") == "Third prompt\n" assert list(prompt_dir.glob("*.tmp")) == [] @pytest.mark.parametrize( "name", [ "../outside.md", "nested/prompt.md", r"nested\prompt.md", "prompt.txt", ], ) def test_prompt_names_are_constrained(prompt_store, name): store, _ = prompt_store with pytest.raises(PromptStoreError) as caught: store.read_prompt(name) assert caught.value.category == ErrorCategory.VALIDATION def test_prompt_creation_and_empty_content_are_rejected(prompt_store): store, _ = prompt_store with pytest.raises(PromptStoreError) as missing: store.write_prompt("new_prompt.md", "content") with pytest.raises(PromptStoreError) as empty: store.write_prompt("transcribe_document.md", " \n") assert missing.value.category == ErrorCategory.NOT_FOUND assert empty.value.category == ErrorCategory.VALIDATION def test_recovery_requires_a_backup(prompt_store): store, _ = prompt_store with pytest.raises(PromptStoreError) as caught: store.recover_prompt("transcribe_document.md") assert caught.value.category == ErrorCategory.NOT_FOUND def test_failed_active_replace_preserves_complete_prompt(prompt_store, monkeypatch): store, prompt_dir = prompt_store prompt_path = prompt_dir / "transcribe_document.md" original_replace = type(prompt_path).replace def fail_active_replace(path, target): if target == prompt_path: raise OSError("simulated replace failure") return original_replace(path, target) monkeypatch.setattr(type(prompt_path), "replace", fail_active_replace) with pytest.raises(PromptStoreError) as caught: store.write_prompt(prompt_path.name, "Replacement prompt") assert caught.value.category == ErrorCategory.INFRA_PERSISTENT assert prompt_path.read_text(encoding="utf-8") == "Original prompt\n" assert (prompt_dir / "transcribe_document.md.bak").read_text(encoding="utf-8") == "Original prompt\n" assert list(prompt_dir.glob(".*.tmp")) == []