rebase
This commit is contained in:
105
tests/unit/test_tools_calc_random.py
Normal file
105
tests/unit/test_tools_calc_random.py
Normal file
@@ -0,0 +1,105 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user