@@ -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 = "[email protected] " ,
sender_full_name = "boo" 1717 )
@@ -21,25 +21,58 @@ 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
28+ class GameAdapterTestLib :
29+ """Small helpers for driving GameAdapter-based bots in tests."""
30+
31+ def send (
32+ self ,
33+ bot ,
34+ bot_handler ,
35+ content : str ,
36+ * ,
37+ 38+ user_name : str = "foo" ,
39+ ) -> None :
40+ bot .handle_message (
41+ self .make_request_message (content , user = user , user_name = user_name ),
42+ bot_handler ,
43+ )
44+
45+ def replies (self , bot_handler ):
46+ # Return the bot message 'content' fields from the transcript.
47+ return [m ["content" ] for (_method , m ) in bot_handler .transcript ]
48+
49+ def send_and_collect (
50+ self ,
51+ bot ,
52+ bot_handler ,
53+ content : str ,
54+ * ,
55+ 56+ user_name : str = "foo" ,
57+ ):
58+ bot_handler .reset_transcript ()
59+ self .send (bot , bot_handler , content , user = user , user_name = user_name )
60+ return self .replies (bot_handler )
61+
2862
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- """
63+ # Note: Merels has no vs-computer mode (in merels.py, supports_computer=False).
64+ # If computer mode is added in the future, add adapter-level tests here.
65+
66+ class TestMerelsAdapter (BotTestCase , DefaultTests , GameAdapterTestLib ):
67+ """Adapter-focused tests (mirrors connect_four); use stable fragment assertions."""
3568
3669 bot_name = "merels"
3770
3871 @override
3972 def make_request_message (
4073 self ,
content :
str ,
user :
str = "[email protected] " ,
user_name :
str = "foo" 4174 ) -> Dict [str , str ]:
42- # Provide stream metadata; GameAdapter reads message["type"], topic, etc .
75+ # Provide stream metadata consumed by GameAdapter .
4376 return {
4477 "sender_email" : user ,
4578 "sender_full_name" : user_name ,
@@ -59,13 +92,12 @@ def test_help_is_merels_help(self) -> None:
5992 self .assertTrue (responses , "No bot response to 'help'" )
6093 help_text = responses [0 ]["content" ]
6194
62- # Stable fragments; resilient to copy tweaks .
95+ # Assert on stable fragments to avoid brittle exact matches .
6396 self .assertIn ("Merels Bot Help" , help_text )
6497 self .assertIn ("start game" , help_text )
6598 self .assertIn ("play game" , help_text )
6699 self .assertIn ("quit" , help_text )
67100 self .assertIn ("rules" , help_text )
68- # Present today; OK if dropped in future wording changes.
69101 self .assertIn ("leaderboard" , help_text )
70102 self .assertIn ("cancel game" , help_text )
71103
@@ -104,12 +136,12 @@ def test_join_starts_game_emits_start_message(self) -> None:
104136 def test_message_handler_helpers (self ) -> None :
105137 bot , _ = self ._get_handlers ()
106138
107- # parse_board returns the given board representation .
139+ # Identity parse_board .
108140 self .assertEqual (
109141 bot .game_message_handler .parse_board ("sample_board_repr" ), "sample_board_repr"
110142 )
111143
112- # Token color is one of the two known emoji .
144+ # Token color in allowed set .
113145 self .assertIn (
114146 bot .game_message_handler .get_player_color (0 ),
115147 (":o_button:" , ":cross_mark_button:" ),
@@ -124,3 +156,46 @@ def test_message_handler_helpers(self) -> None:
124156 bot .game_message_handler .alert_move_message ("foo" , "move 1,1" ),
125157 "foo :move 1,1" ,
126158 )
159+
160+ def test_move_after_join_invokes_make_move_and_replies (self ) -> None :
161+ """
162+ Start a two-player game, send a move via the adapter, count make_move calls,
163+ and assert we get a reply. Try both users to avoid turn assumptions.
164+ """
165+ import types
166+
167+ bot , bot_handler = self ._get_handlers ()
168+
169+ # Start 2P game.
170+ _ = self .send_and_collect (
171+ bot ,
bot_handler ,
"start game" ,
user = "[email protected] " ,
user_name = "foo" 172+ )
173+ _ = self .
send_and_collect (
bot ,
bot_handler ,
"join" ,
user = "[email protected] " ,
user_name = "bar" )
174+
175+ # Count model.make_move invocations.
176+ self .assertTrue (hasattr (bot .model , "make_move" ), "Merels model has no make_move method" )
177+ original = bot .model .make_move
178+ calls = {"n" : 0 }
179+
180+ def _wrapped_make_move (* args , ** kwargs ):
181+ calls ["n" ] += 1
182+ return original (* args , ** kwargs )
183+
184+ bot .model .make_move = types .MethodType (_wrapped_make_move , bot .model ) # type: ignore[attr-defined]
185+ try :
186+ contents_foo = self .send_and_collect (
187+ bot ,
bot_handler ,
"move 1,1" ,
user = "[email protected] " ,
user_name = "foo" 188+ )
189+ if calls ["n" ] == 0 :
190+ contents_bar = self .send_and_collect (
191+ bot ,
bot_handler ,
"move 1,1" ,
user = "[email protected] " ,
user_name = "bar" 192+ )
193+ else :
194+ contents_bar = []
195+
196+ self .assertGreaterEqual (calls ["n" ], 1 , "make_move was not called for a move command" )
197+ self .assertTrue (
198+ contents_foo or contents_bar , "No bot reply after sending a move command"
199+ )
200+ finally :
201+ bot .model .make_move = original # type: ignore[assignment]
0 commit comments