106 lines
3.2 KiB
Python
106 lines
3.2 KiB
Python
|
|
"""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
|