gmw / tests /test_modular_sync.py
nanoppa's picture
Upload 26 files
8de2e20 verified
Raw History Blame Contribute Delete
16.5 kB
import http.client
import base64
import json
import threading
import unittest
from unittest import mock
from urllib.parse import parse_qs
from gemini_web2api.config import CONFIG, DEFAULT_CONFIG
from gemini_web2api.gemini import _build_payload
from gemini_web2api.server import GeminiHandler, ThreadedServer
from gemini_web2api.tools import google_contents_to_prompt, messages_to_prompt
def _decode_payload(payload):
outer = json.loads(parse_qs(payload)["f.req"][0])
return json.loads(outer[1])
def _decode_sse(body):
events = []
for block in body.strip().split("\n\n"):
lines = block.splitlines()
event_type = next(
(line[len("event: "):] for line in lines if line.startswith("event: ")),
None,
)
data = next(
(line[len("data: "):] for line in lines if line.startswith("data: ")),
None,
)
if event_type and data:
events.append((event_type, json.loads(data)))
return events
class PayloadPersistenceTests(unittest.TestCase):
def setUp(self):
self.original_config = dict(CONFIG)
def tearDown(self):
CONFIG.clear()
CONFIG.update(self.original_config)
def test_temporary_chats_default_to_disabled(self):
self.assertIs(DEFAULT_CONFIG["temporary_chats"], False)
def test_persistent_chat_payload(self):
CONFIG["temporary_chats"] = False
inner = _decode_payload(_build_payload("hello", 1, 4))
self.assertEqual(inner[41], [2])
self.assertIsNone(inner[45])
def test_temporary_chat_payload(self):
CONFIG["temporary_chats"] = True
inner = _decode_payload(_build_payload("hello", 1, 4))
self.assertEqual(inner[41], [1])
self.assertEqual(inner[45], 1)
def test_payload_includes_uploaded_image_refs(self):
inner = _decode_payload(_build_payload("describe", 1, 4, ["/uploaded/image-ref"]))
self.assertEqual(inner[0][0], "describe")
self.assertEqual(inner[0][3], [[None, None, "/uploaded/image-ref"]])
class MessageParsingTests(unittest.TestCase):
def test_messages_to_prompt_extracts_openai_image_url_data_url(self):
image_data = base64.b64encode(b"fake png").decode()
prompt, images = messages_to_prompt([{
"role": "user",
"content": [
{"type": "text", "text": "Describe"},
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_data}"}},
],
}])
self.assertEqual(prompt, "Describe [Image attached]")
self.assertEqual(images, [(b"fake png", "image/png")])
def test_messages_to_prompt_extracts_responses_input_image_url(self):
prompt, images = messages_to_prompt([{
"role": "user",
"content": [
{"type": "input_text", "text": "Describe"},
{"type": "input_image", "image_url": "https://example.com/image.png"},
],
}])
self.assertEqual(prompt, "Describe [Image attached]")
self.assertEqual(images, [("https://example.com/image.png", "image/png")])
def test_messages_to_prompt_ignores_malformed_image_data_url(self):
prompt, images = messages_to_prompt([{
"role": "user",
"content": [
{"type": "text", "text": "Describe"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,%%%"}},
],
}])
self.assertEqual(prompt, "Describe")
self.assertEqual(images, [])
def test_google_contents_to_prompt_extracts_inline_image_data(self):
image_data = base64.b64encode(b"fake png").decode()
prompt, images = google_contents_to_prompt({
"contents": [{
"role": "user",
"parts": [
{"text": "Describe"},
{"inlineData": {"mimeType": "image/png", "data": image_data}},
],
}],
})
self.assertEqual(prompt, "Describe\n[Image attached]")
self.assertEqual(images, [(b"fake png", "image/png")])
def test_google_contents_to_prompt_ignores_malformed_inline_image_data(self):
prompt, images = google_contents_to_prompt({
"contents": [{
"role": "user",
"parts": [
{"text": "Describe"},
{"inlineData": {"mimeType": "image/png", "data": "%%%"}},
],
}],
})
self.assertEqual(prompt, "Describe")
self.assertEqual(images, [])
class StreamingEndpointTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.server = ThreadedServer(("127.0.0.1", 0), GeminiHandler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
cls.port = cls.server.server_address[1]
@classmethod
def tearDownClass(cls):
cls.server.shutdown()
cls.server.server_close()
cls.thread.join(timeout=5)
def setUp(self):
self.original_config = dict(CONFIG)
CONFIG["api_keys"] = []
CONFIG["log_requests"] = False
def tearDown(self):
CONFIG.clear()
CONFIG.update(self.original_config)
def post_json(self, path, payload):
connection = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5)
connection.request(
"POST",
path,
body=json.dumps(payload),
headers={"Content-Type": "application/json"},
)
response = connection.getresponse()
body = response.read().decode()
headers = dict(response.getheaders())
connection.close()
return response.status, headers, body
def post_chunked_json(self, path, payload):
connection = http.client.HTTPConnection("127.0.0.1", self.port, timeout=5)
connection.request(
"POST",
path,
body=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
encode_chunked=True,
)
response = connection.getresponse()
body = response.read().decode()
headers = dict(response.getheaders())
connection.close()
return response.status, headers, body
@mock.patch("gemini_web2api.server.generate_stream")
def test_chat_stream_starts_with_assistant_role(self, generate_stream):
generate_stream.return_value = iter(["hel", "lo"])
status, headers, body = self.post_json(
"/v1/chat/completions",
{
"model": "gemini-3.6-flash",
"messages": [{"role": "user", "content": "hello"}],
"stream": True,
},
)
self.assertEqual(status, 200)
self.assertEqual(headers["Content-Type"], "text/event-stream")
chunks = [
json.loads(line[len("data: "):])
for line in body.splitlines()
if line.startswith("data: {")
]
self.assertEqual(chunks[0]["choices"][0]["delta"], {"role": "assistant"})
self.assertEqual(chunks[1]["choices"][0]["delta"], {"content": "hel"})
self.assertEqual(chunks[2]["choices"][0]["delta"], {"content": "lo"})
self.assertTrue(body.endswith("data: [DONE]\n\n"))
@mock.patch("gemini_web2api.server.generate", return_value="chunked ok")
def test_chat_accepts_chunked_body(self, _generate):
status, _, body = self.post_chunked_json(
"/v1/chat/completions",
{
"model": "gemini-3.6-flash",
"messages": [{"role": "user", "content": "hello"}],
},
)
self.assertEqual(status, 200)
self.assertEqual(json.loads(body)["choices"][0]["message"]["content"], "chunked ok")
@mock.patch("gemini_web2api.server.upload_image", return_value="/uploaded/image-ref")
@mock.patch("gemini_web2api.server.generate", return_value="looks good")
def test_chat_accepts_openai_image_url_data_url(self, generate, upload_image):
image_data = base64.b64encode(b"fake png").decode()
status, _, body = self.post_json(
"/v1/chat/completions",
{
"model": "gemini-3.6-flash",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "Describe this image"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_data}"
},
},
],
}],
},
)
self.assertEqual(status, 200)
upload_image.assert_called_once_with(b"fake png", "image.png", "image/png")
self.assertEqual(generate.call_args.args[3], ["/uploaded/image-ref"])
self.assertIn("[Image attached]", generate.call_args.args[0])
self.assertEqual(json.loads(body)["choices"][0]["message"]["content"], "looks good")
@mock.patch("gemini_web2api.server.fetch_image_bytes", return_value=b"\xff\xd8\xffremote jpeg")
@mock.patch("gemini_web2api.server.upload_image", return_value="/uploaded/remote-ref")
@mock.patch("gemini_web2api.server.generate", return_value="remote ok")
def test_responses_accepts_input_image_url(self, generate, upload_image, fetch_image_bytes):
status, _, _ = self.post_json(
"/v1/responses",
{
"model": "gemini-3.6-flash",
"input": [{
"role": "user",
"content": [
{"type": "input_text", "text": "What is shown?"},
{
"type": "input_image",
"image_url": "https://example.com/image.jpg",
},
],
}],
},
)
self.assertEqual(status, 200)
fetch_image_bytes.assert_called_once_with("https://example.com/image.jpg")
upload_image.assert_called_once_with(b"\xff\xd8\xffremote jpeg", "image.png", "image/jpeg")
self.assertEqual(generate.call_args.args[3], ["/uploaded/remote-ref"])
self.assertIn("[Image attached]", generate.call_args.args[0])
@mock.patch("gemini_web2api.server.upload_image", return_value="/uploaded/image-ref")
@mock.patch("gemini_web2api.server.generate", return_value="top-level image ok")
def test_responses_accepts_top_level_input_image(self, generate, upload_image):
image_data = base64.b64encode(b"fake png").decode()
status, _, _ = self.post_json(
"/v1/responses",
{
"model": "gemini-3.6-flash",
"input": [
{"type": "input_text", "text": "What is shown?"},
{
"type": "input_image",
"image_url": f"data:image/png;base64,{image_data}",
},
],
},
)
self.assertEqual(status, 200)
upload_image.assert_called_once_with(b"fake png", "image.png", "image/png")
self.assertEqual(generate.call_args.args[3], ["/uploaded/image-ref"])
self.assertIn("What is shown?", generate.call_args.args[0])
self.assertIn("[Image attached]", generate.call_args.args[0])
@mock.patch("gemini_web2api.server.upload_image", side_effect=RuntimeError("upload denied"))
def test_google_image_upload_failure_returns_502(self, _upload_image):
image_data = base64.b64encode(b"fake png").decode()
status, _, body = self.post_json(
"/v1beta/models/gemini-3.6-flash:generateContent",
{
"contents": [{
"role": "user",
"parts": [{
"inlineData": {
"mimeType": "image/png",
"data": image_data,
},
}],
}],
},
)
self.assertEqual(status, 502)
self.assertIn("image upload failed: upload denied", json.loads(body)["error"]["message"])
@mock.patch("gemini_web2api.server.generate_stream", return_value=iter(["streamed"]))
def test_google_stream_generate_content_uses_sse(self, _generate_stream):
status, headers, body = self.post_json(
"/v1beta/models/gemini-3.6-flash:streamGenerateContent",
{
"contents": [{
"role": "user",
"parts": [{"text": "Stream this"}],
}],
},
)
self.assertEqual(status, 200)
self.assertEqual(headers["Content-Type"], "text/event-stream")
self.assertIn('"text": "streamed"', body)
@mock.patch("gemini_web2api.server.generate", return_value="hello")
def test_responses_text_stream_has_complete_event_sequence(self, _generate):
status, headers, body = self.post_json(
"/v1/responses",
{
"model": "gemini-3.6-flash",
"input": "hello",
"stream": True,
},
)
self.assertEqual(status, 200)
self.assertEqual(headers["Content-Type"], "text/event-stream")
events = _decode_sse(body)
self.assertEqual(
[event_type for event_type, _ in events],
[
"response.created",
"response.in_progress",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
],
)
self.assertEqual(
[event["sequence_number"] for _, event in events],
list(range(1, len(events) + 1)),
)
self.assertEqual(events[4][1]["delta"], "hello")
self.assertEqual(events[-1][1]["response"]["status"], "completed")
self.assertEqual(events[-1][1]["response"]["output"][0]["content"][0]["text"], "hello")
@mock.patch("gemini_web2api.server.parse_tool_calls")
@mock.patch("gemini_web2api.server.generate", return_value="tool output")
def test_responses_function_call_stream_has_complete_event_sequence(
self, _generate, parse_tool_calls
):
parse_tool_calls.return_value = (
"",
[
{
"id": "call_test",
"type": "function",
"function": {"name": "get_weather", "arguments": '{"city":"Shanghai"}'},
}
],
)
status, _, body = self.post_json(
"/v1/responses",
{
"model": "gemini-3.6-flash",
"input": "weather",
"tools": [
{
"type": "function",
"name": "get_weather",
"description": "Get weather",
"parameters": {"type": "object"},
}
],
"stream": True,
},
)
self.assertEqual(status, 200)
events = _decode_sse(body)
self.assertEqual(
[event_type for event_type, _ in events],
[
"response.created",
"response.in_progress",
"response.output_item.added",
"response.function_call_arguments.delta",
"response.function_call_arguments.done",
"response.output_item.done",
"response.completed",
],
)
self.assertEqual(
[event["sequence_number"] for _, event in events],
list(range(1, len(events) + 1)),
)
self.assertEqual(events[2][1]["output_index"], 0)
self.assertEqual(events[3][1]["delta"], '{"city":"Shanghai"}')
self.assertEqual(events[4][1]["arguments"], '{"city":"Shanghai"}')
self.assertEqual(events[-1][1]["response"]["output"][0]["name"], "get_weather")
if __name__ == "__main__":
unittest.main()