4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
162 lines
6.3 KiB
Python
162 lines
6.3 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
from unittest.mock import MagicMock
|
|
|
|
from gateway.gateway_output_sink import GatewayOutputSink
|
|
from gateway.polling.telegram_poller.client import TelegramBotClient
|
|
from platform.notifications.limits import MAX_MESSAGE_SIZE
|
|
|
|
|
|
def test_initial_status_is_not_working_placeholder() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
|
|
GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sent = client.send_message.call_args.args[1]
|
|
assert sent != "Working…"
|
|
assert sent.endswith("…")
|
|
client.send_chat_action.assert_called_with("123", "typing")
|
|
|
|
|
|
def test_set_status_never_shows_working_placeholder() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.set_tool_status("Working…")
|
|
|
|
edited = client.edit_message_text.call_args.args[2]
|
|
assert edited != "Working…"
|
|
assert not edited.startswith("Working")
|
|
|
|
|
|
def test_render_response_header_uses_friendly_assistant_status() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.render_response_header("assistant")
|
|
|
|
edited = client.edit_message_text.call_args.args[2]
|
|
assert edited == "💬 Composing your reply…"
|
|
|
|
|
|
def test_stream_throttles_edits() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=10.0)
|
|
text = sink.stream(label="assistant", chunks=["hello", " world"])
|
|
assert text == "hello world"
|
|
assert client.edit_message_text.call_count >= 1
|
|
|
|
|
|
def test_finalize_truncates_long_text() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
sink.finalize("x" * 5000)
|
|
edited = client.edit_message_text.call_args[0][2]
|
|
assert len(edited) <= MAX_MESSAGE_SIZE
|
|
|
|
|
|
def test_finalize_logs_outbound_edited_message(caplog) -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
with caplog.at_level(logging.INFO, logger="gateway"):
|
|
sink.finalize("hello\nteam")
|
|
|
|
assert "outbound chat=123 text='hello team'" in caplog.text
|
|
|
|
|
|
def test_finalize_logs_outbound_fallback_send(caplog) -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.side_effect = [(True, "", "1"), (True, "", "2")]
|
|
client.edit_message_text.return_value = (False, "edit failed")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
with caplog.at_level(logging.INFO, logger="gateway"):
|
|
sink.finalize("fallback message")
|
|
|
|
assert "outbound chat=123 text='fallback message'" in caplog.text
|
|
|
|
|
|
def test_finalize_renders_headers_and_tables_as_html() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.finalize("## Open PRs\n\n| # | Title |\n|---|---|\n| 3811 | docs fix |\n\n---\n\n**Done**")
|
|
|
|
html_text = client.edit_message_text.call_args.args[2]
|
|
assert "<b>Open PRs</b>" in html_text
|
|
assert "• <b>3811</b> — docs fix" in html_text
|
|
assert "<b>Done</b>" in html_text
|
|
assert "|---|" not in html_text
|
|
|
|
|
|
def test_finalize_renders_markdown_as_html() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.finalize("**bold** and `code`")
|
|
|
|
call = client.edit_message_text.call_args
|
|
assert call.kwargs.get("parse_mode") == "HTML"
|
|
assert "<b>bold</b>" in call.args[2]
|
|
assert "<code>code</code>" in call.args[2]
|
|
|
|
|
|
def test_finalize_falls_back_to_plain_when_html_rejected() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
# HTML edit fails (bad markup); plain retry succeeds.
|
|
client.edit_message_text.side_effect = [(False, "can't parse entities"), (True, "")]
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.finalize("**bold**")
|
|
|
|
assert client.edit_message_text.call_count == 2
|
|
first, second = client.edit_message_text.call_args_list
|
|
assert first.kwargs.get("parse_mode") == "HTML"
|
|
assert second.kwargs.get("parse_mode", "") == ""
|
|
assert second.args[2] == "**bold**"
|
|
|
|
|
|
def test_render_error_appends_auth_login_hint_on_credit_exhaustion() -> None:
|
|
from core.llm.shared.llm_retry import CREDIT_EXHAUSTED_MARKER
|
|
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.render_error(f"Anthropic {CREDIT_EXHAUSTED_MARKER}. Original error: 400")
|
|
|
|
finalized = client.edit_message_text.call_args[0][2]
|
|
assert "opensre auth login" in finalized
|
|
|
|
|
|
def test_render_error_no_auth_hint_for_generic_error() -> None:
|
|
client = MagicMock(spec=TelegramBotClient)
|
|
client.send_message.return_value = (True, "", "1")
|
|
client.edit_message_text.return_value = (True, "")
|
|
sink = GatewayOutputSink(client=client, chat_id="123", edit_interval_seconds=0.0)
|
|
|
|
sink.render_error("something else broke")
|
|
|
|
finalized = client.edit_message_text.call_args[0][2]
|
|
assert "opensre auth login" not in finalized
|