from __future__ import annotations import base64 import unittest from unittest import mock from services.config import config from services.openai_backend_api import OpenAIBackendAPI from services.protocol.conversation import ImageOutput, extract_conversation_ids from services.protocol.openai_v1_response import stream_image_response def _conversation(file_ids: list[str], sediment_ids: list[str] | None = None) -> dict: parts: list[object] = [ {"content_type": "image_asset_pointer", "asset_pointer": f"file-service://{file_id}"} for file_id in file_ids ] parts.extend(f"sediment://{sediment_id}" for sediment_id in (sediment_ids or [])) return { "mapping": { "tool": { "message": { "author": {"role": "tool"}, "create_time": 1, "metadata": {"async_task_type": "image_gen"}, "content": {"content_type": "multimodal_text", "parts": parts}, } } } } class FakeBackend(OpenAIBackendAPI): def __init__(self, conversations: list[dict] | None = None) -> None: self.conversations = conversations or [] self.calls = 0 self.file_urls: dict[str, str] = {} self.sediment_urls: dict[str, str] = {} def _get_conversation(self, conversation_id: str) -> dict: self.calls += 1 index = min(self.calls - 1, len(self.conversations) - 1) return self.conversations[index] def _get_file_download_url(self, file_id: str) -> str: return self.file_urls.get(file_id, "") def _get_attachment_download_url(self, conversation_id: str, attachment_id: str) -> str: return self.sediment_urls.get(attachment_id, "") class MultiImageResultTests(unittest.TestCase): def test_stream_id_extractor_keeps_full_file_ids(self) -> None: payload = ( '{"conversation_id":"conv-1"} ' 'file-service://file-first_123-extra sediment://sed-second_456-extra' ) conversation_id, file_ids, sediment_ids = extract_conversation_ids(payload) self.assertEqual(conversation_id, "conv-1") self.assertEqual(file_ids, ["file-first_123-extra"]) self.assertEqual(sediment_ids, ["sed-second_456-extra"]) def test_conversation_record_extractor_finds_all_generated_assets(self) -> None: backend = FakeBackend() conversation = { "mapping": { "user": { "message": { "author": {"role": "user"}, "content": {"parts": ["file-service://file-user-input"]}, } }, "tool": { "message": { "author": {"role": "tool"}, "create_time": 1, "metadata": { "async_task_type": "image_gen", "nested": {"asset": "file-service://file-second"}, }, "content": { "content_type": "text", "parts": [ {"content_type": "image_asset_pointer", "asset_pointer": "file-service://file-first"}, "sediment://sed-first", ], }, } }, "assistant": { "message": { "author": {"role": "assistant"}, "create_time": 2, "metadata": {}, "content": { "parts": [ {"content_type": "image_asset_pointer", "asset_pointer": "file-service://file-third"} ] }, } }, } } records = backend._extract_image_tool_records(conversation) file_ids = [file_id for record in records for file_id in record["file_ids"]] sediment_ids = [sediment_id for record in records for sediment_id in record["sediment_ids"]] self.assertEqual(file_ids, ["file-first", "file-second", "file-third"]) self.assertEqual(sediment_ids, ["sed-first"]) def test_poll_waits_for_generated_asset_ids_to_settle(self) -> None: backend = FakeBackend([ _conversation(["file-one"]), _conversation(["file-one", "file-two"], ["sed-one"]), _conversation(["file-one", "file-two"], ["sed-one"]), ]) with ( mock.patch.dict(config.data, {"image_poll_initial_wait_secs": 0, "image_poll_interval_secs": 0.5}), mock.patch("services.openai_backend_api.time.sleep", lambda _seconds: None), ): file_ids, sediment_ids = backend._poll_image_results("conv-1", timeout_secs=10) self.assertEqual(file_ids, ["file-one", "file-two"]) self.assertEqual(sediment_ids, ["sed-one"]) self.assertEqual(backend.calls, 3) def test_resolver_uses_file_and_sediment_urls(self) -> None: backend = FakeBackend() backend.file_urls = {"file-one": "https://files.test/one.png"} backend.sediment_urls = { "sed-one": "https://attachments.test/one.png", "sed-two": "https://attachments.test/two.png", } urls = backend._resolve_image_urls("conv-1", ["file-one"], ["sed-one", "sed-two"]) self.assertEqual(urls, [ "https://files.test/one.png", "https://attachments.test/one.png", "https://attachments.test/two.png", ]) def test_resolver_keeps_stream_ids_when_poll_extension_fails(self) -> None: backend = FakeBackend() backend.file_urls = {"file-one": "https://files.test/one.png"} backend._get_conversation = mock.Mock(side_effect=RuntimeError("poll failed")) with mock.patch("services.openai_backend_api.time.sleep", lambda _seconds: None): urls = backend.resolve_conversation_image_urls("conv-1", ["file-one"], [], poll=True) self.assertEqual(urls, ["https://files.test/one.png"]) def test_responses_stream_emits_all_image_output_items(self) -> None: first = base64.b64encode(b"first").decode("ascii") second = base64.b64encode(b"second").decode("ascii") events = list(stream_image_response( [ImageOutput( kind="result", model="gpt-image-2", index=1, total=1, data=[{"b64_json": first}, {"b64_json": second}], )], "draw two options", "gpt-image-2", )) done_events = [event for event in events if event.get("type") == "response.output_item.done"] completed = next(event["response"] for event in events if event.get("type") == "response.completed") self.assertEqual([event["output_index"] for event in done_events], [0, 1]) self.assertEqual([item["result"] for item in completed["output"]], [first, second]) if __name__ == "__main__": unittest.main()