"""Tests for the CalcTool and RandomChoiceTool.""" from __future__ import annotations import pytest from app.engine.tools.base import ToolContext from app.engine.tools.game import CalcTool, RandomChoiceTool class _FakeWorld: id = "00000000-0000-0000-0000-000000000001" @pytest.mark.asyncio async def test_calc_simple_arithmetic(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"expression": "2 + 3 * 4"}, ctx) assert r.ok assert r.data["result"] == 14 @pytest.mark.asyncio async def test_calc_with_variables(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({ "expression": "max(1, player_attack - enemy_armor)", "variables": {"player_attack": 10, "enemy_armor": 7}, }, ctx) assert r.ok assert r.data["result"] == 3 @pytest.mark.asyncio async def test_calc_dice_notation(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"expression": "1d6"}, ctx) assert r.ok assert 1 <= r.data["result"] <= 6 assert len(r.data["rolls"]) == 1 @pytest.mark.asyncio async def test_calc_dice_with_modifier(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"expression": "2d6+3"}, ctx) assert r.ok assert 5 <= r.data["result"] <= 15 assert len(r.data["rolls"]) == 2 @pytest.mark.asyncio async def test_calc_rejects_disallowed_chars(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"expression": "__import__('os')"}, ctx) assert not r.ok assert r.error_code == "validation_error" @pytest.mark.asyncio async def test_calc_division_by_zero_returns_error(): tool = CalcTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"expression": "1/0"}, ctx) assert not r.ok assert r.error_code == "evaluation_error" @pytest.mark.asyncio async def test_random_choice_deterministic_with_same_step(): """Same world_id + step_id → same choice.""" tool = RandomChoiceTool() ctx1 = ToolContext(db=None, world=_FakeWorld(), step_id="00000000-0000-0000-0000-000000000010") ctx2 = ToolContext(db=None, world=_FakeWorld(), step_id="00000000-0000-0000-0000-000000000010") r1 = await tool.execute({"options": ["a", "b", "c"]}, ctx1) r2 = await tool.execute({"options": ["a", "b", "c"]}, ctx2) assert r1.ok and r2.ok assert r1.data["choice"] == r2.data["choice"] @pytest.mark.asyncio async def test_random_choice_weights(): """Weighted choice should always pick the heavy option when others have weight 0.""" tool = RandomChoiceTool() ctx = ToolContext(db=None, world=_FakeWorld(), step_id="00000000-0000-0000-0000-000000000020") for _ in range(10): r = await tool.execute({ "options": ["always", "never"], "weights": [1, 0], }, ctx) assert r.ok assert r.data["choice"] == "always" @pytest.mark.asyncio async def test_random_choice_requires_two_options(): tool = RandomChoiceTool() ctx = ToolContext(db=None, world=_FakeWorld()) r = await tool.execute({"options": ["only"]}, ctx) assert not r.ok