186 lines
7.2 KiB
Python
186 lines
7.2 KiB
Python
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,
|
|
"image_check_before_hit_enabled": True,
|
|
"image_settle_enabled": True,
|
|
}),
|
|
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()
|