diff --git a/tests/test_application_coverage.py b/tests/test_application_coverage.py index 798de2c..48888ca 100644 --- a/tests/test_application_coverage.py +++ b/tests/test_application_coverage.py @@ -245,6 +245,71 @@ class TestConversionService: log_messages = [msg for msg, _ in events.logs] assert any("Conversion failed" in msg for msg in log_messages) + def test_tts_context_applies_normalization_overrides(self): + """Service applies normalization_overrides from request to apostrophe config.""" + from abogen.application.conversion_service import run_conversion + + with tempfile.TemporaryDirectory() as tmpdir: + from abogen.application.conversion_config import PronunciationConfig + + req = ConversionRequest( + direct_text="Hello", + voice="M1", + save_mode="custom_folder", + output_folder=Path(tmpdir), + pronunciation=PronunciationConfig( + normalization_overrides={"normalization_numbers": False}, + ), + ) + events = FakeEvents() + pipeline = FakePipelineProvider() + resolver = FakeVoiceResolver() + + result = run_conversion(req, events, pipeline, resolver) + assert result is not None + + def test_tts_context_rejects_unconfigured_llm_mode(self): + """Service raises RuntimeError if LLM apostrophe mode is selected but unconfigured.""" + from abogen.application.conversion_service import run_conversion + + with tempfile.TemporaryDirectory() as tmpdir: + from abogen.application.conversion_config import PronunciationConfig + + req = ConversionRequest( + direct_text="Hello", + voice="M1", + save_mode="custom_folder", + output_folder=Path(tmpdir), + pronunciation=PronunciationConfig( + normalization_overrides={"normalization_apostrophe_mode": "llm"}, + ), + ) + events = FakeEvents() + pipeline = FakePipelineProvider() + resolver = FakeVoiceResolver() + + with pytest.raises(RuntimeError, match="LLM.*apostrophe"): + run_conversion(req, events, pipeline, resolver) + + def test_usage_counter_populated_in_result(self): + """usage_counter is created and accessible in result.""" + from abogen.application.conversion_service import run_conversion + + with tempfile.TemporaryDirectory() as tmpdir: + req = ConversionRequest( + direct_text="Hello", + voice="M1", + save_mode="custom_folder", + output_folder=Path(tmpdir), + ) + events = FakeEvents() + pipeline = FakePipelineProvider() + resolver = FakeVoiceResolver() + + result = run_conversion(req, events, pipeline, resolver) + assert hasattr(result, "usage_counter") + assert isinstance(result.usage_counter, dict) + # ─── Tests for output_layout_service.py ───────────────────────────── diff --git a/tests/test_conversion_planner.py b/tests/test_conversion_planner.py index b9ae65d..4d52482 100644 --- a/tests/test_conversion_planner.py +++ b/tests/test_conversion_planner.py @@ -246,6 +246,30 @@ class TestWordSubstitution: assert len(plan.chapters) >= 1 assert "cat" in plan.chapters[0].body_text + def test_chunks_assigned_to_correct_chapter(self): + """Chunks are grouped by chapter_index and only assigned to matching chapters.""" + req = ConversionRequest( + direct_text="<>\nText A\n<>\nText B", + voice="M1", + chapter_chunk=ChapterChunkConfig( + chunks=[ + {"text": "Ch1 chunk", "chapter_index": 0}, + {"text": "Ch2 chunk", "chapter_index": 1}, + ], + ), + ) + plan = build_conversion_plan(req) + + assert len(plan.chapters) == 2 + # Ch1 should have only its chunk + ch1_texts = [s.text for s in plan.chapters[0].segments] + assert "Ch1 chunk" in ch1_texts + assert "Ch2 chunk" not in ch1_texts + # Ch2 should have only its chunk + ch2_texts = [s.text for s in plan.chapters[1].segments] + assert "Ch2 chunk" in ch2_texts + assert "Ch1 chunk" not in ch2_texts + def test_no_substitution_when_disabled(self): """No substitution when word_substitution is None.""" req = ConversionRequest(