diff --git a/backend/app/channels/feishu.py b/backend/app/channels/feishu.py index 32f71f1f6..6492d28e5 100644 --- a/backend/app/channels/feishu.py +++ b/backend/app/channels/feishu.py @@ -206,7 +206,9 @@ class FeishuChannel(Channel): await asyncio.sleep(delay) logger.error("[Feishu] send failed after %d attempts: %s", _max_retries, last_exc) - raise last_exc # type: ignore[misc] + if last_exc is None: + raise RuntimeError("Feishu send failed without an exception from any attempt") + raise last_exc async def send_file(self, msg: OutboundMessage, attachment: ResolvedAttachment) -> bool: if not self._api_client: diff --git a/backend/app/channels/slack.py b/backend/app/channels/slack.py index 760911083..32b42e5a8 100644 --- a/backend/app/channels/slack.py +++ b/backend/app/channels/slack.py @@ -126,7 +126,9 @@ class SlackChannel(Channel): ) except Exception: pass - raise last_exc # type: ignore[misc] + if last_exc is None: + raise RuntimeError("Slack send failed without an exception from any attempt") + raise last_exc async def send_file(self, msg: OutboundMessage, attachment: ResolvedAttachment) -> bool: if not self._web_client: diff --git a/backend/app/channels/telegram.py b/backend/app/channels/telegram.py index 97c50aa9f..9985fd43f 100644 --- a/backend/app/channels/telegram.py +++ b/backend/app/channels/telegram.py @@ -125,7 +125,9 @@ class TelegramChannel(Channel): await asyncio.sleep(delay) logger.error("[Telegram] send failed after %d attempts: %s", _max_retries, last_exc) - raise last_exc # type: ignore[misc] + if last_exc is None: + raise RuntimeError("Telegram send failed without an exception from any attempt") + raise last_exc async def send_file(self, msg: OutboundMessage, attachment: ResolvedAttachment) -> bool: if not self._application: diff --git a/backend/app/gateway/routers/threads.py b/backend/app/gateway/routers/threads.py index 562edfdb7..808604980 100644 --- a/backend/app/gateway/routers/threads.py +++ b/backend/app/gateway/routers/threads.py @@ -488,16 +488,19 @@ async def get_thread(thread_id: str, request: Request) -> ThreadResponse: "metadata": {k: v for k, v in ckpt_meta.items() if k not in ("created_at", "updated_at", "step", "source", "writes", "parents")}, } - status = _derive_thread_status(checkpoint_tuple) if checkpoint_tuple is not None else record.get("status", "idle") # type: ignore[union-attr] + if record is None: + raise HTTPException(status_code=404, detail=f"Thread {thread_id} not found") + + status = _derive_thread_status(checkpoint_tuple) if checkpoint_tuple is not None else record.get("status", "idle") checkpoint = getattr(checkpoint_tuple, "checkpoint", {}) or {} if checkpoint_tuple is not None else {} channel_values = checkpoint.get("channel_values", {}) return ThreadResponse( thread_id=thread_id, status=status, - created_at=str(record.get("created_at", "")), # type: ignore[union-attr] - updated_at=str(record.get("updated_at", "")), # type: ignore[union-attr] - metadata=record.get("metadata", {}), # type: ignore[union-attr] + created_at=str(record.get("created_at", "")), + updated_at=str(record.get("updated_at", "")), + metadata=record.get("metadata", {}), values=serialize_channel_values(channel_values), ) diff --git a/backend/tests/test_channels.py b/backend/tests/test_channels.py index c48137c76..fb58bcfb4 100644 --- a/backend/tests/test_channels.py +++ b/backend/tests/test_channels.py @@ -1854,6 +1854,20 @@ class TestSlackSendRetry: _run(go()) + def test_raises_runtime_error_when_no_attempts_configured(self): + from app.channels.slack import SlackChannel + + async def go(): + bus = MessageBus() + ch = SlackChannel(bus=bus, config={"bot_token": "xoxb-test", "app_token": "xapp-test"}) + ch._web_client = MagicMock() + + msg = OutboundMessage(channel_name="slack", chat_id="C123", thread_id="t1", text="hello") + with pytest.raises(RuntimeError, match="without an exception"): + await ch.send(msg, _max_retries=0) + + _run(go()) + # --------------------------------------------------------------------------- # Telegram send retry tests @@ -1912,6 +1926,36 @@ class TestTelegramSendRetry: _run(go()) + def test_raises_runtime_error_when_no_attempts_configured(self): + from app.channels.telegram import TelegramChannel + + async def go(): + bus = MessageBus() + ch = TelegramChannel(bus=bus, config={"bot_token": "test-token"}) + ch._application = MagicMock() + + msg = OutboundMessage(channel_name="telegram", chat_id="12345", thread_id="t1", text="hello") + with pytest.raises(RuntimeError, match="without an exception"): + await ch.send(msg, _max_retries=0) + + _run(go()) + + +class TestFeishuSendRetry: + def test_raises_runtime_error_when_no_attempts_configured(self): + from app.channels.feishu import FeishuChannel + + async def go(): + bus = MessageBus() + ch = FeishuChannel(bus=bus, config={"app_id": "id", "app_secret": "secret"}) + ch._api_client = MagicMock() + + msg = OutboundMessage(channel_name="feishu", chat_id="chat", thread_id="t1", text="hello") + with pytest.raises(RuntimeError, match="without an exception"): + await ch.send(msg, _max_retries=0) + + _run(go()) + # --------------------------------------------------------------------------- # Telegram private-chat thread context tests