"""Tests for translation style configuration.""" import json import pytest from pathlib import Path from src.config.style_config import StyleConfigManager from src.context.glossary import GlossaryManager from src.models.style import ( CONSISTENCY_PROMPTS, EQUIVALENCE_PROMPTS, FORMALITY_PROMPTS, GENDER_PROMPTS, READABILITY_PROMPTS, TRADITION_PROMPTS, EquivalenceApproach, FormalityLevel, GenderLanguage, ReadabilityLevel, TermConsistency, TheologicalTradition, TranslationStyle, build_system_prompt, ) from src.pipeline.prompt import SYSTEM_PROMPT, PromptBuilder class TestEquivalenceApproach: """Tests for EquivalenceApproach enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert EquivalenceApproach.FORMAL.value == "formal" assert EquivalenceApproach.DYNAMIC.value == "dynamic" assert EquivalenceApproach.OPTIMAL.value == "optimal" def test_has_prompt_template(self) -> None: """Test each approach has a prompt template.""" for approach in EquivalenceApproach: assert approach in EQUIVALENCE_PROMPTS assert len(EQUIVALENCE_PROMPTS[approach]) > 0 class TestGenderLanguage: """Tests for GenderLanguage enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert GenderLanguage.TRADITIONAL.value == "traditional" assert GenderLanguage.MODERATE_INCLUSIVE.value == "moderate_inclusive" assert GenderLanguage.FULLY_INCLUSIVE.value == "fully_inclusive" def test_has_prompt_template(self) -> None: """Test each option has a prompt template.""" for option in GenderLanguage: assert option in GENDER_PROMPTS class TestReadabilityLevel: """Tests for ReadabilityLevel enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert ReadabilityLevel.SCHOLARLY.value == "scholarly" assert ReadabilityLevel.GENERAL_PUBLIC.value == "general_public" assert ReadabilityLevel.CHILDREN.value == "children" def test_has_prompt_template(self) -> None: """Test each level has a prompt template.""" for level in ReadabilityLevel: assert level in READABILITY_PROMPTS class TestTheologicalTradition: """Tests for TheologicalTradition enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert TheologicalTradition.EVANGELICAL.value == "evangelical" assert TheologicalTradition.CATHOLIC.value == "catholic" assert TheologicalTradition.ORTHODOX.value == "orthodox" assert TheologicalTradition.ECUMENICAL.value == "ecumenical" def test_has_prompt_template(self) -> None: """Test each tradition has a prompt template.""" for tradition in TheologicalTradition: assert tradition in TRADITION_PROMPTS class TestTermConsistency: """Tests for TermConsistency enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert TermConsistency.STRICT.value == "strict" assert TermConsistency.BALANCED.value == "balanced" assert TermConsistency.CONTEXTUAL.value == "contextual" def test_has_prompt_template(self) -> None: """Test each option has a prompt template.""" for option in TermConsistency: assert option in CONSISTENCY_PROMPTS class TestFormalityLevel: """Tests for FormalityLevel enum.""" def test_all_values_exist(self) -> None: """Test all expected values exist.""" assert FormalityLevel.CASUAL.value == "casual" assert FormalityLevel.STANDARD.value == "standard" assert FormalityLevel.FORMAL.value == "formal" def test_has_prompt_template(self) -> None: """Test each level has a prompt template.""" for level in FormalityLevel: assert level in FORMALITY_PROMPTS class TestTranslationStyle: """Tests for TranslationStyle dataclass.""" def test_default_values(self) -> None: """Test default values are set correctly.""" style = TranslationStyle() assert style.equivalence_approach == EquivalenceApproach.DYNAMIC assert style.gender_language == GenderLanguage.MODERATE_INCLUSIVE assert style.readability_level == ReadabilityLevel.WEB_NATIVE assert style.theological_tradition == TheologicalTradition.EVANGELICAL assert style.textual_basis == "MorphGNT/SBLGNT" assert style.term_consistency == TermConsistency.BALANCED assert style.formality_level == FormalityLevel.STANDARD assert "なり" in style.avoid_archaic assert style.target_style_reference == "現代のウェブ記事・ナレーションのような平易な日本語" assert style.additional_instructions is None def test_custom_values(self) -> None: """Test creating with custom values.""" style = TranslationStyle( equivalence_approach=EquivalenceApproach.FORMAL, gender_language=GenderLanguage.TRADITIONAL, readability_level=ReadabilityLevel.SCHOLARLY, theological_tradition=TheologicalTradition.CATHOLIC, textual_basis="Textus Receptus", term_consistency=TermConsistency.STRICT, formality_level=FormalityLevel.FORMAL, avoid_archaic=["候"], target_style_reference="文語訳-style", additional_instructions="Focus on liturgical usage", ) assert style.equivalence_approach == EquivalenceApproach.FORMAL assert style.theological_tradition == TheologicalTradition.CATHOLIC assert style.textual_basis == "Textus Receptus" assert style.additional_instructions == "Focus on liturgical usage" def test_to_dict(self) -> None: """Test conversion to dictionary.""" style = TranslationStyle() data = style.to_dict() assert data["equivalence_approach"] == "dynamic" assert data["gender_language"] == "moderate_inclusive" assert data["readability_level"] == "web_native" assert data["theological_tradition"] == "evangelical" assert data["term_consistency"] == "balanced" assert data["formality_level"] == "standard" assert isinstance(data["avoid_archaic"], list) def test_from_dict(self) -> None: """Test creation from dictionary.""" data = { "equivalence_approach": "formal", "gender_language": "traditional", "readability_level": "scholarly", "theological_tradition": "catholic", "textual_basis": "Custom", "term_consistency": "strict", "formality_level": "formal", "avoid_archaic": ["候"], "target_style_reference": "custom-style", "additional_instructions": "Test instructions", } style = TranslationStyle.from_dict(data) assert style.equivalence_approach == EquivalenceApproach.FORMAL assert style.gender_language == GenderLanguage.TRADITIONAL assert style.theological_tradition == TheologicalTradition.CATHOLIC assert style.additional_instructions == "Test instructions" def test_from_dict_with_defaults(self) -> None: """Test creation from partial dictionary uses defaults.""" data = {"equivalence_approach": "optimal"} style = TranslationStyle.from_dict(data) assert style.equivalence_approach == EquivalenceApproach.OPTIMAL assert style.gender_language == GenderLanguage.MODERATE_INCLUSIVE # Default def test_roundtrip(self) -> None: """Test to_dict and from_dict roundtrip.""" original = TranslationStyle( equivalence_approach=EquivalenceApproach.OPTIMAL, gender_language=GenderLanguage.FULLY_INCLUSIVE, additional_instructions="Test", ) data = original.to_dict() restored = TranslationStyle.from_dict(data) assert restored.equivalence_approach == original.equivalence_approach assert restored.gender_language == original.gender_language assert restored.additional_instructions == original.additional_instructions class TestBuildSystemPrompt: """Tests for build_system_prompt function.""" def test_default_style_prompt(self) -> None: """Test prompt with default style.""" style = TranslationStyle() prompt = build_system_prompt(style) assert "Dynamic/Functional" in prompt assert "Moderate Inclusive" in prompt assert "Web-native" in prompt assert "Protestant Evangelical" in prompt assert "MorphGNT/SBLGNT" in prompt def test_formal_equivalence_prompt(self) -> None: """Test prompt with formal equivalence.""" style = TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) prompt = build_system_prompt(style) assert "word-for-word" in prompt assert "Formal" in prompt def test_traditional_gender_prompt(self) -> None: """Test prompt with traditional gender language.""" style = TranslationStyle(gender_language=GenderLanguage.TRADITIONAL) prompt = build_system_prompt(style) assert "Traditional" in prompt assert "masculine generic" in prompt def test_scholarly_readability_prompt(self) -> None: """Test prompt with scholarly readability.""" style = TranslationStyle(readability_level=ReadabilityLevel.SCHOLARLY) prompt = build_system_prompt(style) assert "Scholarly" in prompt assert "academic" in prompt def test_catholic_tradition_prompt(self) -> None: """Test prompt with Catholic tradition.""" style = TranslationStyle( theological_tradition=TheologicalTradition.CATHOLIC ) prompt = build_system_prompt(style) assert "Catholic" in prompt def test_strict_consistency_prompt(self) -> None: """Test prompt with strict term consistency.""" style = TranslationStyle(term_consistency=TermConsistency.STRICT) prompt = build_system_prompt(style) assert "Strict" in prompt assert "same word" in prompt def test_formal_formality_prompt(self) -> None: """Test prompt with formal formality level.""" style = TranslationStyle(formality_level=FormalityLevel.FORMAL) prompt = build_system_prompt(style) assert "Formal/Literary" in prompt or "formal literary" in prompt def test_avoid_archaic_included(self) -> None: """Test archaic expressions are included.""" style = TranslationStyle(avoid_archaic=["なり", "候"]) prompt = build_system_prompt(style) assert "なり" in prompt assert "候" in prompt def test_additional_instructions_included(self) -> None: """Test additional instructions are included.""" style = TranslationStyle( additional_instructions="Focus on poetic language" ) prompt = build_system_prompt(style) assert "ADDITIONAL INSTRUCTIONS" in prompt assert "poetic language" in prompt def test_additional_instructions_not_included_when_none(self) -> None: """Test no additional instructions section when None.""" style = TranslationStyle(additional_instructions=None) prompt = build_system_prompt(style) assert "ADDITIONAL INSTRUCTIONS" not in prompt def test_input_format_section(self) -> None: """Test input format section is always included.""" style = TranslationStyle() prompt = build_system_prompt(style) assert "INPUT FORMAT" in prompt assert "Source text" in prompt assert "Japanese translation" in prompt class TestStyleConfigManager: """Tests for StyleConfigManager.""" def test_init_default_path(self) -> None: """Test initialization with default path.""" manager = StyleConfigManager() assert manager.config_path == Path("data/config/style.json") def test_init_custom_path(self, tmp_path: Path) -> None: """Test initialization with custom path.""" custom_path = tmp_path / "custom_style.json" manager = StyleConfigManager(custom_path) assert manager.config_path == custom_path def test_load_nonexistent_returns_default(self, tmp_path: Path) -> None: """Test loading from nonexistent file returns default.""" path = tmp_path / "nonexistent.json" manager = StyleConfigManager(path) style = manager.load() assert style.equivalence_approach == EquivalenceApproach.DYNAMIC def test_save_creates_file(self, tmp_path: Path) -> None: """Test save creates the config file.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) style = TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) manager.save(style) assert path.exists() def test_save_and_load_roundtrip(self, tmp_path: Path) -> None: """Test save and load roundtrip.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) original = TranslationStyle( equivalence_approach=EquivalenceApproach.OPTIMAL, gender_language=GenderLanguage.FULLY_INCLUSIVE, additional_instructions="Test roundtrip", ) manager.save(original) # Create new manager and load manager2 = StyleConfigManager(path) loaded = manager2.load() assert loaded.equivalence_approach == original.equivalence_approach assert loaded.gender_language == original.gender_language assert loaded.additional_instructions == original.additional_instructions def test_get_style_loads_lazily(self, tmp_path: Path) -> None: """Test get_style loads on first call.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) # First call loads default (file doesn't exist) style1 = manager.get_style() assert style1 is not None # Second call returns cached style2 = manager.get_style() assert style2 is style1 def test_set_style_in_memory(self, tmp_path: Path) -> None: """Test set_style only updates memory.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) new_style = TranslationStyle( equivalence_approach=EquivalenceApproach.FORMAL ) manager.set_style(new_style) # Memory updated assert manager.get_style().equivalence_approach == EquivalenceApproach.FORMAL # File not created yet assert not path.exists() def test_reset_to_default(self, tmp_path: Path) -> None: """Test reset_to_default returns default style.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) # Set custom style custom = TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) manager.set_style(custom) # Reset default = manager.reset_to_default() assert default.equivalence_approach == EquivalenceApproach.DYNAMIC def test_reload_discards_changes(self, tmp_path: Path) -> None: """Test reload discards in-memory changes.""" path = tmp_path / "style.json" # Save a style to file initial = TranslationStyle(equivalence_approach=EquivalenceApproach.OPTIMAL) with path.open("w") as f: json.dump(initial.to_dict(), f) manager = StyleConfigManager(path) manager.load() # Make in-memory change manager.set_style( TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) ) assert manager.get_style().equivalence_approach == EquivalenceApproach.FORMAL # Reload from file reloaded = manager.reload() assert reloaded.equivalence_approach == EquivalenceApproach.OPTIMAL def test_save_creates_directories(self, tmp_path: Path) -> None: """Test save creates parent directories.""" path = tmp_path / "nested" / "dir" / "style.json" manager = StyleConfigManager(path) manager.save(TranslationStyle()) assert path.exists() def test_save_without_argument_uses_current(self, tmp_path: Path) -> None: """Test save without argument uses current style.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) # Load default manager.load() # Save without argument manager.save() # Verify file exists and has default values assert path.exists() with path.open() as f: data = json.load(f) assert data["equivalence_approach"] == "dynamic" def test_load_returns_cached_style(self, tmp_path: Path) -> None: """Test load returns cached style on subsequent calls.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) # First load creates default style1 = manager.load() # Second load returns cached style2 = manager.load() assert style1 is style2 def test_save_without_loaded_style(self, tmp_path: Path) -> None: """Test save creates default when no style loaded.""" path = tmp_path / "style.json" manager = StyleConfigManager(path) # Save without loading first (no argument) manager.save() # Verify file exists and has default values assert path.exists() with path.open() as f: data = json.load(f) assert data["equivalence_approach"] == "dynamic" class TestPromptBuilderWithStyle: """Tests for PromptBuilder with style configuration.""" def test_default_system_prompt_without_style(self) -> None: """Test default system prompt when no style is set.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) prompt = builder.get_system_prompt() assert prompt == SYSTEM_PROMPT def test_system_prompt_with_style(self) -> None: """Test system prompt with custom style.""" glossary = GlossaryManager() style = TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) builder = PromptBuilder(glossary, style=style) prompt = builder.get_system_prompt() assert "word-for-word" in prompt assert prompt != SYSTEM_PROMPT def test_style_property_creates_default(self) -> None: """Test style property creates default when None.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) style = builder.style assert style.equivalence_approach == EquivalenceApproach.DYNAMIC def test_style_setter_invalidates_cache(self) -> None: """Test setting style invalidates cached prompt.""" glossary = GlossaryManager() style1 = TranslationStyle(equivalence_approach=EquivalenceApproach.FORMAL) builder = PromptBuilder(glossary, style=style1) # Get initial prompt prompt1 = builder.get_system_prompt() assert "word-for-word" in prompt1 # Change style style2 = TranslationStyle(equivalence_approach=EquivalenceApproach.DYNAMIC) builder.style = style2 # Get new prompt prompt2 = builder.get_system_prompt() assert "thought-for-thought" in prompt2 assert prompt1 != prompt2 def test_build_system_prompt_no_cache(self) -> None: """Test build_system_prompt always regenerates.""" glossary = GlossaryManager() builder = PromptBuilder(glossary) # Access style to initialize it _ = builder.style prompt = builder.build_system_prompt() assert "Dynamic/Functional" in prompt def test_system_prompt_cached(self) -> None: """Test system prompt is cached.""" glossary = GlossaryManager() style = TranslationStyle() builder = PromptBuilder(glossary, style=style) prompt1 = builder.get_system_prompt() prompt2 = builder.get_system_prompt() assert prompt1 is prompt2 # Same object (cached)