|
5 | 5 | satisfies the runtime contract — most importantly that a kit-built schema, run |
6 | 6 | through the registry's real OpenAI-tool conversion, yields a function whose |
7 | 7 | ``parameters`` are populated (the empty-``{}`` failure mode this kit exists to |
8 | | -prevent). |
| 8 | +prevent). The host-tool contract also sends the exact generated-image payload |
| 9 | +through Hermes' real target parser, media extractor, and Telegram formatter, |
| 10 | +mocking only the final Bot API client. |
9 | 11 |
|
10 | 12 | The whole module is skipped when hermes-agent is not importable, so the suite |
11 | 13 | stays green standalone and in public CI. Point it at a checkout with |
|
22 | 24 | import types |
23 | 25 | import unittest |
24 | 26 | from pathlib import Path |
25 | | -from unittest.mock import patch |
| 27 | +from unittest.mock import AsyncMock, Mock, patch |
26 | 28 |
|
27 | 29 | import hermes_plugin_kit as hpk |
28 | 30 |
|
@@ -209,24 +211,120 @@ def test_lifecycle_calls_bind_to_real_plugincontext_signatures(self) -> None: |
209 | 211 | description="Probe", |
210 | 212 | ) |
211 | 213 |
|
212 | | - def test_host_tool_invocation_uses_real_non_registry_handler(self) -> None: |
213 | | - from tools import send_message_tool # type: ignore |
214 | | - |
215 | | - expected = '{"success": true, "message_id": "contract-probe"}' |
216 | | - args = { |
217 | | - "action": "send", |
218 | | - "target": "telegram:8670382527", |
219 | | - "message": "MEDIA:/opt/data/avatars/generated/contract-probe.png", |
220 | | - } |
221 | | - with patch.object( |
222 | | - send_message_tool, |
223 | | - "send_message_tool", |
224 | | - return_value=expected, |
225 | | - ) as handler: |
226 | | - result = hpk.invoke_host_tool("send_message", args) |
227 | | - |
228 | | - self.assertEqual(result, expected) |
229 | | - handler.assert_called_once_with(args) |
| 214 | + def test_host_tool_invocation_reaches_real_telegram_photo_contract(self) -> None: |
| 215 | + """Exercise Hermes parsing and Telegram formatting without network I/O.""" |
| 216 | + import asyncio |
| 217 | + from tempfile import TemporaryDirectory |
| 218 | + |
| 219 | + from gateway import config as gateway_config # type: ignore |
| 220 | + from gateway.config import Platform # type: ignore |
| 221 | + from tools import interrupt as interrupt_module # type: ignore |
| 222 | + |
| 223 | + telegram_config = types.SimpleNamespace( |
| 224 | + enabled=True, |
| 225 | + token="contract-token", |
| 226 | + extra={}, |
| 227 | + ) |
| 228 | + config = types.SimpleNamespace( |
| 229 | + platforms={Platform.TELEGRAM: telegram_config}, |
| 230 | + get_home_channel=lambda _platform: None, |
| 231 | + ) |
| 232 | + |
| 233 | + bot = types.SimpleNamespace( |
| 234 | + send_message=AsyncMock(), |
| 235 | + send_photo=AsyncMock( |
| 236 | + return_value=types.SimpleNamespace(message_id=42) |
| 237 | + ), |
| 238 | + send_video=AsyncMock(), |
| 239 | + send_voice=AsyncMock(), |
| 240 | + send_audio=AsyncMock(), |
| 241 | + send_document=AsyncMock(), |
| 242 | + ) |
| 243 | + bot_factory = Mock(return_value=bot) |
| 244 | + |
| 245 | + telegram_module = types.ModuleType("telegram") |
| 246 | + telegram_module.Bot = bot_factory |
| 247 | + telegram_constants = types.ModuleType("telegram.constants") |
| 248 | + telegram_constants.ParseMode = types.SimpleNamespace( |
| 249 | + HTML="HTML", |
| 250 | + MARKDOWN_V2="MarkdownV2", |
| 251 | + ) |
| 252 | + telegram_module.constants = telegram_constants |
| 253 | + |
| 254 | + class TelegramAdapter: |
| 255 | + MAX_MESSAGE_LENGTH = 4096 |
| 256 | + |
| 257 | + @staticmethod |
| 258 | + def format_message(message: str) -> str: |
| 259 | + return message |
| 260 | + |
| 261 | + telegram_adapter = types.ModuleType("plugins.platforms.telegram.adapter") |
| 262 | + telegram_adapter.TelegramAdapter = TelegramAdapter |
| 263 | + telegram_adapter.register = Mock() |
| 264 | + |
| 265 | + model_tools = types.ModuleType("model_tools") |
| 266 | + model_tools._run_async = asyncio.run |
| 267 | + mirror = types.ModuleType("gateway.mirror") |
| 268 | + mirror.mirror_to_session = Mock(return_value=False) |
| 269 | + session_context = types.ModuleType("gateway.session_context") |
| 270 | + session_context.get_session_env = Mock(return_value="") |
| 271 | + |
| 272 | + with TemporaryDirectory() as tmp: |
| 273 | + image_path = Path(tmp) / "avatars" / "generated" / "contract-probe.png" |
| 274 | + image_path.parent.mkdir(parents=True) |
| 275 | + image_path.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 32) |
| 276 | + args = { |
| 277 | + "action": "send", |
| 278 | + "target": "telegram:8670382527", |
| 279 | + "message": f"MEDIA:{image_path}", |
| 280 | + } |
| 281 | + |
| 282 | + with ( |
| 283 | + patch.dict( |
| 284 | + sys.modules, |
| 285 | + { |
| 286 | + "telegram": telegram_module, |
| 287 | + "telegram.constants": telegram_constants, |
| 288 | + "plugins.platforms.telegram.adapter": telegram_adapter, |
| 289 | + "model_tools": model_tools, |
| 290 | + "gateway.mirror": mirror, |
| 291 | + "gateway.session_context": session_context, |
| 292 | + }, |
| 293 | + ), |
| 294 | + patch.dict( |
| 295 | + os.environ, |
| 296 | + { |
| 297 | + "HERMES_MEDIA_DELIVERY_STRICT": "0", |
| 298 | + "TELEGRAM_PROXY": "", |
| 299 | + }, |
| 300 | + clear=False, |
| 301 | + ), |
| 302 | + patch.object( |
| 303 | + gateway_config, |
| 304 | + "load_gateway_config", |
| 305 | + return_value=config, |
| 306 | + ), |
| 307 | + patch.object( |
| 308 | + interrupt_module, |
| 309 | + "is_interrupted", |
| 310 | + return_value=False, |
| 311 | + ), |
| 312 | + ): |
| 313 | + result = json.loads(hpk.invoke_host_tool("send_message", args)) |
| 314 | + |
| 315 | + self.assertTrue(result.get("success"), result) |
| 316 | + self.assertEqual(result["platform"], "telegram") |
| 317 | + self.assertEqual(result["chat_id"], "8670382527") |
| 318 | + self.assertEqual(result["message_id"], "42") |
| 319 | + bot_factory.assert_called_once_with(token="contract-token") |
| 320 | + bot.send_message.assert_not_awaited() |
| 321 | + bot.send_photo.assert_awaited_once() |
| 322 | + photo_call = bot.send_photo.await_args |
| 323 | + self.assertEqual(photo_call.kwargs["chat_id"], 8670382527) |
| 324 | + self.assertEqual( |
| 325 | + photo_call.kwargs["photo"].name, |
| 326 | + str(image_path.resolve()), |
| 327 | + ) |
230 | 328 |
|
231 | 329 |
|
232 | 330 | if __name__ == "__main__": |
|
0 commit comments