@@ -174,6 +174,101 @@ async def test_bot_own_messages_are_ignored(self, bot):
174174
175175 bot .agent .run .assert_not_called ()
176176
177+ @pytest .mark .anyio
178+ async def test_thread_followup_without_mention_is_ignored (self , bot ):
179+ """A plain (non-@mention) message in a thread is ignored, even if the
180+ same user previously @mentioned the bot in that thread. Every turn
181+ requires an explicit mention.
182+ """
183+ # First turn: user @mentions in thread.
184+ mention_event = {
185+ "channel" : "C001" ,
186+ "user" : "U123" ,
187+ "text" : "<@U_BOT> first question" ,
188+ "thread_ts" : "1111111111.111111" ,
189+ "ts" : "1111111111.111111" ,
190+ }
191+ await bot .handle_mention (mention_event , AsyncMock (), AsyncMock ())
192+ assert bot .agent .run .call_count == 1
193+
194+ # Second turn: same user, same thread, no @mention — should be ignored.
195+ followup = {
196+ "channel" : "C001" ,
197+ "channel_type" : "channel" ,
198+ "user" : "U123" ,
199+ "text" : "follow up without mention" ,
200+ "thread_ts" : "1111111111.111111" ,
201+ "ts" : "2222222222.222222" ,
202+ }
203+ await bot .handle_message (followup , AsyncMock (), AsyncMock ())
204+
205+ # Still only the original mention call.
206+ assert bot .agent .run .call_count == 1
207+
208+
209+ class TestMultiUserThread :
210+ @pytest .mark .anyio
211+ async def test_second_user_mention_in_same_thread_shares_history (self , bot ):
212+ """When a second user joins a thread by @mentioning the bot, they engage
213+ the same conversation key (thread_ts) so the bot has shared history.
214+ """
215+ thread_ts = "1111111111.111111"
216+
217+ alice_mention = {
218+ "channel" : "C001" ,
219+ "user" : "U_ALICE" ,
220+ "text" : "<@U_BOT> what is the capital of France?" ,
221+ "thread_ts" : thread_ts ,
222+ "ts" : thread_ts ,
223+ }
224+ bob_mention = {
225+ "channel" : "C001" ,
226+ "user" : "U_BOB" ,
227+ "text" : "<@U_BOT> and Germany?" ,
228+ "thread_ts" : thread_ts ,
229+ "ts" : "2222222222.222222" ,
230+ }
231+
232+ await bot .handle_mention (alice_mention , AsyncMock (), AsyncMock ())
233+ await bot .handle_mention (bob_mention , AsyncMock (), AsyncMock ())
234+
235+ # Both calls used the same thread_ts as the conversation key.
236+ assert bot .agent .run .call_count == 2
237+ assert bot .agent .run .call_args_list [0 ].args [0 ] == thread_ts
238+ assert bot .agent .run .call_args_list [1 ].args [0 ] == thread_ts
239+
240+ @pytest .mark .anyio
241+ async def test_second_user_plain_message_in_thread_is_ignored (self , bot ):
242+ """A second user typing a plain (non-@mention) message in an active
243+ thread is ignored — they must @mention to participate.
244+ """
245+ thread_ts = "1111111111.111111"
246+
247+ # Alice opens the thread with a mention.
248+ alice_mention = {
249+ "channel" : "C001" ,
250+ "user" : "U_ALICE" ,
251+ "text" : "<@U_BOT> hi" ,
252+ "thread_ts" : thread_ts ,
253+ "ts" : thread_ts ,
254+ }
255+ await bot .handle_mention (alice_mention , AsyncMock (), AsyncMock ())
256+ assert bot .agent .run .call_count == 1
257+
258+ # Bob types in the thread without mentioning the bot.
259+ bob_plain = {
260+ "channel" : "C001" ,
261+ "channel_type" : "channel" ,
262+ "user" : "U_BOB" ,
263+ "text" : "hey what's going on" ,
264+ "thread_ts" : thread_ts ,
265+ "ts" : "2222222222.222222" ,
266+ }
267+ await bot .handle_message (bob_plain , AsyncMock (), AsyncMock ())
268+
269+ # Bob's plain message did not trigger the bot.
270+ assert bot .agent .run .call_count == 1
271+
177272
178273class TestErrorHandling :
179274 @pytest .mark .anyio
0 commit comments