import base64 import importlib import sys import types from types import SimpleNamespace import pytest from gateway.config import Platform, PlatformConfig from gateway.platforms.base import BasePlatformAdapter, MessageEvent, MessageType, SendResult from gateway.session import SessionSource _ONE_BY_ONE_PNG = base64.b64decode( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO6L2ioAAAAASUVORK5CYII=" ) class CaptureAdapter(BasePlatformAdapter): def __init__(self): super().__init__(PlatformConfig(enabled=True, token="***"), Platform.TELEGRAM) self.sent = [] self.typing = [] async def connect(self) -> bool: return True async def disconnect(self) -> None: return None async def send(self, chat_id, content, reply_to=None, metadata=None) -> SendResult: self.sent.append( { "chat_id": chat_id, "content": content, "reply_to": reply_to, "metadata": metadata, } ) return SendResult(success=True, message_id="sent-1") async def send_typing(self, chat_id, metadata=None) -> None: self.typing.append({"chat_id": chat_id, "metadata": metadata}) async def stop_typing(self, chat_id) -> None: return None async def get_chat_info(self, chat_id: str): return {"id": chat_id} class CaptureQueuedNativeImageAgent: calls = [] def __init__(self, **kwargs): self.tools = [] self.tool_progress_callback = kwargs.get("tool_progress_callback") def run_conversation(self, message, conversation_history=None, task_id=None): type(self).calls.append(message) return { "final_response": f"done-{len(type(self).calls)}", "messages": [], "api_calls": 1, } def _make_runner(adapter): gateway_run = importlib.import_module("gateway.run") runner = object.__new__(gateway_run.GatewayRunner) runner.adapters = {adapter.platform: adapter} runner._voice_mode = {} runner._prefill_messages = [] runner._ephemeral_system_prompt = "" runner._reasoning_config = None runner._provider_routing = {} runner._fallback_model = None runner._session_db = None runner._running_agents = {} runner._session_run_generation = {} runner.hooks = SimpleNamespace(loaded_hooks=False) runner.config = SimpleNamespace( thread_sessions_per_user=False, group_sessions_per_user=False, stt_enabled=False, ) runner._model = "openai/gpt-4.1-mini" runner._base_url = None runner._decide_image_input_mode = lambda **_kw: "native" return runner @pytest.mark.asyncio async def test_queued_followup_uses_pending_event_session_key_for_native_images(monkeypatch, tmp_path): CaptureQueuedNativeImageAgent.calls = [] fake_dotenv = types.ModuleType("dotenv") fake_dotenv.load_dotenv = lambda *args, **kwargs: None monkeypatch.setitem(sys.modules, "dotenv", fake_dotenv) fake_run_agent = types.ModuleType("run_agent") fake_run_agent.AIAgent = CaptureQueuedNativeImageAgent monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent) gateway_run = importlib.import_module("gateway.run") monkeypatch.setattr(gateway_run, "_hermes_home", tmp_path) monkeypatch.setattr(gateway_run, "_resolve_runtime_agent_kwargs", lambda: {"api_key": "***"}) adapter = CaptureAdapter() runner = _make_runner(adapter) image_path = tmp_path / "queued-image.png" image_path.write_bytes(_ONE_BY_ONE_PNG) source = SessionSource( platform=Platform.TELEGRAM, chat_id="-1001", chat_type="group", ) pending_source = SessionSource( platform=Platform.TELEGRAM, chat_id="-1001", chat_type="group", thread_id="17585", ) adapter._pending_messages["agent:main:telegram:group:-1001"] = MessageEvent( text="describe this", message_type=MessageType.PHOTO, source=pending_source, media_urls=[str(image_path)], media_types=["image/png"], message_id="queued-1", ) result = await runner._run_agent( message="hello", context_prompt="", history=[], source=source, session_id="sess-native-image-followup", session_key="agent:main:telegram:group:-1001", ) assert result["final_response"] == "done-2" assert len(CaptureQueuedNativeImageAgent.calls) == 2 queued_message = CaptureQueuedNativeImageAgent.calls[1] assert isinstance(queued_message, list) assert queued_message[0]["type"] == "text" assert queued_message[0]["text"].startswith("describe this") assert any(part.get("type") == "image_url" for part in queued_message)