Breaks the two import cycles that forced Protocol stand-ins in the F821 sweep, so the two sites now name the real types. gateway/platforms/event.py (new leaf): MessageType, ProcessingOutcome, MessageEvent moved out of base.py verbatim. Their only dependency is gateway.session.SessionSource; base.py imported helpers.py at module level, so helpers could not name MessageEvent. Now TextBatchAggregator is typed by the real MessageEvent. 249 importers repointed (`from gateway.platforms.base import` -> `.event`, preserving each import's layout); gateway.platforms.__init__ re-exports from .event. The three revert-scheduled PLUGIN-COMPAT pointers that named these symbols (gateway.slash_commands → MessageType, dingtalk → MessageType, photon → ProcessingOutcome) and their COMPAT_MANIFEST rows now target gateway.platforms.event. Docs updated: ADDING_A_PLATFORM.md, adding-platform-adapters.md (en + zh-Hans). tools/mcp_tool_sampling.py: ElicitationHandler no longer holds a back-reference to its MCPServerTask (mcp_tool imports sampling, so the task type cannot be named there). It only ever read owner._pending_call_context, so it takes `call_context: Callable[[], Context | None]` and MCPServerTask passes `lambda: self._pending_call_context`. The consent call is one `functools.partial`, run directly or inside the captured Context. ty on the 11 touched production files vs origin/main: 0 new diagnostics, 14 resolved. (The one `source: SessionSource = None` diagnostic moves with the class; typing it Optional exposes ~60 unguarded call sites — separate follow-up.) Tests: tests/gateway + tests/plugins + tests/tools + touched files, 18,235 passed; the 31 failures reproduce identically on origin/main (macOS /private/tmp, systemd socket, long-path fixtures, live-service tests).
81 lines
2.6 KiB
Python
81 lines
2.6 KiB
Python
"""Tests verifying interrupt key consistency between adapter and gateway.
|
|
|
|
Regression test for a bug where monitor_for_interrupt() in _run_agent used
|
|
source.chat_id to query the adapter, but the adapter stores interrupts under
|
|
the full session key (build_session_key output). This mismatch meant
|
|
interrupts were never detected, causing subagents to ignore new messages.
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import pytest
|
|
|
|
from gateway.config import Platform, PlatformConfig
|
|
from gateway.platforms.base import BasePlatformAdapter, SendResult
|
|
from gateway.platforms.event import MessageEvent, MessageType
|
|
from gateway.session import SessionSource, build_session_key
|
|
|
|
|
|
class StubAdapter(BasePlatformAdapter):
|
|
"""Minimal adapter for interrupt tests."""
|
|
|
|
def __init__(self):
|
|
super().__init__(PlatformConfig(enabled=True, token="test"), Platform.TELEGRAM)
|
|
|
|
async def connect(self, *, is_reconnect: bool = False):
|
|
return True
|
|
|
|
async def disconnect(self):
|
|
pass
|
|
|
|
async def send(self, chat_id, content, reply_to=None, metadata=None):
|
|
return SendResult(success=True, message_id="1")
|
|
|
|
async def send_typing(self, chat_id, metadata=None):
|
|
pass
|
|
|
|
async def get_chat_info(self, chat_id):
|
|
return {"id": chat_id}
|
|
|
|
|
|
def _source(chat_id="123456", chat_type="dm", thread_id=None):
|
|
return SessionSource(
|
|
platform=Platform.TELEGRAM,
|
|
chat_id=chat_id,
|
|
chat_type=chat_type,
|
|
thread_id=thread_id,
|
|
)
|
|
|
|
|
|
class TestInterruptKeyConsistency:
|
|
"""Ensure adapter interrupt methods are queried with session_key, not chat_id."""
|
|
|
|
|
|
def test_session_key_differs_from_chat_id_for_group(self):
|
|
"""Session key for a group chat includes prefix, unlike raw chat_id."""
|
|
source = _source("-1001234", "group")
|
|
session_key = build_session_key(source)
|
|
assert session_key != source.chat_id
|
|
assert "agent:main:" in session_key
|
|
assert source.chat_id in session_key
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_has_pending_interrupt_requires_session_key(self):
|
|
"""has_pending_interrupt returns True only when queried with session_key."""
|
|
adapter = StubAdapter()
|
|
source = _source("123456", "dm")
|
|
session_key = build_session_key(source)
|
|
|
|
# Simulate adapter storing interrupt under session_key
|
|
interrupt_event = asyncio.Event()
|
|
adapter._active_sessions[session_key] = interrupt_event
|
|
interrupt_event.set()
|
|
|
|
# Using session_key → found
|
|
assert adapter.has_pending_interrupt(session_key) is True
|
|
|
|
# Using chat_id → NOT found (this was the bug)
|
|
assert adapter.has_pending_interrupt(source.chat_id) is False
|
|
|
|
|