| """E2E tests for gateway slash commands (Telegram, Discord). |
| |
| Each test drives a message through the full async pipeline: |
| adapter.handle_message(event) |
| → BasePlatformAdapter._process_message_background() |
| → GatewayRunner._handle_message() (command dispatch) |
| → adapter.send() (captured for assertions) |
| |
| No LLM involved — only gateway-level commands are tested. |
| Tests are parametrized over platforms via the ``platform`` fixture in conftest. |
| """ |
|
|
| import asyncio |
| from unittest.mock import AsyncMock |
|
|
| import pytest |
|
|
| from gateway.platforms.base import SendResult |
| from tests.e2e.conftest import make_event, send_and_capture |
|
|
|
|
| class TestSlashCommands: |
| """Gateway slash commands dispatched through the full adapter pipeline.""" |
|
|
| @pytest.mark.asyncio |
| async def test_help_returns_command_list(self, adapter, platform): |
| send = await send_and_capture(adapter, "/help", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "/new" in response_text |
| assert "/status" in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_status_shows_session_info(self, adapter, platform): |
| send = await send_and_capture(adapter, "/status", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "session" in response_text.lower() or "Session" in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_new_resets_session(self, adapter, runner, platform): |
| send = await send_and_capture(adapter, "/new", platform) |
|
|
| send.assert_called_once() |
| runner.session_store.reset_session.assert_called_once() |
|
|
| @pytest.mark.asyncio |
| async def test_stop_when_no_agent_running(self, adapter, platform): |
| send = await send_and_capture(adapter, "/stop", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| response_lower = response_text.lower() |
| assert "no" in response_lower or "stop" in response_lower or "not running" in response_lower |
|
|
| @pytest.mark.asyncio |
| async def test_commands_shows_listing(self, adapter, platform): |
| send = await send_and_capture(adapter, "/commands", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| |
| assert "/" in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_sequential_commands_share_session(self, adapter, platform): |
| """Two commands from the same chat_id should both succeed.""" |
| send_help = await send_and_capture(adapter, "/help", platform) |
| send_help.assert_called_once() |
|
|
| send_status = await send_and_capture(adapter, "/status", platform) |
| send_status.assert_called_once() |
|
|
| @pytest.mark.asyncio |
| async def test_provider_shows_current_provider(self, adapter, platform): |
| send = await send_and_capture(adapter, "/provider", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "provider" in response_text.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_verbose_responds(self, adapter, platform): |
| send = await send_and_capture(adapter, "/verbose", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| |
| assert "verbose" in response_text.lower() or "tool_progress" in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_personality_lists_options(self, adapter, platform): |
| send = await send_and_capture(adapter, "/personality", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "personalit" in response_text.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_yolo_toggles_mode(self, adapter, platform): |
| send = await send_and_capture(adapter, "/yolo", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "yolo" in response_text.lower() |
|
|
| @pytest.mark.asyncio |
| async def test_compress_command(self, adapter, platform): |
| send = await send_and_capture(adapter, "/compress", platform) |
|
|
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| assert "compress" in response_text.lower() or "context" in response_text.lower() |
|
|
|
|
| class TestSessionLifecycle: |
| """Verify session state changes across command sequences.""" |
|
|
| @pytest.mark.asyncio |
| async def test_new_then_status_reflects_reset(self, adapter, runner, session_entry, platform): |
| """After /new, /status should report the fresh session.""" |
| await send_and_capture(adapter, "/new", platform) |
| runner.session_store.reset_session.assert_called_once() |
|
|
| send = await send_and_capture(adapter, "/status", platform) |
| send.assert_called_once() |
| response_text = send.call_args[1].get("content") or send.call_args[0][1] |
| |
| assert session_entry.session_id[:8] in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_new_is_idempotent(self, adapter, runner, platform): |
| """/new called twice should not crash.""" |
| await send_and_capture(adapter, "/new", platform) |
| await send_and_capture(adapter, "/new", platform) |
| assert runner.session_store.reset_session.call_count == 2 |
|
|
|
|
| class TestAuthorization: |
| """Verify the pipeline handles unauthorized users.""" |
|
|
| @pytest.mark.asyncio |
| async def test_unauthorized_user_gets_pairing_response(self, adapter, runner, platform): |
| """Unauthorized DM should trigger pairing code, not a command response.""" |
| runner._is_user_authorized = lambda _source: False |
|
|
| event = make_event(platform, "/help") |
| adapter.send.reset_mock() |
| await adapter.handle_message(event) |
| await asyncio.sleep(0.3) |
|
|
| |
| |
| adapter.send.assert_called() |
| response_text = adapter.send.call_args[0][1] if len(adapter.send.call_args[0]) > 1 else "" |
| assert "recognize" in response_text.lower() or "pair" in response_text.lower() or "ABC123" in response_text |
|
|
| @pytest.mark.asyncio |
| async def test_unauthorized_user_does_not_get_help(self, adapter, runner, platform): |
| """Unauthorized user should NOT see the help command output.""" |
| runner._is_user_authorized = lambda _source: False |
|
|
| event = make_event(platform, "/help") |
| adapter.send.reset_mock() |
| await adapter.handle_message(event) |
| await asyncio.sleep(0.3) |
|
|
| |
| if adapter.send.called: |
| response_text = adapter.send.call_args[0][1] if len(adapter.send.call_args[0]) > 1 else "" |
| assert "/new" not in response_text |
|
|
|
|
| class TestSendFailureResilience: |
| """Verify the pipeline handles send failures gracefully.""" |
|
|
| @pytest.mark.asyncio |
| async def test_send_failure_does_not_crash_pipeline(self, adapter, platform): |
| """If send() returns failure, the pipeline should not raise.""" |
| adapter.send = AsyncMock(return_value=SendResult(success=False, error="network timeout")) |
| adapter.set_message_handler(adapter._message_handler) |
|
|
| event = make_event(platform, "/help") |
| |
| await adapter.handle_message(event) |
| await asyncio.sleep(0.3) |
|
|
| adapter.send.assert_called() |
|
|