From 9298099689f9e79dbd03d8b6feae7b2c02aed40d Mon Sep 17 00:00:00 2001 From: miqaeli Date: Wed, 20 May 2026 15:18:17 +0400 Subject: [PATCH] fix(gateway): strip /queue prefix when no agent is running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no agent is active, '/queue ' previously fell through dispatch with its raw text intact instead of being treated as a normal prompt. Rewrite event.text to the bare payload (mirroring the /steer no-active-agent path just below) and return a usage hint when the payload is empty. Salvaged from PR #29290 (queue half only — the /footer mid-run dispatch half already landed on main via #65521). --- gateway/run.py | 9 ++ .../test_gateway_command_dispatch_minimal.py | 148 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 tests/gateway/test_gateway_command_dispatch_minimal.py diff --git a/gateway/run.py b/gateway/run.py index f711e69c8957..e5ca409706ad 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -10257,6 +10257,15 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew if canonical == "background": return await self._handle_background_command(event) + if canonical == "queue": + queue_payload = event.get_command_args().strip() + if not queue_payload: + return "Usage: /queue " + try: + event.text = queue_payload + except Exception: + pass + if canonical == "steer": # No active agent — /steer has no tool call to inject into. # Strip the prefix so downstream treats it as a normal user diff --git a/tests/gateway/test_gateway_command_dispatch_minimal.py b/tests/gateway/test_gateway_command_dispatch_minimal.py new file mode 100644 index 000000000000..e094f22caf93 --- /dev/null +++ b/tests/gateway/test_gateway_command_dispatch_minimal.py @@ -0,0 +1,148 @@ +from datetime import datetime +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from gateway.config import GatewayConfig, Platform, PlatformConfig +from gateway.platforms.base import MessageEvent, MessageType +from gateway.session import SessionEntry, SessionSource, build_session_key + + +def _make_source() -> SessionSource: + return SessionSource( + platform=Platform.TELEGRAM, + user_id="u1", + chat_id="c1", + user_name="tester", + chat_type="dm", + ) + + +def _make_event(text: str) -> MessageEvent: + return MessageEvent( + text=text, + message_type=MessageType.TEXT, + source=_make_source(), + message_id="m1", + internal=True, + ) + + +def _session_entry() -> SessionEntry: + return SessionEntry( + session_key=build_session_key(_make_source()), + session_id="sess-1", + created_at=datetime.now(), + updated_at=datetime.now(), + platform=Platform.TELEGRAM, + chat_type="dm", + total_tokens=0, + ) + + +def _make_runner(): + from gateway.run import GatewayRunner + + runner = object.__new__(GatewayRunner) + runner.config = GatewayConfig( + platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="***")} + ) + adapter = MagicMock() + adapter.send = AsyncMock() + adapter._pending_messages = {} + runner.adapters = {Platform.TELEGRAM: adapter} + runner._voice_mode = {} + runner.hooks = SimpleNamespace( + emit=AsyncMock(), + emit_collect=AsyncMock(return_value=[]), + loaded_hooks=False, + ) + runner.session_store = MagicMock() + runner.session_store.get_or_create_session.return_value = _session_entry() + runner.session_store.load_transcript.return_value = [] + runner.session_store.has_any_sessions.return_value = True + runner._running_agents = {} + runner._running_agents_ts = {} + runner._pending_messages = {} + runner._pending_approvals = {} + runner._queued_events = {} + runner._session_db = MagicMock() + runner._session_db.get_session_title.return_value = None + runner._reasoning_config = None + runner._provider_routing = {} + runner._fallback_model = None + runner._show_reasoning = False + runner._is_user_authorized = lambda _source: True + runner._set_session_env = lambda _context: None + runner._should_send_voice_reply = lambda *_args, **_kwargs: False + runner._send_voice_reply = AsyncMock() + runner._capture_gateway_honcho_if_configured = lambda *args, **kwargs: None + runner._emit_gateway_run_progress = AsyncMock() + runner._update_prompt_pending = {} + runner._busy_input_mode = "interrupt" + runner._draining = False + runner._session_run_generation = {} + runner._session_sources = {} + runner._pending_native_image_paths_by_session = {} + runner._background_tasks = {} + runner._background_task_counter = 0 + runner._session_model_overrides = {} + runner._pending_model_notes = {} + runner._service_tier = None + runner._fast_mode_by_session = {} + runner._goal_state_by_session = {} + runner._goal_runs_in_progress = set() + runner._goal_queued_by_session = set() + runner._is_telegram_topic_root_lobby = lambda _source: False + runner._should_send_telegram_lobby_reminder = lambda _source: False + runner._check_slash_access = lambda _source, _command: None + runner._begin_session_run_generation = lambda _key: 1 + runner._release_running_agent_state = lambda key: runner._running_agents.pop(key, None) + return runner, adapter + + +@pytest.mark.asyncio +@pytest.mark.parametrize("command_text", ["/queue do this next", "/q do this next"]) +async def test_idle_queue_sends_payload_as_next_turn(command_text): + runner, _adapter = _make_runner() + captured = {} + + async def fake_handle_message_with_agent(event, source, key, generation): + captured["text"] = event.text + captured["command"] = event.get_command() + captured["source"] = source + captured["key"] = key + captured["generation"] = generation + return {"final_response": "", "messages": []} + + runner._handle_message_with_agent = fake_handle_message_with_agent + + result = await runner._handle_message(_make_event(command_text)) + + assert result == {"final_response": "", "messages": []} + assert captured["text"] == "do this next" + assert captured["command"] is None + assert captured["source"] == _make_source() + assert captured["key"] == build_session_key(_make_source()) + assert captured["generation"] == 1 + assert runner._running_agents == {} + + +@pytest.mark.asyncio +async def test_idle_queue_without_payload_returns_usage(): + runner, _adapter = _make_runner() + called = False + + async def fake_handle_message_with_agent(event, source, key, generation): + nonlocal called + called = True + return {"final_response": "", "messages": []} + + runner._handle_message_with_agent = fake_handle_message_with_agent + + result = await runner._handle_message(_make_event("/queue")) + + assert result == "Usage: /queue " + assert called is False + assert runner._running_agents == {}