@@ -11,7 +11,7 @@ class TestMerelsBot(BotTestCase, DefaultTests):
1111 bot_name = "merels"
1212
1313 def test_no_command (self ) -> None :
14- # Sanity: out -of-game message for random content .
14+ # Out -of-game message for arbitrary input .
1515 message = dict (
1616 content = "magic" , type = "stream" , sender_email = "boo@email.com" , sender_full_name = "boo"
1717 )
@@ -21,25 +21,60 @@ def test_no_command(self) -> None:
2121 )
2222
2323 def test_parse_board_identity_empty_board (self ) -> None :
24- # parse_board is identity for Merels ; verify with the canonical empty board.
24+ # Merels parse_board is identity; verify with the canonical empty board.
2525 bot , _ = self ._get_handlers ()
2626 self .assertEqual (bot .game_message_handler .parse_board (EMPTY_BOARD ), EMPTY_BOARD )
2727
2828
29- class TestMerelsAdapter (BotTestCase , DefaultTests ):
30- """
31- Adapter-focused tests mirroring connect_four, kept in this file to
32- keep Merels tests cohesive. Assert on stable fragments to avoid brittle
33- exact-string matches.
34- """
29+ class GameAdapterTestLib :
30+ """Small helpers for driving GameAdapter-based bots in tests."""
31+
32+ def send (
33+ self ,
34+ bot ,
35+ bot_handler ,
36+ content : str ,
37+ * ,
38+ user : str = "foo@example.com" ,
39+ user_name : str = "foo" ,
40+ ) -> None :
41+ bot .handle_message (
42+ self .make_request_message (content , user = user , user_name = user_name ),
43+ bot_handler ,
44+ )
45+
46+ def replies (self , bot_handler ):
47+ # Return the bot message 'content' fields from the transcript.
48+ return [m ["content" ] for (_method , m ) in bot_handler .transcript ]
49+
50+ def send_and_collect (
51+ self ,
52+ bot ,
53+ bot_handler ,
54+ content : str ,
55+ * ,
56+ user : str = "foo@example.com" ,
57+ user_name : str = "foo" ,
58+ ):
59+ bot_handler .reset_transcript ()
60+ self .send (bot , bot_handler , content , user = user , user_name = user_name )
61+ return self .replies (bot_handler )
62+
63+
64+ # Note: Merels has no vs-computer mode (in merels.py, supports_computer=False).
65+ # If computer mode is added in the future, add adapter-level tests here.
66+
67+
68+ class TestMerelsAdapter (BotTestCase , DefaultTests , GameAdapterTestLib ):
69+ """Adapter-focused tests (mirrors connect_four); use stable fragment assertions."""
3570
3671 bot_name = "merels"
3772
3873 @override
3974 def make_request_message (
4075 self , content : str , user : str = "foo@example.com" , user_name : str = "foo"
4176 ) -> Dict [str , str ]:
42- # Provide stream metadata; GameAdapter reads message["type"], topic, etc .
77+ # Provide stream metadata consumed by GameAdapter .
4378 return {
4479 "sender_email" : user ,
4580 "sender_full_name" : user_name ,
@@ -59,13 +94,12 @@ def test_help_is_merels_help(self) -> None:
5994 self .assertTrue (responses , "No bot response to 'help'" )
6095 help_text = responses [0 ]["content" ]
6196
62- # Stable fragments; resilient to copy tweaks .
97+ # Assert on stable fragments to avoid brittle exact matches .
6398 self .assertIn ("Merels Bot Help" , help_text )
6499 self .assertIn ("start game" , help_text )
65100 self .assertIn ("play game" , help_text )
66101 self .assertIn ("quit" , help_text )
67102 self .assertIn ("rules" , help_text )
68- # Present today; OK if dropped in future wording changes.
69103 self .assertIn ("leaderboard" , help_text )
70104 self .assertIn ("cancel game" , help_text )
71105
@@ -104,12 +138,12 @@ def test_join_starts_game_emits_start_message(self) -> None:
104138 def test_message_handler_helpers (self ) -> None :
105139 bot , _ = self ._get_handlers ()
106140
107- # parse_board returns the given board representation .
141+ # Identity parse_board .
108142 self .assertEqual (
109143 bot .game_message_handler .parse_board ("sample_board_repr" ), "sample_board_repr"
110144 )
111145
112- # Token color is one of the two known emoji .
146+ # Token color in allowed set .
113147 self .assertIn (
114148 bot .game_message_handler .get_player_color (0 ),
115149 (":o_button:" , ":cross_mark_button:" ),
@@ -124,3 +158,46 @@ def test_message_handler_helpers(self) -> None:
124158 bot .game_message_handler .alert_move_message ("foo" , "move 1,1" ),
125159 "foo :move 1,1" ,
126160 )
161+
162+ def test_move_after_join_invokes_make_move_and_replies (self ) -> None :
163+ """
164+ Start a two-player game, send a move via the adapter, count make_move calls,
165+ and assert we get a reply. Try both users to avoid turn assumptions.
166+ """
167+ import types
168+
169+ bot , bot_handler = self ._get_handlers ()
170+
171+ # Start 2P game.
172+ _ = self .send_and_collect (
173+ bot , bot_handler , "start game" , user = "foo@example.com" , user_name = "foo"
174+ )
175+ _ = self .send_and_collect (bot , bot_handler , "join" , user = "bar@example.com" , user_name = "bar" )
176+
177+ # Count model.make_move invocations.
178+ self .assertTrue (hasattr (bot .model , "make_move" ), "Merels model has no make_move method" )
179+ original = bot .model .make_move
180+ calls = {"n" : 0 }
181+
182+ def _wrapped_make_move (* args , ** kwargs ):
183+ calls ["n" ] += 1
184+ return original (* args , ** kwargs )
185+
186+ bot .model .make_move = types .MethodType (_wrapped_make_move , bot .model ) # type: ignore[attr-defined]
187+ try :
188+ contents_foo = self .send_and_collect (
189+ bot , bot_handler , "move 1,1" , user = "foo@example.com" , user_name = "foo"
190+ )
191+ if calls ["n" ] == 0 :
192+ contents_bar = self .send_and_collect (
193+ bot , bot_handler , "move 1,1" , user = "bar@example.com" , user_name = "bar"
194+ )
195+ else :
196+ contents_bar = []
197+
198+ self .assertGreaterEqual (calls ["n" ], 1 , "make_move was not called for a move command" )
199+ self .assertTrue (
200+ contents_foo or contents_bar , "No bot reply after sending a move command"
201+ )
202+ finally :
203+ bot .model .make_move = original # type: ignore[assignment]
0 commit comments