From ebda1fbf8efc1d46357cfb1b5bc2828be3940320 Mon Sep 17 00:00:00 2001 From: Riyaaz Date: Thu, 30 Jul 2026 15:44:06 -0400 Subject: [PATCH] fix(buzz): deliver local images through native upload --- plugins/platforms/buzz/adapter.py | 44 ++++++++++++++ tests/gateway/test_buzz_adapter.py | 96 ++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/plugins/platforms/buzz/adapter.py b/plugins/platforms/buzz/adapter.py index 99569c6266..47e188749f 100644 --- a/plugins/platforms/buzz/adapter.py +++ b/plugins/platforms/buzz/adapter.py @@ -1494,6 +1494,50 @@ class BuzzAdapter(BasePlatformAdapter): text = f"{caption}\n{image_url}" if caption else image_url return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata) + async def send_image_file( + self, + chat_id: str, + image_path: str, + caption: Optional[str] = None, + reply_to: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> SendResult: + local = Path(image_path).expanduser() + if local.is_file(): + args = [ + "messages", "send", + "--channel", str(chat_id), + "--file", str(local), + "--content", "-", + ] + reply_target = reply_to or (metadata or {}).get("thread_id") + if reply_target: + args += ["--reply-to", str(reply_target)] + code, out, err = await self._run_cli(args, input_text=caption or "") + if code != 0: + return SendResult(success=False, error=_cli_error_message(err, code), retryable=code == 2) + try: + data = json.loads(out or "{}") + except ValueError: + data = {} + event_id = data.get("event_id") + if event_id: + self._mark_seen(str(chat_id), str(event_id)) + return SendResult( + success=bool(data.get("accepted", True)), + message_id=str(event_id) if event_id else None, + raw_response=data, + ) + return await super().send_image_file( + chat_id=chat_id, + image_path=image_path, + caption=caption, + reply_to=reply_to, + metadata=metadata, + **kwargs, + ) + async def get_chat_info(self, chat_id: str) -> Dict[str, Any]: chat_id = str(chat_id) state = self._channel_state.get(chat_id) diff --git a/tests/gateway/test_buzz_adapter.py b/tests/gateway/test_buzz_adapter.py index bd4f875252..408c7ebb50 100644 --- a/tests/gateway/test_buzz_adapter.py +++ b/tests/gateway/test_buzz_adapter.py @@ -2340,6 +2340,102 @@ class TestBuzzAdapterSend: assert cli.calls[0][1] == "See @session:default/example." assert cli.calls[1][1] == "See @\u200bsession:default/example." + @pytest.mark.asyncio + async def test_send_image_file_existing_local_path_stays_native_upload_after_probe_flip( + self, tmp_path, monkeypatch + ): + img = tmp_path / "shot.png" + img.write_bytes(b"\x89PNG fake") + adapter = _make_adapter() + cli = _ScriptedCli() + cli.script("messages", "send", {"accepted": True, "event_id": "evt126b", "message": ""}) + adapter._run_cli = cli + + original_is_file = _buzz_mod.Path.is_file + probe_results = iter([True, False]) + + def sequential_is_file(path): + if path == img: + return next(probe_results, original_is_file(path)) + return original_is_file(path) + + monkeypatch.setattr(_buzz_mod.Path, "is_file", sequential_is_file) + + result = await adapter.send_image_file(CHANNEL, str(img), caption="screenshot") + + assert result.success is True + args, stdin_text = cli.calls[0] + assert args[:2] == ["messages", "send"] + assert args[args.index("--channel") + 1] == CHANNEL + assert args[args.index("--file") + 1] == str(img) + assert args[args.index("--content") + 1] == "-" + assert stdin_text == "screenshot" + + @pytest.mark.asyncio + async def test_send_image_file_uses_metadata_thread_id_when_reply_to_missing(self, tmp_path): + img = tmp_path / "reply-shot.png" + img.write_bytes(b"\x89PNG fake") + thread_id = "b" * 64 + adapter = _make_adapter() + cli = _ScriptedCli() + cli.script("messages", "send", {"accepted": True, "event_id": "evt126c", "message": ""}) + adapter._run_cli = cli + + result = await adapter.send_image_file( + CHANNEL, + str(img), + caption="screenshot", + metadata={"thread_id": thread_id}, + ) + + assert result.success is True + args, stdin_text = cli.calls[0] + assert args[:2] == ["messages", "send"] + assert args[args.index("--channel") + 1] == CHANNEL + assert args[args.index("--file") + 1] == str(img) + assert args[args.index("--content") + 1] == "-" + assert args[args.index("--reply-to") + 1] == thread_id + assert stdin_text == "screenshot" + + @pytest.mark.asyncio + async def test_send_image_file_missing_local_path_uses_base_fallback_notice(self, tmp_path): + missing = tmp_path / "missing.png" + adapter = _make_adapter() + cli = _ScriptedCli() + cli.script("messages", "send", {"accepted": True, "event_id": "evt127", "message": ""}) + adapter._run_cli = cli + + result = await adapter.send_image_file(CHANNEL, str(missing), caption="screenshot") + + assert result.success is True + args, stdin_text = cli.calls[0] + assert args[:2] == ["messages", "send"] + assert "--file" not in args + assert str(missing) not in args + assert stdin_text == "screenshot\n⚠️ Couldn't deliver the image attachment." + assert str(missing) not in stdin_text + + @pytest.mark.asyncio + async def test_send_multiple_images_file_url_uses_native_file_send(self, tmp_path): + img = tmp_path / "shot with spaces.png" + img.write_bytes(b"\x89PNG fake") + adapter = _make_adapter() + cli = _ScriptedCli() + cli.script("messages", "send", {"accepted": True, "event_id": "evt127", "message": ""}) + adapter._run_cli = cli + + await adapter.send_multiple_images(CHANNEL, [(img.as_uri(), "screenshot")]) + + assert len(cli.calls) == 1 + args, stdin_text = cli.calls[0] + assert args[:2] == ["messages", "send"] + assert args[args.index("--channel") + 1] == CHANNEL + assert args[args.index("--file") + 1] == str(img) + assert args[args.index("--content") + 1] == "-" + assert stdin_text == "screenshot" + assert "Couldn't deliver the image attachment." not in stdin_text + + # ── Thread anchoring ──────────────────────────────────────────────────────