From eb048772f6644e3e4d3d992f7c8c555c61df8564 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Mon, 15 Jun 2026 09:23:46 +0800 Subject: [PATCH] fix(simplex): use structured /_send for standalone DM text sends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The _standalone_send() function (used by the send_message tool for proactive/scheduled sends) has the same bare `@ text` bug that send() had before PR #44444. SimpleX's `@` syntax resolves x as a display name, not a contactId — the daemon silently drops messages when it cannot find a contact named "6". Use the structured `/_send @ json [...]` form, matching what send_image, send_document, and the send() fix already use. Fixes #46265 --- plugins/platforms/simplex/adapter.py | 6 ++- tests/gateway/test_simplex_plugin.py | 58 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/plugins/platforms/simplex/adapter.py b/plugins/platforms/simplex/adapter.py index 4046ce4910ad9..2f9dd4e493229 100644 --- a/plugins/platforms/simplex/adapter.py +++ b/plugins/platforms/simplex/adapter.py @@ -1269,8 +1269,10 @@ async def _standalone_send( ) cmd_str = f"/_send #{group_id} json {composed}" else: - # Direct contacts are addressed by display name without brackets. - cmd_str = f"@{chat_id} {message}" + composed = json.dumps( + [{"msgContent": {"type": "text", "text": message}}] + ) + cmd_str = f"/_send @{chat_id} json {composed}" payload = { "corrId": f"{_CORR_PREFIX}snd-{int(time.time() * 1000)}", diff --git a/tests/gateway/test_simplex_plugin.py b/tests/gateway/test_simplex_plugin.py index 87808f81f1bd1..1a88d56513017 100644 --- a/tests/gateway/test_simplex_plugin.py +++ b/tests/gateway/test_simplex_plugin.py @@ -298,6 +298,64 @@ async def test_standalone_send_missing_websockets(monkeypatch): sys.modules["websockets"] = saved_websockets +@pytest.mark.asyncio +async def test_standalone_send_defaults_to_local_daemon(monkeypatch): + monkeypatch.delenv("SIMPLEX_WS_URL", raising=False) + pconfig = MagicMock() + pconfig.extra = {} + + sent_payloads = [] + + class DummyWs: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc, tb): + return None + + async def send(self, payload): + sent_payloads.append(json.loads(payload)) + + def fake_connect(url, **kwargs): + assert url == "ws://127.0.0.1:5225" + assert kwargs["open_timeout"] == 10 + assert kwargs["close_timeout"] == 5 + return DummyWs() + + import websockets + monkeypatch.setattr(websockets, "connect", fake_connect) + + result = await _standalone_send(pconfig, "contact-42", "hi") + assert result == {"success": True, "platform": "simplex", "chat_id": "contact-42"} + assert sent_payloads[0]["cmd"].startswith("/_send @contact-42 json ") + msg_content = json.loads( + sent_payloads[0]["cmd"].split(" json ", 1)[1] + )[0]["msgContent"] + assert msg_content == {"type": "text", "text": "hi"} + + +@pytest.mark.asyncio +async def test_health_monitor_does_not_reconnect_quiet_healthy_ws(monkeypatch): + from gateway.config import PlatformConfig + cfg = PlatformConfig(enabled=True, extra={"ws_url": "ws://localhost:5225"}) + adapter = SimplexAdapter(cfg) + adapter._running = True + adapter._last_ws_activity = 0 + adapter._ws = AsyncMock() + + monkeypatch.setattr(_simplex, "HEALTH_CHECK_INTERVAL", 0.01) + monkeypatch.setattr(_simplex, "HEALTH_CHECK_STALE_THRESHOLD", 0.01) + + task = asyncio.create_task(adapter._health_monitor()) + await asyncio.sleep(0.03) + adapter._running = False + await asyncio.wait_for(task, timeout=1) + + adapter._ws.close.assert_not_called() + + + + # --------------------------------------------------------------------------- # 10. register() — plugin-side metadata # ---------------------------------------------------------------------------