Quellcode durchsuchen

feat(chatto): implement retire_clarify_card for interactive prompts

Override to let the gateway end clarify without a click (timeout,
session reset, superseding free prose). Edits the prompt message to a
terminal state, clears both registries, and guards against double-
resolution.

Also fixes basedpyright errors in the shared reaction routing from
the previous task: move clarify_gateway import to module level, use
getattr for attribute access on union-typed prompts, add type ignores
where the dataclass fields differ.
Paul Klumpp vor 1 Woche
Ursprung
Commit
8b7c8e68ca
2 geänderte Dateien mit 89 neuen und 8 gelöschten Zeilen
  1. 31 8
      adapter.py
  2. 58 0
      test_adapter.py

+ 31 - 8
adapter.py

@@ -1856,16 +1856,16 @@ class ChattoAdapter(BasePlatformAdapter):
         )
 
         if is_clarify:
+            from tools import clarify_gateway  # type: ignore[import-not-found]
+
             number_emojis = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣"]
             other_emoji = "✏️"
             choices = list(getattr(prompt, "choices", []) or [])
 
             if emoji == other_emoji:
-                setattr(prompt, "awaiting_text", True)
+                prompt.awaiting_text = True  # type: ignore[assignment]
                 try:
-                    import tools.clarify_gateway as clarify_gateway
-
-                    clarify_gateway.mark_awaiting_text(getattr(prompt, "clarify_id"))
+                    clarify_gateway.mark_awaiting_text(prompt.clarify_id)  # type: ignore[attr-defined]
                 except Exception:
                     logger.debug("Chatto: mark_awaiting_text failed", exc_info=True)
                 try:
@@ -1882,14 +1882,12 @@ class ChattoAdapter(BasePlatformAdapter):
                     choice = choices[idx]
                     prompt.resolved = True
                     self._clarify_prompts.pop(message_id, None)
-                    cid = getattr(prompt, "clarify_id", None)
+                    cid = getattr(prompt, "clarify_id", None)  # type: ignore[arg-type]
                     if cid is not None:
                         self._clarify_id_to_message.pop(cid, None)
                     try:
-                        import tools.clarify_gateway as clarify_gateway
-
                         clarify_gateway.resolve_gateway_clarify(
-                            getattr(prompt, "clarify_id"), choice
+                            str(cid), choice
                         )
                     except Exception:
                         logger.exception("Chatto: resolve_gateway_clarify failed")
@@ -3018,6 +3016,31 @@ class ChattoAdapter(BasePlatformAdapter):
 
         return SendResult(success=True, message_id=message_id)
 
+    async def retire_clarify_card(self, clarify_id: str, notice: str) -> None:
+        """Rewrite a still-live clarify card into a terminal, button-less state.
+
+        The gateway calls this whenever it ends a clarify without a click — the
+        wait timed out, the session was reset, or unmatched free prose superseded
+        the prompt — so the card stops advertising an answer path the released
+        clarify can no longer accept.  Keyed by ``clarify_id``, so a late call
+        cannot touch a newer prompt; no-op once resolved.
+
+        BasePlatformAdapter override
+        """
+        message_id = self._clarify_id_to_message.pop(clarify_id, None)
+        if message_id is None:
+            return
+        prompt = self._clarify_prompts.get(message_id)
+        if prompt is None:
+            return
+        prompt.resolved = True
+        terminal = f"❌ {notice}"
+        try:
+            await self.edit_message(prompt.chat_id, message_id, terminal)
+        except Exception:
+            logger.debug("Chatto: retire_clarify_card edit failed", exc_info=True)
+        self._clarify_prompts.pop(message_id, None)
+
     async def _send_exec_approval_prompt(
         self, prompt: ExecApprovalPrompt
     ) -> SendResult:

+ 58 - 0
test_adapter.py

@@ -5623,3 +5623,61 @@ class TestInteractivePromptAuth:
         adapter.chatto_config.allow_all_users.value = False
         adapter.chatto_config.allowed_users.value = []
         assert adapter._is_allowed_user_id("user-1") is False
+
+
+class TestRetireClarifyCard:
+    """Test retire_clarify_card edits the prompt message and untracks."""
+
+    def _make_clarify_prompt(self, adapter, clarify_id, chat_id):
+        """Helper to populate the clarify registries."""
+        import time
+
+        from adapter import INTERACTIVE_PROMPT_TIMEOUT_SECONDS, _ClarifyPrompt
+
+        prompt = _ClarifyPrompt(
+            clarify_id=clarify_id,
+            choices=["A", "B"],
+            session_key="sk-1",
+            chat_id=chat_id,
+            message_id="msg-prompt",
+            expires_at=time.monotonic() + INTERACTIVE_PROMPT_TIMEOUT_SECONDS,
+        )
+        adapter._clarify_prompts["msg-prompt"] = prompt
+        adapter._clarify_id_to_message[clarify_id] = "msg-prompt"
+        return prompt
+
+    @pytest.mark.asyncio
+    async def test_retire_edits_message_and_untracks(self):
+        adapter = _make_adapter()
+        adapter.edit_message = AsyncMock()
+        self._make_clarify_prompt(adapter, "clarify-1", "room-1")
+
+        await adapter.retire_clarify_card("clarify-1", "Timed out")
+
+        adapter.edit_message.assert_awaited_once_with(
+            "room-1", "msg-prompt", "❌ Timed out"
+        )
+        assert "clarify-1" not in adapter._clarify_id_to_message
+        assert "msg-prompt" not in adapter._clarify_prompts
+        assert adapter._clarify_prompts.get("msg-prompt") is None
+
+    @pytest.mark.asyncio
+    async def test_retire_unknown_clarify_id_noop(self):
+        adapter = _make_adapter()
+        adapter.edit_message = AsyncMock()
+
+        await adapter.retire_clarify_card("nonexistent", "gone")
+
+        adapter.edit_message.assert_not_awaited()
+
+    @pytest.mark.asyncio
+    async def test_retire_edited_message_on_error_logs(self):
+        adapter = _make_adapter()
+        adapter.edit_message = AsyncMock(side_effect=Exception("edit failed"))
+
+        self._make_clarify_prompt(adapter, "clarify-2", "room-1")
+
+        await adapter.retire_clarify_card("clarify-2", "error")
+
+        assert "clarify-2" not in adapter._clarify_id_to_message
+        assert "msg-prompt" not in adapter._clarify_prompts