Skip to content

Commit daecdc3

Browse files
committed
tests: Exercise Merels adapter move path; add minimal test helpers.
Merels previously had adapter-focused tests for help/start/join, but the move path through GameAdapter was untested. This adds a small in-file test helper to drive the adapter and a test that starts a 2-player game, sends a move, counts model.make_move calls, and asserts that the bot replies. This covers the 'test lib for game_handler' FIXME. I did not add 'computer move' tests, because the Merels bot declares supports_computer = False; there is no single-player/computer flow to exercise. I left a comment in above TestMerelsAdapter that clarifies this. No production changes; tests only. Passes local pytest, mypy, and lint. Fixes #433.
1 parent ec346d6 commit daecdc3

File tree

1 file changed

+90
-13
lines changed

1 file changed

+90
-13
lines changed

zulip_bots/zulip_bots/bots/merels/test_merels.py

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)