|
| 1 | +# test_chess_smoke.py |
| 2 | +""" |
| 3 | +End-to-end smoke test for the chess puzzle agent pipeline. |
| 4 | +
|
| 5 | +Verifies that ReactAgent + chess tools + chess reward work together: |
| 6 | +- Agent receives a puzzle |
| 7 | +- Tools dispatch to ChessPuzzleEnv |
| 8 | +- Trajectories are populated after run() |
| 9 | +- Reward function produces a valid result |
| 10 | +
|
| 11 | +Requires: Stockfish installed (brew install stockfish / apt-get install stockfish) |
| 12 | +""" |
| 13 | + |
| 14 | +import shutil |
| 15 | +from unittest.mock import AsyncMock |
| 16 | + |
| 17 | +import pytest |
| 18 | + |
| 19 | +from agentfly.agents import ReactAgent |
| 20 | +from agentfly.envs.chess_env import ChessPuzzleEnv |
| 21 | +from agentfly.rewards import chess_puzzle_reward |
| 22 | +from agentfly.tools import chess_get_legal_moves, chess_get_state, chess_move |
| 23 | + |
| 24 | +# Skip if Stockfish is not available |
| 25 | +pytestmark = pytest.mark.skipif( |
| 26 | + shutil.which("stockfish") is None, |
| 27 | + reason="Stockfish not installed", |
| 28 | +) |
| 29 | + |
| 30 | +# A simple mate-in-1 puzzle: White plays Qxf7# |
| 31 | +MATE_IN_1_PUZZLE = { |
| 32 | + "puzzle_id": "smoke_mate1", |
| 33 | + "fen": "r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4", |
| 34 | + "moves": "h5f7", |
| 35 | +} |
| 36 | + |
| 37 | + |
| 38 | +def _make_react_responses(): |
| 39 | + """Return a sequence of canned ReAct responses for the smoke test. |
| 40 | +
|
| 41 | + Turn 0: agent checks the board state |
| 42 | + Turn 1: agent makes the winning move Qxf7# |
| 43 | + """ |
| 44 | + return [ |
| 45 | + # Turn 0 – get state |
| 46 | + ( |
| 47 | + 'Thought: Let me look at the current board position first.\n' |
| 48 | + 'Action: chess_get_state\n' |
| 49 | + 'Input: {}' |
| 50 | + ), |
| 51 | + # Turn 1 – make the winning move |
| 52 | + ( |
| 53 | + 'Thought: I see White can play Qxf7# for checkmate.\n' |
| 54 | + 'Action: chess_move\n' |
| 55 | + 'Input: {"move": "h5f7"}' |
| 56 | + ), |
| 57 | + ] |
| 58 | + |
| 59 | + |
| 60 | +@pytest.mark.asyncio |
| 61 | +async def test_chess_smoke_e2e(): |
| 62 | + """Smoke test: ReactAgent solves a mate-in-1 puzzle with chess tools.""" |
| 63 | + |
| 64 | + canned = _make_react_responses() |
| 65 | + call_idx = 0 |
| 66 | + |
| 67 | + async def fake_generate(messages_list, **kwargs): |
| 68 | + nonlocal call_idx |
| 69 | + idx = min(call_idx, len(canned) - 1) |
| 70 | + call_idx += 1 |
| 71 | + return [canned[idx]] |
| 72 | + |
| 73 | + tools = [chess_move, chess_get_state, chess_get_legal_moves] |
| 74 | + |
| 75 | + agent = ReactAgent( |
| 76 | + model_name_or_path="Qwen/Qwen2.5-3B-Instruct", |
| 77 | + tools=tools, |
| 78 | + backend="client", |
| 79 | + reward_fn=chess_puzzle_reward, |
| 80 | + monitors=[], |
| 81 | + debug=True, |
| 82 | + ) |
| 83 | + |
| 84 | + # Replace LLM engine methods with mocks |
| 85 | + agent.llm_engine = AsyncMock() |
| 86 | + agent.llm_engine.generate_async = fake_generate |
| 87 | + agent.llm_engine.preprocess = lambda: None |
| 88 | + agent.llm_engine.postprocess = lambda: None |
| 89 | + |
| 90 | + messages = [ |
| 91 | + { |
| 92 | + "messages": [ |
| 93 | + { |
| 94 | + "role": "user", |
| 95 | + "content": ( |
| 96 | + "Solve this chess puzzle. The position is a mate-in-1. " |
| 97 | + "Find the winning move for White." |
| 98 | + ), |
| 99 | + } |
| 100 | + ], |
| 101 | + **MATE_IN_1_PUZZLE, |
| 102 | + } |
| 103 | + ] |
| 104 | + |
| 105 | + await agent.run( |
| 106 | + messages=messages, |
| 107 | + max_turns=2, |
| 108 | + num_chains=1, |
| 109 | + enable_streaming=False, |
| 110 | + ) |
| 111 | + |
| 112 | + # Trajectories should be populated |
| 113 | + trajectories = agent.trajectories |
| 114 | + assert len(trajectories) > 0, "Expected at least one trajectory" |
| 115 | + |
| 116 | + # The trajectory should contain messages |
| 117 | + traj = trajectories[0] |
| 118 | + assert "messages" in traj |
| 119 | + assert len(traj["messages"]) > 1, "Expected multiple messages in trajectory" |
0 commit comments