diff --git a/CHANGELOG.md b/CHANGELOG.md index 711b4ab..5a6ffd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ยท versioned wi restart-safe status tracking cannot become an unbounded database growth path. - **Frontend Quality Gate.** Added `npm run check:frontend` to validate every manifest module for syntax errors, duplicate manifest entries, and top-level declaration collisions before bundling. +- **Provider Boundary Regression Coverage.** Added deterministic tests for STT backend normalization and ComfyUI + workflow submission, prompt injection, polling, seed derivation, and image retrieval. - **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out requests keep the device slot held until the underlying worker actually exits. - **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly diff --git a/TODO.md b/TODO.md index e2c7b08..4d9dc76 100644 --- a/TODO.md +++ b/TODO.md @@ -168,8 +168,8 @@ the first block before investing in architecture or polish. - [ ] Voice-matching constraints. - [ ] Profile-completeness counting. - [x] Rating persistence. -- [ ] STT endpoint normalization. -- [ ] ComfyUI workflow submission. +- [x] STT endpoint normalization and backend-response shape handling. +- [x] ComfyUI workflow submission, prompt injection, polling, and image retrieval. - [ ] API failure and timeout behavior. ### Playwright smoke suite diff --git a/VERSION b/VERSION index 49e0a31..14bee92 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.23.1 +1.23.2 diff --git a/tests/test_conversation.py b/tests/test_conversation.py index ba8bda7..a9060ac 100644 --- a/tests/test_conversation.py +++ b/tests/test_conversation.py @@ -17,6 +17,8 @@ from routes.conversation import ( _consistent_image_cancel_events, _consistent_image_jobs, _fallback_attribute_response, + _bundled_comfyui_workflow, + _comfyui_run, ) @@ -63,6 +65,47 @@ class AttributionParserTests(unittest.TestCase): ) self.assertTrue(all(segment["type"] == "dialogue" for segment in result["segments"][1:])) + def test_bundled_comfyui_workflows_have_required_prompt_and_save_nodes(self): + for image_kind in ("portrait", "concept_art"): + raw, prompt_node, prompt_field, save_node = _bundled_comfyui_workflow(image_kind) + graph = json.loads(raw) + self.assertIn(prompt_node, graph) + self.assertEqual(graph[prompt_node]["class_type"], "CLIPTextEncode") + self.assertIn(prompt_field, graph[prompt_node]["inputs"]) + self.assertIn(save_node, graph) + self.assertIn("filename_prefix", graph[save_node]["inputs"]) + + def test_comfyui_submission_injects_prompt_polls_and_returns_image(self): + from unittest.mock import patch + + class ResponseStub: + def __init__(self, payload=None, content=b"", headers=None): + self._payload, self.content, self.headers = payload, content, headers or {} + self.ok = True + self.status_code = 200 + self.text = "" + def json(self): + return self._payload + def raise_for_status(self): + return None + + graph = { + "2": {"class_type": "CLIPTextEncode", "inputs": {"text": "old"}}, + "5": {"class_type": "KSampler", "inputs": {"seed": 0}}, + "7": {"class_type": "SaveImage", "inputs": {"filename_prefix": "test"}}, + } + post = ResponseStub({"prompt_id": "p1"}) + history = ResponseStub({"p1": {"status": {"completed": True}, "outputs": {"7": {"images": [{"filename": "out.png"}]}}}}) + view = ResponseStub(content=b"png", headers={"Content-Type": "image/png"}) + with patch("routes.conversation.requests.post", return_value=post) as post_call, \ + patch("routes.conversation.requests.get", side_effect=[history, view]), \ + patch("routes.conversation.time.sleep"): + result = _comfyui_run("http://comfyui:8188", json.dumps(graph), "2", "text", "7", "new prompt", "stable") + self.assertTrue(result.startswith("data:image/png;base64,")) + submitted = post_call.call_args.kwargs["json"]["prompt"] + self.assertEqual(submitted["2"]["inputs"]["text"], "new prompt") + self.assertNotEqual(submitted["5"]["inputs"]["seed"], 0) + def test_consistent_image_job_can_be_cancelled(self): from fastapi.testclient import TestClient from server import app diff --git a/tests/test_security.py b/tests/test_security.py index b2c1604..8f0a8f4 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -47,6 +47,18 @@ class SecurityValidationTests(unittest.TestCase): _validated_upload_suffix(filename, default=".wav", allowed={".wav"}) self.assertEqual(_validated_upload_suffix("VOICE.WAV", default=".wav", allowed={".wav"}), ".wav") + def test_stt_backend_aliases_and_response_shapes_are_normalized(self): + from routes.stt import _clean_stt_backend, _transcription_text_from_response + class ResponseStub: + def __init__(self, payload, text=""): + self._payload, self.text = payload, text + def json(self): + return self._payload + self.assertEqual(_clean_stt_backend("faster-whisper"), "faster_whisper") + self.assertEqual(_clean_stt_backend("groq"), "groq_whisper") + self.assertEqual(_transcription_text_from_response(ResponseStub({"segments": [{"text": " one "}, {"text": "two"}]})), "one two") + self.assertEqual(_transcription_text_from_response(ResponseStub(" plain text ")), "plain text") + def test_request_size_middleware_rejects_oversized_json(self): sent = []