From fc8f8ad33f41fbb019d75012aad07b62dbfdd017 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:31:29 +0530 Subject: [PATCH] fix(slack): clear uniquely scoped assistant status --- plugins/platforms/slack/adapter.py | 29 ++++++++++++++-------- tests/gateway/test_slack.py | 40 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index e6cd83e1af30..42ff90bbe6ce 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -1608,15 +1608,26 @@ class SlackAdapter(BasePlatformAdapter): metadata.get("thread_id") or metadata.get("thread_ts") or "" ) requested_team_id = self._metadata_team_id(metadata) - if not requested_team_id: - requested_team_id = self._channel_team.get(chat_id, "") active = None if requested_thread_ts: - active_key = self._workspace_thread_key( - requested_team_id, chat_id, requested_thread_ts - ) - if active_key: - active = self._active_status_threads.pop(active_key, None) + if requested_team_id: + active_key = self._workspace_thread_key( + requested_team_id, chat_id, requested_thread_ts + ) + if active_key: + active = self._active_status_threads.pop(active_key, None) + else: + # Do not trust the mutable channel-only workspace fallback for + # a thread-specific cleanup: Slack Connect workspaces can share + # a channel ID. Clear the uniquely matching tracked status and + # let its stored team choose the correct client. + matching_keys = [ + key + for key in self._active_status_threads + if key[1] == str(chat_id) and key[2] == requested_thread_ts + ] + if len(matching_keys) == 1: + active = self._active_status_threads.pop(matching_keys[0], None) else: # Metadata-free cleanup is safe only if exactly one status exists # for this channel; otherwise it may clear another Slack Connect @@ -1624,9 +1635,7 @@ class SlackAdapter(BasePlatformAdapter): matching_keys = [ key for key in self._active_status_threads - if isinstance(key, tuple) - and len(key) == 3 - and key[1] == str(chat_id) + if key[1] == str(chat_id) ] if len(matching_keys) == 1: active = self._active_status_threads.pop(matching_keys[0], None) diff --git a/tests/gateway/test_slack.py b/tests/gateway/test_slack.py index c99ef5f6d17a..604926751090 100644 --- a/tests/gateway/test_slack.py +++ b/tests/gateway/test_slack.py @@ -2292,6 +2292,46 @@ class TestSendTyping: ) assert ("T_TWO", "D_SHARED", "171.000") in adapter._active_status_threads + @pytest.mark.asyncio + async def test_stop_typing_without_team_uses_unique_thread_status(self, adapter): + """A stale channel fallback must not strand a uniquely tracked status.""" + team_one, team_two = AsyncMock(), AsyncMock() + adapter._team_clients.update({"T_ONE": team_one, "T_TWO": team_two}) + await adapter.send_typing( + "D_SHARED", + metadata={"thread_id": "171.000", "slack_team_id": "T_ONE"}, + ) + # Another workspace can overwrite this channel-only fallback map. + adapter._channel_team["D_SHARED"] = "T_TWO" + + await adapter.stop_typing("D_SHARED", metadata={"thread_id": "171.000"}) + + assert team_one.assistant_threads_setStatus.call_args_list[-1] == call( + channel_id="D_SHARED", thread_ts="171.000", status="" + ) + assert ("T_ONE", "D_SHARED", "171.000") not in adapter._active_status_threads + team_two.assistant_threads_setStatus.assert_not_called() + + @pytest.mark.asyncio + async def test_stop_typing_without_team_preserves_ambiguous_thread_statuses( + self, adapter + ): + """Without team metadata, matching workspace statuses must not be guessed.""" + team_one, team_two = AsyncMock(), AsyncMock() + adapter._team_clients.update({"T_ONE": team_one, "T_TWO": team_two}) + for team_id in ("T_ONE", "T_TWO"): + await adapter.send_typing( + "D_SHARED", + metadata={"thread_id": "171.000", "slack_team_id": team_id}, + ) + + await adapter.stop_typing("D_SHARED", metadata={"thread_id": "171.000"}) + + assert ("T_ONE", "D_SHARED", "171.000") in adapter._active_status_threads + assert ("T_TWO", "D_SHARED", "171.000") in adapter._active_status_threads + assert team_one.assistant_threads_setStatus.call_count == 1 + assert team_two.assistant_threads_setStatus.call_count == 1 + @pytest.mark.asyncio async def test_streaming_final_edit_uses_workspace_client_from_metadata( self, adapter