From 0c58318d7925249e214c789747a9f0290106a738 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 7 Jul 2026 14:56:57 -0400 Subject: [PATCH 1/2] test: add payload_utils assistant predicate characterization tests Add unit tests for the existing assistant-thread payload predicates in slack_bolt/request/payload_utils.py (is_event, the user/bot/other message predicates, and is_assistant_event and thread predicates). These tests characterize current behavior and pass against main as-is. Also improve the shared scenario fixtures they rely on: - test_events_assistant: use a C-prefixed channel id for the channel (non-im) message fixtures to match their channel_type - test_events_ignore_self: add channel_type to the reaction item Extracted from the set_suggested_prompts refactor branch so this regression net can land on main first. Co-Authored-By: Claude --- tests/scenario_tests/test_events_assistant.py | 4 +- .../scenario_tests/test_events_ignore_self.py | 1 + .../test_events_assistant.py | 4 +- .../test_events_ignore_self.py | 1 + .../slack_bolt/request/test_payload_utils.py | 287 ++++++++++++++++++ 5 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 tests/slack_bolt/request/test_payload_utils.py diff --git a/tests/scenario_tests/test_events_assistant.py b/tests/scenario_tests/test_events_assistant.py index a1c3f1343..c95296154 100644 --- a/tests/scenario_tests/test_events_assistant.py +++ b/tests/scenario_tests/test_events_assistant.py @@ -409,7 +409,7 @@ def build_payload(event: dict) -> dict: "user_profile": {}, "thread_ts": "1726133698.626339", "parent_user_id": "W222", - "channel": "D111", + "channel": "C111", "event_ts": "1726133700.887259", "channel_type": "channel", } @@ -444,7 +444,7 @@ def build_payload(event: dict) -> dict: "reply_users": ["U222", "W111"], "is_locked": False, }, - "channel": "D111", + "channel": "C111", "hidden": True, "ts": "1726133701.028300", "event_ts": "1726133701.028300", diff --git a/tests/scenario_tests/test_events_ignore_self.py b/tests/scenario_tests/test_events_ignore_self.py index db7d07ea8..3c66f3278 100644 --- a/tests/scenario_tests/test_events_ignore_self.py +++ b/tests/scenario_tests/test_events_ignore_self.py @@ -100,6 +100,7 @@ def handle_app_mention(say): "type": "message", "channel": "C111", "ts": "1599529504.000400", + "channel_type": "channel", }, "reaction": "heart_eyes", "item_user": "W111", diff --git a/tests/scenario_tests_async/test_events_assistant.py b/tests/scenario_tests_async/test_events_assistant.py index edc77ecf3..9e1176c74 100644 --- a/tests/scenario_tests_async/test_events_assistant.py +++ b/tests/scenario_tests_async/test_events_assistant.py @@ -483,7 +483,7 @@ def build_payload(event: dict) -> dict: "user_profile": {}, "thread_ts": "1726133698.626339", "parent_user_id": "W222", - "channel": "D111", + "channel": "C111", "event_ts": "1726133700.887259", "channel_type": "channel", } @@ -518,7 +518,7 @@ def build_payload(event: dict) -> dict: "reply_users": ["U222", "W111"], "is_locked": False, }, - "channel": "D111", + "channel": "C111", "hidden": True, "ts": "1726133701.028300", "event_ts": "1726133701.028300", diff --git a/tests/scenario_tests_async/test_events_ignore_self.py b/tests/scenario_tests_async/test_events_ignore_self.py index 7ec9d0cce..fa398a907 100644 --- a/tests/scenario_tests_async/test_events_ignore_self.py +++ b/tests/scenario_tests_async/test_events_ignore_self.py @@ -89,6 +89,7 @@ async def test_self_events_disabled(self): "type": "message", "channel": "C111", "ts": "1599529504.000400", + "channel_type": "channel", }, "reaction": "heart_eyes", "item_user": "W111", diff --git a/tests/slack_bolt/request/test_payload_utils.py b/tests/slack_bolt/request/test_payload_utils.py new file mode 100644 index 000000000..7f046853f --- /dev/null +++ b/tests/slack_bolt/request/test_payload_utils.py @@ -0,0 +1,287 @@ +from slack_bolt.request.payload_utils import ( + is_event, + is_user_message_event_in_assistant_thread, + is_bot_message_event_in_assistant_thread, + is_other_message_sub_event_in_assistant_thread, + is_assistant_event, + is_assistant_thread_started_event, + is_assistant_thread_context_changed_event, +) +from tests.scenario_tests.test_events_assistant import ( + build_payload, + thread_started_event_body, + thread_context_changed_event_body, + user_message_event_body, + user_message_event_body_with_assistant_thread, + message_changed_event_body, + channel_user_message_event_body, + channel_message_changed_event_body, +) +from tests.scenario_tests.test_message_bot import ( + bot_message_event_payload, + classic_bot_message_event_payload, +) +from tests.scenario_tests.test_message_deleted import event_payload as message_deleted_channel_body +from tests.scenario_tests.test_events_ignore_self import event_body as reaction_added_event_body +from tests.scenario_tests.test_block_actions import body as block_actions_body + +file_share_im_message_body = build_payload( + { + "user": "W222", + "type": "message", + "subtype": "file_share", + "ts": "1726133700.887259", + "text": "uploaded a file", + "files": [ + { + "id": "F111", + "created": 1726133700, + "name": "test.png", + "title": "test.png", + "mimetype": "image/png", + "filetype": "png", + "user": "W222", + "size": 12345, + "mode": "hosted", + "is_external": False, + "is_public": False, + "url_private": "https://files.slack.com/files-pri/T111-F111/test.png", + "permalink": "https://example.slack.com/files/W222/F111/test.png", + } + ], + "upload": True, + "display_as_bot": False, + "thread_ts": "1726133698.626339", + "channel": "D111", + "event_ts": "1726133700.887259", + "channel_type": "im", + } +) + +bot_im_thread_message_body = build_payload( + { + "type": "message", + "ts": "1726133700.887259", + "text": "Here is your answer", + "user": "UB111", + "bot_id": "B111", + "app_id": "A222", + "bot_profile": { + "id": "B111", + "deleted": False, + "name": "assistant-app", + "updated": 1726133600, + "app_id": "A222", + "team_id": "T111", + }, + "thread_ts": "1726133698.626339", + "channel": "D111", + "event_ts": "1726133700.887259", + "channel_type": "im", + } +) + +im_message_no_thread_ts_body = build_payload( + { + "user": "W222", + "type": "message", + "ts": "1726133700.887259", + "text": "A top-level DM, not in a thread", + "channel": "D111", + "event_ts": "1726133700.887259", + "channel_type": "im", + } +) + +slash_command_body = { + "token": "verification_token", + "command": "/test", + "text": "hello", + "user_id": "U111", + "user_name": "primary-owner", + "channel_id": "C111", + "channel_name": "test-channel", + "team_id": "T111", + "team_domain": "test-domain", + "api_app_id": "A111", + "is_enterprise_install": "false", + "response_url": "https://hooks.slack.com/commands/T111/111/xxx", + "trigger_id": "111.222.xxx", +} + + +class TestPayloadUtils: + def test_is_event(self): + positives = { + "thread_started": thread_started_event_body, + "thread_context_changed": thread_context_changed_event_body, + "user_message_im": user_message_event_body, + "user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread, + "message_changed_im": message_changed_event_body, + "channel_user_message": channel_user_message_event_body, + "channel_message_changed": channel_message_changed_event_body, + "bot_message_channel": bot_message_event_payload, + "classic_bot_message_channel": classic_bot_message_event_payload, + "message_deleted_channel": message_deleted_channel_body, + "reaction_added": reaction_added_event_body, + "file_share_im": file_share_im_message_body, + "bot_im_thread": bot_im_thread_message_body, + "im_no_thread_ts": im_message_no_thread_ts_body, + } + negatives = { + "block_actions": block_actions_body, + "slash_command": slash_command_body, + "empty_dict": {}, + } + for key, body in positives.items(): + assert is_event(body), f"{key} should be recognized as an event" + for key, body in negatives.items(): + assert not is_event(body), f"{key} should NOT be recognized as an event" + + def test_is_user_message_event_in_assistant_thread(self): + # Requires: is_im_message_event + thread_ts present + bot_id absent + positives = { + "user_message_im": user_message_event_body, + "user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread, + "file_share_im": file_share_im_message_body, + } + negatives = { + "bot_im_thread": bot_im_thread_message_body, + "im_no_thread_ts": im_message_no_thread_ts_body, + "message_changed_im": message_changed_event_body, + "channel_user_message": channel_user_message_event_body, + "channel_message_changed": channel_message_changed_event_body, + "bot_message_channel": bot_message_event_payload, + "thread_started": thread_started_event_body, + "thread_context_changed": thread_context_changed_event_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in positives.items(): + assert is_user_message_event_in_assistant_thread( + body + ), f"{key} should pass {is_user_message_event_in_assistant_thread.__name__}" + for key, body in negatives.items(): + assert not is_user_message_event_in_assistant_thread( + body + ), f"{key} should NOT pass {is_user_message_event_in_assistant_thread.__name__}" + + def test_is_bot_message_event_in_assistant_thread(self): + # Requires: is_any_im_message_event + subtype is None + thread_ts present + bot_id present + positives = { + "bot_im_thread": bot_im_thread_message_body, + } + negatives = { + "user_message_im": user_message_event_body, + # file_share_im has subtype="file_share" -- the key user/bot asymmetry + "file_share_im": file_share_im_message_body, + "im_no_thread_ts": im_message_no_thread_ts_body, + "message_changed_im": message_changed_event_body, + "channel_user_message": channel_user_message_event_body, + "bot_message_channel": bot_message_event_payload, + "classic_bot_message_channel": classic_bot_message_event_payload, + "thread_started": thread_started_event_body, + "thread_context_changed": thread_context_changed_event_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in positives.items(): + assert is_bot_message_event_in_assistant_thread( + body + ), f"{key} should pass {is_bot_message_event_in_assistant_thread.__name__}" + for key, body in negatives.items(): + assert not is_bot_message_event_in_assistant_thread( + body + ), f"{key} should NOT pass {is_bot_message_event_in_assistant_thread.__name__}" + + def test_is_bot_message_user_message_asymmetry(self): + # file_share in IM thread: user predicate accepts it, bot predicate rejects it + assert is_user_message_event_in_assistant_thread(file_share_im_message_body) + assert not is_bot_message_event_in_assistant_thread(file_share_im_message_body) + + # bot message in IM thread: bot predicate accepts it, user predicate rejects it + assert is_bot_message_event_in_assistant_thread(bot_im_thread_message_body) + assert not is_user_message_event_in_assistant_thread(bot_im_thread_message_body) + + def test_is_other_message_sub_event_in_assistant_thread(self): + positives = { + "message_changed_im": message_changed_event_body, + } + negatives = { + "user_message_im": user_message_event_body, + "bot_im_thread": bot_im_thread_message_body, + "im_no_thread_ts": im_message_no_thread_ts_body, + "channel_message_changed": channel_message_changed_event_body, + "channel_user_message": channel_user_message_event_body, + "message_deleted_channel": message_deleted_channel_body, + "thread_started": thread_started_event_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in positives.items(): + assert is_other_message_sub_event_in_assistant_thread( + body + ), f"{key} should pass {is_other_message_sub_event_in_assistant_thread.__name__}" + for key, body in negatives.items(): + assert not is_other_message_sub_event_in_assistant_thread( + body + ), f"{key} should NOT pass {is_other_message_sub_event_in_assistant_thread.__name__}" + + def test_is_assistant_event(self): + positives = { + "thread_started": thread_started_event_body, + "thread_context_changed": thread_context_changed_event_body, + "user_message_im": user_message_event_body, + "user_message_im_with_assistant_thread": user_message_event_body_with_assistant_thread, + "file_share_im": file_share_im_message_body, + "bot_im_thread": bot_im_thread_message_body, + } + negatives = { + "message_changed_im": message_changed_event_body, + "im_no_thread_ts": im_message_no_thread_ts_body, + "channel_user_message": channel_user_message_event_body, + "channel_message_changed": channel_message_changed_event_body, + "bot_message_channel": bot_message_event_payload, + "classic_bot_message_channel": classic_bot_message_event_payload, + "message_deleted_channel": message_deleted_channel_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in positives.items(): + assert is_assistant_event(body), f"{key} should pass {is_assistant_event.__name__}" + for key, body in negatives.items(): + assert not is_assistant_event(body), f"{key} should NOT pass {is_assistant_event.__name__}" + + def test_is_assistant_thread_started_event(self): + assert is_assistant_thread_started_event(thread_started_event_body) + + negatives = { + "thread_context_changed": thread_context_changed_event_body, + "user_message_im": user_message_event_body, + "message_changed_im": message_changed_event_body, + "bot_im_thread": bot_im_thread_message_body, + "channel_user_message": channel_user_message_event_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in negatives.items(): + assert not is_assistant_thread_started_event( + body + ), f"{key} should NOT pass {is_assistant_thread_started_event.__name__}" + + def test_is_assistant_thread_context_changed_event(self): + assert is_assistant_thread_context_changed_event(thread_context_changed_event_body) + + negatives = { + "thread_started": thread_started_event_body, + "user_message_im": user_message_event_body, + "message_changed_im": message_changed_event_body, + "bot_im_thread": bot_im_thread_message_body, + "channel_user_message": channel_user_message_event_body, + "reaction_added": reaction_added_event_body, + "block_actions": block_actions_body, + } + for key, body in negatives.items(): + assert not is_assistant_thread_context_changed_event( + body + ), f"{key} should NOT pass {is_assistant_thread_context_changed_event.__name__}" From a0509b4b16ad4715596de90a0357d2f7301a32d4 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 7 Jul 2026 15:06:58 -0400 Subject: [PATCH 2/2] clean up comments --- tests/slack_bolt/request/test_payload_utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/slack_bolt/request/test_payload_utils.py b/tests/slack_bolt/request/test_payload_utils.py index 7f046853f..f1c1d94c6 100644 --- a/tests/slack_bolt/request/test_payload_utils.py +++ b/tests/slack_bolt/request/test_payload_utils.py @@ -167,13 +167,11 @@ def test_is_user_message_event_in_assistant_thread(self): ), f"{key} should NOT pass {is_user_message_event_in_assistant_thread.__name__}" def test_is_bot_message_event_in_assistant_thread(self): - # Requires: is_any_im_message_event + subtype is None + thread_ts present + bot_id present positives = { "bot_im_thread": bot_im_thread_message_body, } negatives = { "user_message_im": user_message_event_body, - # file_share_im has subtype="file_share" -- the key user/bot asymmetry "file_share_im": file_share_im_message_body, "im_no_thread_ts": im_message_no_thread_ts_body, "message_changed_im": message_changed_event_body, @@ -195,11 +193,9 @@ def test_is_bot_message_event_in_assistant_thread(self): ), f"{key} should NOT pass {is_bot_message_event_in_assistant_thread.__name__}" def test_is_bot_message_user_message_asymmetry(self): - # file_share in IM thread: user predicate accepts it, bot predicate rejects it assert is_user_message_event_in_assistant_thread(file_share_im_message_body) assert not is_bot_message_event_in_assistant_thread(file_share_im_message_body) - # bot message in IM thread: bot predicate accepts it, user predicate rejects it assert is_bot_message_event_in_assistant_thread(bot_im_thread_message_body) assert not is_user_message_event_in_assistant_thread(bot_im_thread_message_body)