Skip to content

Commit 08aa6e2

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 08aa6e2

File tree

1 file changed

+83
-12
lines changed

1 file changed

+83
-12
lines changed

zulip_bots/zulip_bots/bots/merels/test_merels.py

Lines changed: 83 additions & 12 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,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 = "[email protected]",
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 = "[email protected]",
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 = "[email protected]", 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,7 +94,7 @@ 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)
@@ -104,12 +139,12 @@ def test_join_starts_game_emits_start_message(self) -> None:
104139
def test_message_handler_helpers(self) -> None:
105140
bot, _ = self._get_handlers()
106141

107-
# parse_board returns the given board representation.
142+
# Identity parse_board.
108143
self.assertEqual(
109144
bot.game_message_handler.parse_board("sample_board_repr"), "sample_board_repr"
110145
)
111146

112-
# Token color is one of the two known emoji.
147+
# Token color in allowed set.
113148
self.assertIn(
114149
bot.game_message_handler.get_player_color(0),
115150
(":o_button:", ":cross_mark_button:"),
@@ -124,3 +159,39 @@ def test_message_handler_helpers(self) -> None:
124159
bot.game_message_handler.alert_move_message("foo", "move 1,1"),
125160
"foo :move 1,1",
126161
)
162+
163+
def test_move_after_join_invokes_make_move_and_replies(self) -> None:
164+
"""
165+
After start/join, Merels begins in placement (Phase 1). Use 'put v,h'
166+
and assert the adapter emits an acknowledgement. Try both players to
167+
avoid assuming turn order.
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="[email protected]", user_name="foo"
174+
)
175+
_ = self.send_and_collect(bot, bot_handler, "join", user="[email protected]", user_name="bar")
176+
177+
# Stable oracles from the handler's formatter.
178+
ack_foo = bot.game_message_handler.alert_move_message("foo", "put 1,1")
179+
ack_bar = bot.game_message_handler.alert_move_message("bar", "put 1,1")
180+
181+
# Try current player first (unknown), then the other.
182+
contents_foo = self.send_and_collect(
183+
bot, bot_handler, "put 1,1", user="[email protected]", user_name="foo"
184+
)
185+
joined = " ".join(contents_foo)
186+
187+
if (ack_foo not in joined) and (ack_bar not in joined) and (":put 1,1" not in joined):
188+
contents_bar = self.send_and_collect(
189+
bot, bot_handler, "put 1,1", user="[email protected]", user_name="bar"
190+
)
191+
joined += " " + " ".join(contents_bar)
192+
193+
# Assert the adapter produced a placement acknowledgement.
194+
self.assertTrue(
195+
any(h in joined for h in (":put 1,1", ack_foo, ack_bar)),
196+
f"No placement acknowledgement found in: {joined}",
197+
)

0 commit comments

Comments
 (0)