"""Native deepseek-flash multimodal chat tests; no real API calls.""" import base64 from pathlib import Path import sys import tempfile import unittest from unittest.mock import AsyncMock, patch BASE = Path(__file__).resolve().parents[1] sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)] import agent_loop from agent import ToolResult from chat import engine class MultimodalContentTests(unittest.IsolatedAsyncioTestCase): async def test_local_image_and_task_share_one_user_content(self): payload = b"\x89PNG\r\n\x1a\nsmall-test-image" with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "task.png" path.write_bytes(payload) content = await engine._build_user_content( "总结图片,并读出其中要做的任务", [{"url": str(path), "file_size": str(len(payload))}], ) self.assertIsInstance(content, list) self.assertEqual(content[0]["type"], "image_url") self.assertEqual(content[1]["type"], "text") self.assertEqual(content[1]["text"], "总结图片,并读出其中要做的任务") data_url = content[0]["image_url"]["url"] self.assertTrue(data_url.startswith("data:image/png;base64,")) self.assertEqual(base64.b64decode(data_url.split(",", 1)[1]), payload) async def test_text_only_content_is_unchanged(self): content = await engine._build_user_content("普通聊天", None) self.assertEqual(content, "普通聊天") async def test_main_call_passes_same_multimodal_content_to_official_model(self): parts = [ {"type": "image_url", "image_url": {"url": "data:image/png;base64,eA=="}}, {"type": "text", "text": "读图"}, ] official = AsyncMock(return_value="ok") with patch.object(engine, "_modelscope_available", return_value=False), \ patch.object(engine, "_build_user_content", new=AsyncMock(return_value=parts)), \ patch.object(engine, "_call_deepseek_official", new=official): result = await engine.call_deepseek("system", "读图", image_infos=[{"url": "x"}]) self.assertEqual(result, "ok") self.assertIs(official.await_args.args[1], parts) self.assertEqual(engine.MODEL_NAME, "deepseek-flash") async def test_private_pending_flow_forwards_original_images_without_description_step(self): images = [{"url": "/tmp/original.png"}] reply = AsyncMock() engine._user_pending_messages[123] = ["按图执行"] engine._user_pending_images[123] = images try: with patch.object(engine, "_call_model_and_reply", new=reply): await engine._process_pending_messages(123, 0) finally: engine._user_pending_messages.pop(123, None) engine._user_pending_images.pop(123, None) engine._user_pending_tasks.pop(123, None) self.assertEqual(reply.await_args.args[2], "按图执行") self.assertIs(reply.await_args.kwargs["image_infos"], images) class FakeStore: def start(self, uid, conversation): return "task" def record(self, *args): pass def finish(self, *args): pass class FakeExecutor: async def execute(self, name, args, ctx): return ToolResult(True, "ok", "done") class MultiRoundEngine: def __init__(self): self.image_kwargs = [] self.reasoning_kwargs = [] async def call_deepseek(self, *args, **kwargs): self.image_kwargs.append(kwargs.get("image_infos")) self.reasoning_kwargs.append(kwargs.get("reasoning")) return {"message": [{"content": "完成"}], "tools": []} def parse_json_response(self, raw): return raw class AgentMultimodalTests(unittest.IsolatedAsyncioTestCase): async def test_tool_followup_keeps_original_images(self): images = [{"url": "/tmp/original.png"}] model = MultiRoundEngine() initial = {"message": [], "tools": [{"name": "mail", "args": {"action": "list"}}]} channel = type("Channel", (), {"name": "tg", "is_admin": lambda self, uid: True})() with patch.object(agent_loop, "AgentTaskStore", FakeStore), \ patch.object(agent_loop, "ToolExecutor", FakeExecutor): result = await agent_loop.run_agent_loop( model, initial, "system", "按图执行", channel, "private", 1, 2, image_infos=images, ) self.assertEqual(result["message"][0]["content"], "完成") self.assertEqual(model.image_kwargs, [images]) self.assertEqual(model.reasoning_kwargs, [True]) if __name__ == "__main__": unittest.main()