Bläddra i källkod

test(chatto): pin reaction routing for interactive prompts

The backlog asked for a regression test once the routing landed: the
interactive-prompt path and the self-event filter interact, and nothing
held that interaction still.

Four cases now do: a numbered reaction resolves the clarify and untracks
the card, the bot's own reaction echo resolves nothing because the
envelope filter runs first, a stranger's reaction is consumed without
resolving (auth stays fail-closed), and a reaction on an ordinary message
is still forwarded to the gateway hook.
Paul Klumpp 1 vecka sedan
förälder
incheckning
60f45fe0ab
1 ändrade filer med 92 tillägg och 0 borttagningar
  1. 92 0
      test_adapter.py

+ 92 - 0
test_adapter.py

@@ -1215,6 +1215,98 @@ class TestReactionForwarding:
         await adapter._handle_realtime_event(event)
 
 
+# -- Interactive prompt reactions --
+
+
+class TestInteractiveReactionRouting:
+    """A reaction on a prompt card resolves it; the bot's own echo does not.
+
+    Routing sits behind the envelope's self-event filter, so a reaction the
+    bot itself adds can never answer the prompt it decorates — the echo rule
+    and the prompt rule have to hold together, which is what these pin.
+    """
+
+    @pytest_asyncio.fixture
+    def adapter(self):
+        adapter = _make_adapter(allowed_users="human-1")
+        adapter.me = _make_user("bot-user-id", "hermes_bot")
+        adapter.edit_message = AsyncMock()
+        return adapter
+
+    def _event(self, emoji, *, actor_id="human-1", message_id="prompt-1"):
+        event = MagicMock()
+        event.id = f"evt-{emoji}-{actor_id}"
+        event.kind = "reaction_added"
+        event.actor_id = actor_id
+        event.payload = MagicMock(
+            room_id="room-1", message_event_id=message_id, emoji=emoji
+        )
+        return event
+
+    def _clarify(self, adapter):
+        from adapter import _ClarifyPrompt
+
+        prompt = _ClarifyPrompt(
+            clarify_id="clarify-1",
+            choices=["yes", "no"],
+            session_key="session-1",
+            chat_id="room-1",
+            message_id="prompt-1",
+            expires_at=float("inf"),
+        )
+        adapter._clarify_prompts["prompt-1"] = prompt
+        adapter._clarify_id_to_message["clarify-1"] = "prompt-1"
+        return prompt
+
+    async def test_a_numbered_reaction_resolves_the_clarify(self, adapter):
+        self._clarify(adapter)
+        resolve = MagicMock()
+        with patch("tools.clarify_gateway.resolve_gateway_clarify", resolve):
+            await adapter._handle_realtime_event(self._event("2️⃣"))
+
+        resolve.assert_called_once_with("clarify-1", "no")
+        assert "prompt-1" not in adapter._clarify_prompts
+        assert "clarify-1" not in adapter._clarify_id_to_message
+
+    async def test_the_bots_own_reaction_does_not_resolve_its_prompt(self, adapter):
+        """The self-event filter runs before routing, so an echo of the bot's
+        own marker cannot answer the prompt it decorates."""
+        self._clarify(adapter)
+        resolve = MagicMock()
+        with patch("tools.clarify_gateway.resolve_gateway_clarify", resolve):
+            await adapter._handle_realtime_event(
+                self._event("2️⃣", actor_id="bot-user-id")
+            )
+
+        resolve.assert_not_called()
+        assert "prompt-1" in adapter._clarify_prompts
+
+    async def test_an_unauthorized_reaction_leaves_the_prompt_live(self, adapter):
+        """Auth is fail-closed here too: a stranger's reaction is consumed but
+        answers nothing."""
+        self._clarify(adapter)
+        resolve = MagicMock()
+        with patch("tools.clarify_gateway.resolve_gateway_clarify", resolve):
+            await adapter._handle_realtime_event(
+                self._event("2️⃣", actor_id="stranger-9")
+            )
+
+        resolve.assert_not_called()
+        assert "prompt-1" in adapter._clarify_prompts
+
+    async def test_a_reaction_on_an_untracked_message_is_forwarded(self, adapter):
+        """Only prompt cards are consumed — every other reaction still reaches
+        the gateway's reaction hook."""
+        handler = AsyncMock()
+        adapter.set_reaction_handler(handler)
+
+        await adapter._handle_realtime_event(
+            self._event("👍", message_id="ordinary-msg")
+        )
+
+        handler.assert_called_once()
+
+
 # -- chat_type mapping --