Skip to content

Commit 26dd45f

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 26dd45f

File tree

1 file changed

+88
-13
lines changed

1 file changed

+88
-13
lines changed

zulip_bots/zulip_bots/bots/merels/test_merels.py

Lines changed: 88 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="[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+
user: str = "[email protected]",
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+
user: str = "[email protected]",
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

Comments
 (0)