|
|
@@ -1383,6 +1383,28 @@ class TestInteractiveReactionRouting:
|
|
|
assert "prompt-1" not in adapter._clarify_prompts
|
|
|
assert "clarify-1" not in adapter._clarify_id_to_message
|
|
|
|
|
|
+ async def test_a_shortcode_reaction_event_resolves_the_clarify(self, adapter):
|
|
|
+ """The server echoes shortcodes, not Unicode: reacting on the web
|
|
|
+ frontend delivers "two" in the event, and that must resolve option 2."""
|
|
|
+ self._clarify(adapter)
|
|
|
+ resolve = MagicMock()
|
|
|
+ with patch("tools.clarify_gateway.resolve_gateway_clarify", resolve):
|
|
|
+ await adapter._handle_realtime_event(self._event("two"))
|
|
|
+
|
|
|
+ resolve.assert_called_once_with("clarify-1", "no")
|
|
|
+ assert "prompt-1" not in adapter._clarify_prompts
|
|
|
+
|
|
|
+ async def test_a_check_shortcode_reaction_resolves_the_approval(self, adapter):
|
|
|
+ """The user's bug, pinned twice over: the server delivers the check as
|
|
|
+ the shortcode name — "white_check_mark" from a web pick, or the client
|
|
|
+ variant ✔️ over the wire. Both must resolve "once"."""
|
|
|
+ self._approval(adapter, choices_map={"✅": "once", "❌": "deny"})
|
|
|
+ resolve = MagicMock()
|
|
|
+ with patch("tools.approval.resolve_gateway_approval", resolve):
|
|
|
+ await adapter._handle_realtime_event(self._event("white_check_mark"))
|
|
|
+ resolve.assert_called_once_with("session-1", "once")
|
|
|
+ assert "prompt-1" not in adapter._approval_prompts
|
|
|
+
|
|
|
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."""
|
|
|
@@ -1452,15 +1474,37 @@ class TestInteractiveReactionRouting:
|
|
|
assert adapter._clarify_prompts["prompt-1"].awaiting_text is True
|
|
|
|
|
|
async def test_a_green_check_reaction_resolves_the_approval(self, adapter):
|
|
|
- """The user's bug, pinned: the card seeds ✅ (U+2705) and the client
|
|
|
- sends ✔️ (U+2714 + VS16) — the alias fold must accept the variant,
|
|
|
- not answer "that reaction isn't a valid option"."""
|
|
|
+ """The user's bug, pinned twice over: the wire carries gemoji names, so
|
|
|
+ the check arrives as "white_check_mark" (or the client variant alias
|
|
|
+ "heavy_check_mark" → ✔️) — both must resolve "once", never "that
|
|
|
+ reaction isn't a valid option"."""
|
|
|
+ self._approval(
|
|
|
+ adapter, choices_map={"✅": "once", "🌀": "session", "❌": "deny"}
|
|
|
+ )
|
|
|
+ resolve = MagicMock()
|
|
|
+ with patch("tools.approval.resolve_gateway_approval", resolve):
|
|
|
+ await adapter._handle_realtime_event(self._event("heavy_check_mark"))
|
|
|
+ resolve.assert_called_once_with("session-1", "once")
|
|
|
+ assert "prompt-1" not in adapter._approval_prompts
|
|
|
+
|
|
|
self._approval(
|
|
|
adapter, choices_map={"✅": "once", "🌀": "session", "❌": "deny"}
|
|
|
)
|
|
|
resolve = MagicMock()
|
|
|
with patch("tools.approval.resolve_gateway_approval", resolve):
|
|
|
- await adapter._handle_realtime_event(self._event("✔️"))
|
|
|
+ await adapter._handle_realtime_event(self._event("white_check_mark"))
|
|
|
+ resolve.assert_called_once_with("session-1", "once")
|
|
|
+ assert "prompt-1" not in adapter._approval_prompts
|
|
|
+
|
|
|
+ async def test_a_unicode_check_reaction_resolves_the_approval(self, adapter):
|
|
|
+ """Defensive: a client that sends the Unicode glyph instead of the
|
|
|
+ shortcode still resolves — the normalizer accepts both forms."""
|
|
|
+ self._approval(
|
|
|
+ adapter, choices_map={"✅": "once", "🌀": "session", "❌": "deny"}
|
|
|
+ )
|
|
|
+ resolve = MagicMock()
|
|
|
+ with patch("tools.approval.resolve_gateway_approval", resolve):
|
|
|
+ await adapter._handle_realtime_event(self._event("✅\ufe0f"))
|
|
|
resolve.assert_called_once_with("session-1", "once")
|
|
|
assert "prompt-1" not in adapter._approval_prompts
|
|
|
|
|
|
@@ -1497,16 +1541,23 @@ class TestInteractiveReactionRouting:
|
|
|
body = adapter._chatto_client.post_message.await_args.kwargs["body"]
|
|
|
assert "✅ once" in body and "❌ deny" in body
|
|
|
|
|
|
- async def test_a_routed_reaction_is_logged_with_its_codepoints(
|
|
|
+ async def test_a_routed_reaction_is_logged_with_its_wire_form(
|
|
|
self, adapter, caplog
|
|
|
):
|
|
|
"""Diagnosing a rejected reaction from a live log requires seeing the
|
|
|
- exact codepoints the client sent — ✔️ and ✅ differ only there."""
|
|
|
+ exact form the client sent — the shortcode name on the wire, plus the
|
|
|
+ emoji it decoded to."""
|
|
|
self._approval(adapter)
|
|
|
with caplog.at_level(logging.DEBUG, logger="adapter"):
|
|
|
- await adapter._handle_realtime_event(self._event("✔️"))
|
|
|
+ await adapter._handle_realtime_event(self._event("heavy_check_mark"))
|
|
|
|
|
|
- assert any("U+2714" in record.getMessage() for record in caplog.records)
|
|
|
+ message = next(
|
|
|
+ record.getMessage()
|
|
|
+ for record in caplog.records
|
|
|
+ if "reaction on prompt card" in record.getMessage()
|
|
|
+ )
|
|
|
+ assert "heavy_check_mark" in message
|
|
|
+ assert "✅" in message
|
|
|
|
|
|
|
|
|
# -- Prompt card threading --
|
|
|
@@ -1529,6 +1580,7 @@ class TestPromptCardThreading:
|
|
|
return_value=_make_message(body="card", message_id="card-1")
|
|
|
)
|
|
|
adapter._chatto_client.follow_thread = AsyncMock()
|
|
|
+ adapter._chatto_client.add_reaction = AsyncMock()
|
|
|
return adapter
|
|
|
|
|
|
async def test_clarify_card_threads_into_the_conversation(self, adapter):
|
|
|
@@ -1596,6 +1648,43 @@ class TestPromptCardThreading:
|
|
|
assert "✅" in stored.choices_map
|
|
|
assert stored.choices_map["✅"] == "once"
|
|
|
|
|
|
+ async def test_seed_reactions_are_sent_as_shortcodes(self, adapter):
|
|
|
+ """AddReaction accepts gemoji names only — a Unicode seed was rejected
|
|
|
+ server-side with `unsupported emoji`, leaving the card anchorless."""
|
|
|
+ from gateway.platforms.base import ExecApprovalPrompt
|
|
|
+
|
|
|
+ prompt = ExecApprovalPrompt(
|
|
|
+ chat_id="room-1",
|
|
|
+ session_key="session-1",
|
|
|
+ text="Run this?",
|
|
|
+ actions=[("Approve", "once", "primary"), ("Deny", "deny", "danger")],
|
|
|
+ command="ls",
|
|
|
+ description="",
|
|
|
+ smart_denied=False,
|
|
|
+ )
|
|
|
+ adapter._chatto_client.post_message.return_value = _make_message(
|
|
|
+ body="card", message_id="card-3"
|
|
|
+ )
|
|
|
+ await adapter._send_exec_approval_prompt(prompt)
|
|
|
+
|
|
|
+ sent = [
|
|
|
+ call.kwargs["emoji"]
|
|
|
+ for call in adapter._chatto_client.add_reaction.await_args_list
|
|
|
+ ]
|
|
|
+ assert sent == ["white_check_mark", "x"]
|
|
|
+
|
|
|
+ async def test_clarify_seed_reactions_are_sent_as_shortcodes(self, adapter):
|
|
|
+ result = await adapter.send_clarify(
|
|
|
+ "room-1", "Which one?", ["yes", "no"], "clarify-1", "session-1"
|
|
|
+ )
|
|
|
+
|
|
|
+ assert result.success is True
|
|
|
+ sent = [
|
|
|
+ call.kwargs["emoji"]
|
|
|
+ for call in adapter._chatto_client.add_reaction.await_args_list
|
|
|
+ ]
|
|
|
+ assert sent == ["one", "two", "pencil2"]
|
|
|
+
|
|
|
async def test_cards_without_a_thread_stay_at_the_room_root(self, adapter):
|
|
|
"""No metadata, no thread — a DM card (or a bare room) must not invent
|
|
|
one. The DM case is the strict one: _resolve_outbound_thread returns
|