diff --git a/cron/scheduler.py b/cron/scheduler.py index c2f52be0e..606a9ba7b 100644 --- a/cron/scheduler.py +++ b/cron/scheduler.py @@ -237,6 +237,10 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> None: else: delivery_content = content + # Extract MEDIA: tags so attachments are forwarded as files, not raw text + from gateway.platforms.base import BasePlatformAdapter + media_files, cleaned_delivery_content = BasePlatformAdapter.extract_media(delivery_content) + # Prefer the live adapter when the gateway is running — this supports E2EE # rooms (e.g. Matrix) where the standalone HTTP path cannot encrypt. runtime_adapter = (adapters or {}).get(platform) @@ -264,7 +268,7 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> None: ) # Standalone path: run the async send in a fresh event loop (safe from any thread) - coro = _send_to_platform(platform, pconfig, chat_id, delivery_content, thread_id=thread_id) + coro = _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files) try: result = asyncio.run(coro) except RuntimeError: @@ -275,7 +279,7 @@ def _deliver_result(job: dict, content: str, adapters=None, loop=None) -> None: coro.close() import concurrent.futures with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: - future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, delivery_content, thread_id=thread_id)) + future = pool.submit(asyncio.run, _send_to_platform(platform, pconfig, chat_id, cleaned_delivery_content, thread_id=thread_id, media_files=media_files)) result = future.result(timeout=30) except Exception as e: logger.error("Job '%s': delivery to %s:%s failed: %s", job["id"], platform_name, chat_id, e) diff --git a/tests/cron/test_scheduler.py b/tests/cron/test_scheduler.py index 00531d3c1..33f265de3 100644 --- a/tests/cron/test_scheduler.py +++ b/tests/cron/test_scheduler.py @@ -250,6 +250,33 @@ class TestDeliverResultWrapping: assert "Cronjob Response" not in sent_content assert "The agent cannot see" not in sent_content + def test_delivery_extracts_media_tags_before_send(self): + """Cron delivery should pass MEDIA attachments separately to the send helper.""" + from gateway.config import Platform + + pconfig = MagicMock() + pconfig.enabled = True + mock_cfg = MagicMock() + mock_cfg.platforms = {Platform.TELEGRAM: pconfig} + + with patch("gateway.config.load_gateway_config", return_value=mock_cfg), \ + patch("tools.send_message_tool._send_to_platform", new=AsyncMock(return_value={"success": True})) as send_mock, \ + patch("cron.scheduler.load_config", return_value={"cron": {"wrap_response": False}}): + job = { + "id": "voice-job", + "deliver": "origin", + "origin": {"platform": "telegram", "chat_id": "123"}, + } + _deliver_result(job, "Title\nMEDIA:/tmp/test-voice.ogg") + + send_mock.assert_called_once() + args, kwargs = send_mock.call_args + # Text content should have MEDIA: tag stripped + assert "MEDIA:" not in args[3] + assert "Title" in args[3] + # Media files should be forwarded separately + assert kwargs["media_files"] == [("/tmp/test-voice.ogg", False)] + def test_no_mirror_to_session_call(self): """Cron deliveries should NOT mirror into the gateway session.""" from gateway.config import Platform