From 2608c953eee654c08a70a8d2263da98684d85a65 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 4 Sep 2026 09:34:46 +0200 Subject: [PATCH] test: cover attribution and timeout safety --- CHANGELOG.md | 2 ++ TODO.md | 4 ++-- VERSION | 2 +- tests/test_conversation.py | 22 ++++++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc853d7..7635957 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi workflow submission, prompt injection, polling, seed derivation, and image retrieval. - **Casting Constraint Coverage.** Added deterministic regression coverage proving casting filters invented voice IDs and avoids unnecessary duplicate assignments when distinct catalogue voices are available. +- **Attribution and Timeout Safety Coverage.** Added regressions preventing phantom narrator speakers, preserving source + prose after malformed LLM JSON, clamping caller timeouts, and bounding upstream error text. - **4 GiB Container Validation.** Verified the current image serves the application under a hard 4 GiB cgroup limit with no OOM kill; the probe recorded approximately 50.6 MiB idle memory and 0.09% CPU. - **Profile Completeness Coverage.** Added browser regression coverage for empty, partial, and fully populated diff --git a/TODO.md b/TODO.md index ab9b435..983ccff 100644 --- a/TODO.md +++ b/TODO.md @@ -165,13 +165,13 @@ the first block before investing in architecture or polish. ### Automated tests -- [ ] Character parsing and phantom-speaker prevention. +- [x] Character parsing and phantom-speaker prevention, including malformed-response prose preservation. - [x] Voice-matching constraints reject invented voice IDs and avoid unnecessary duplicate assignments. - [x] Profile-completeness counting for empty, partial, and fully populated sheets. - [x] Rating persistence. - [x] STT endpoint normalization and backend-response shape handling. - [x] ComfyUI workflow submission, prompt injection, polling, and image retrieval. -- [ ] API failure and timeout behavior. +- [x] API timeout clamping and bounded upstream error text; live-provider failure drills remain environment-specific. ### Playwright smoke suite diff --git a/VERSION b/VERSION index ca8ec41..d8c40e5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.23.5 +1.23.6 diff --git a/tests/test_conversation.py b/tests/test_conversation.py index 81ea182..29402b9 100644 --- a/tests/test_conversation.py +++ b/tests/test_conversation.py @@ -21,6 +21,8 @@ from routes.conversation import ( _comfyui_run, _CastJobRequest, match_characters_voices, + _request_timeout_seconds, + _response_error_text, ) @@ -43,6 +45,26 @@ class AttributionParserTests(unittest.TestCase): self.assertEqual([segment["type"] for segment in result["segments"]], ["narration", "dialogue", "narration"]) self.assertIn("Ein langer Satz endet hier.", result["segments"][1]["text"]) + def test_attribution_normalization_prevents_phantom_narrator_and_preserves_text_on_bad_json(self): + raw = json.dumps({"segments": [ + {"speaker": "Invented Narrator", "type": "narration", "text": "A road disappeared.", "emotion": "dramatic"}, + {"speaker": "Mira", "type": "dialogue", "text": "We keep walking.", "emotion": ""}, + ]}) + result = _attribution_parse(raw, "unused") + self.assertEqual(result["segments"][0]["speaker"], "Narrator") + self.assertEqual(result["characters"], ["Mira"]) + fallback = _attribution_parse("not valid JSON", "Original prose must survive") + self.assertEqual(fallback["segments"], [{"speaker": "Narrator", "type": "narration", "text": "Original prose must survive", "emotion": ""}]) + + def test_api_timeout_and_error_text_are_bounded(self): + self.assertEqual(_request_timeout_seconds(-10), 5.0) + self.assertEqual(_request_timeout_seconds(99999), 600.0) + self.assertEqual(_request_timeout_seconds("not-a-number", default=12), 12.0) + class ResponseStub: + text = "x" * 5000 + reason = "Bad Gateway" + self.assertEqual(len(_response_error_text(ResponseStub())), 2000) + def test_repairs_epigraph_split_before_mixed_guillemet_close(self): raw = '{"segments":[' \ '{"speaker":"Schwester Larissa","type":"dialogue","text":"»Von dem Windhager Felsgesteyn bis zur Kueste von Mendena ziehet es sich hin, von Ifirns Eisgebirg bis zu dem dampfenden Waldt der Mohas, und"},' \