"""Relay /stop interrupt routing (relay Phase 1, Task 1.4). Proves a connector-delivered mid-turn interrupt reaches the existing per-session interrupt mechanism and cancels exactly the targeted session_key's turn — never a sibling's. Mirrors the isolation discipline of test_stop_thread_sibling.py. """ from __future__ import annotations import asyncio import pytest from gateway.config import PlatformConfig from gateway.relay.adapter import RelayAdapter from gateway.relay.descriptor import CONTRACT_VERSION, CapabilityDescriptor from tests.gateway.relay.stub_connector import StubConnector def _desc() -> CapabilityDescriptor: return CapabilityDescriptor( contract_version=CONTRACT_VERSION, platform="discord", label="Discord", max_message_length=2000, supports_draft_streaming=False, supports_edit=True, supports_threads=True, markdown_dialect="discord", len_unit="chars", ) @pytest.fixture def adapter(): return RelayAdapter(PlatformConfig(), _desc(), transport=StubConnector(_desc())) @pytest.mark.asyncio async def test_interrupt_sets_only_target_session_event(adapter): key_a = "agent:main:discord:group:chanA:userX" key_b = "agent:main:discord:group:chanB:userY" ev_a = asyncio.Event() ev_b = asyncio.Event() adapter._active_sessions[key_a] = ev_a adapter._active_sessions[key_b] = ev_b await adapter.on_interrupt(key_a, chat_id="chanA") assert ev_a.is_set() is True, "target session's interrupt Event must be set" assert ev_b.is_set() is False, "sibling session must be untouched" @pytest.mark.asyncio async def test_interrupt_unknown_session_is_noop(adapter): # No active session for this key — must not raise. await adapter.on_interrupt("agent:main:discord:group:nope:userZ", chat_id="nope") @pytest.mark.asyncio async def test_outbound_interrupt_reaches_connector(adapter): """The gateway-side /stop egress: send_interrupt is carried to the connector so it can forward down the socket owning the session_key.""" stub = adapter._transport await stub.send_interrupt("agent:main:discord:group:chanA:userX", reason="stop") assert stub.interrupts == [ {"session_key": "agent:main:discord:group:chanA:userX", "reason": "stop"} ] @pytest.mark.asyncio async def test_connect_wires_inbound_interrupt_over_ws(adapter): """WS-only inbound: connect() registers BOTH the inbound message handler AND the interrupt_inbound handler on the transport, so a connector-delivered interrupt_inbound frame (no HTTP receiver) reaches the right session.""" await adapter.connect() stub = adapter._transport # Both connector->gateway handlers are wired post-connect. assert stub._inbound is not None assert stub._interrupt_inbound is not None key = "agent:main:discord:group:chanA:userX" ev = asyncio.Event() adapter._active_sessions[key] = ev # Simulate the connector pushing an interrupt_inbound frame down the WS. await stub.push_interrupt(key, chat_id="chanA") assert ev.is_set() is True, "interrupt delivered over the WS must cancel the target turn"