feat(engine): persistenza enigmi, inventario strutturato, HP mostri
test / test (push) Has been cancelled
test / test (push) Has been cancelled
Fase 2 del remediation plan: - set_puzzle_state / is_puzzle_solved per enigmi area - update_inventory su character_items con add/remove/equip/list - combat() legge HP da combat_state e persiste danno - dm_cycle inietta campaign_id nei tool che ne hanno bisogno - helpers db: dedup inventario, remove/equip per nome Fase 4 del remediation plan: - test integrazione dm_cycle con LLM mockato (5 test) - test WebSocket handler con mock (4 test) - CI minimo .gitea/workflows/test.yml
This commit is contained in:
1 parent
cb45172717
commit
6b048dc0cc
7 files changed
+386
-18
No files matched your search
@@ -0,0 +1,95 @@
|
||||
"""Integration test: dm_cycle con LLM mockato."""
|
||||
import json
|
||||
import sys
|
||||
sys.path.insert(0, "src")
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.engine import db, dm
|
||||
|
||||
MOCK_TOOL_RESP = json.dumps({
|
||||
"tool": "roll_dice",
|
||||
"params": {"die": "d20", "count": 1},
|
||||
"narration": "Tendi l'orecchio nel buio...",
|
||||
})
|
||||
|
||||
MOCK_COMBAT_RESP = json.dumps({
|
||||
"tool": "combat",
|
||||
"params": {
|
||||
"character_id": 1,
|
||||
"target_ac": 12,
|
||||
"attack_bonus": 2,
|
||||
"damage_die": "d8",
|
||||
},
|
||||
"narration": "Affronti il guardiano con la spada sguainata!",
|
||||
})
|
||||
|
||||
|
||||
def _setup_campaign():
|
||||
conn = db.init_db()
|
||||
camp_id = db.create_campaign("Test Integrazione", "Avventura")
|
||||
char_id = db.create_character(camp_id, "Eroe", "guerriero", "soldato")
|
||||
db.update_character(
|
||||
char_id,
|
||||
level=1, str=15, agi=11, sta=14,
|
||||
per=9, int=8, lck=10,
|
||||
hp_current=12, hp_max=12, ac=15,
|
||||
armor_type="light",
|
||||
)
|
||||
db.seed_adventure_state(camp_id)
|
||||
return camp_id, char_id
|
||||
|
||||
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_TOOL_RESP)
|
||||
def test_dm_cycle_returns_tool_params_narration(mock_ask):
|
||||
camp_id, _ = _setup_campaign()
|
||||
out = dm.dm_cycle(camp_id, "Provo a sentire rumori")
|
||||
|
||||
assert "tool" in out
|
||||
assert "params" in out
|
||||
assert "narration" in out
|
||||
assert out["tool"] == "roll_dice"
|
||||
assert out["params"]["die"] == "d20"
|
||||
assert "Tendi l'orecchio" in out["narration"]
|
||||
|
||||
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_TOOL_RESP)
|
||||
def test_dm_cycle_advances_turn(mock_ask):
|
||||
camp_id, _ = _setup_campaign()
|
||||
turn_before = db.get_campaign(camp_id)["turn"]
|
||||
out = dm.dm_cycle(camp_id, "Provo a sentire rumori")
|
||||
assert out["turn"] == turn_before + 1
|
||||
assert db.get_campaign(camp_id)["turn"] == turn_before + 1
|
||||
|
||||
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_TOOL_RESP)
|
||||
def test_dm_cycle_writes_log(mock_ask):
|
||||
camp_id, _ = _setup_campaign()
|
||||
out = dm.dm_cycle(camp_id, "Provo a sentire rumori")
|
||||
|
||||
log = db.get_log(camp_id, limit=1)
|
||||
assert len(log) >= 1
|
||||
assert log[0]["action"] == "Provo a sentire rumori"
|
||||
assert "Tendi l'orecchio" in log[0]["result"]
|
||||
|
||||
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_COMBAT_RESP)
|
||||
def test_dm_cycle_combat_injects_campaign_id(mock_ask):
|
||||
camp_id, char_id = _setup_campaign()
|
||||
# Replace character_id placeholder in mock with real ID
|
||||
resp = json.loads(MOCK_COMBAT_RESP)
|
||||
resp["params"]["character_id"] = char_id
|
||||
|
||||
with patch("src.engine.dm.llm_ask", return_value=json.dumps(resp)):
|
||||
out = dm.dm_cycle(camp_id, "Attacco il guardiano")
|
||||
assert out["tool"] == "combat"
|
||||
assert out["params"].get("campaign_id") == camp_id
|
||||
|
||||
|
||||
@patch("src.engine.dm.llm_ask", return_value="{}")
|
||||
def test_dm_cycle_fallback_on_empty_json(mock_ask):
|
||||
camp_id, _ = _setup_campaign()
|
||||
out = dm.dm_cycle(camp_id, "saluto il guardiano")
|
||||
# Fallback heuristic: should produce dm_override narrate
|
||||
assert out["tool"] in ("combat", "dm_override")
|
||||
assert out["full_output"]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""WebSocket test: test handler logica via mock WS."""
|
||||
import json
|
||||
import sys
|
||||
sys.path.insert(0, "src")
|
||||
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.engine import db
|
||||
|
||||
MOCK_RESP = json.dumps({
|
||||
"tool": "roll_dice",
|
||||
"params": {"die": "d20", "count": 1},
|
||||
"narration": "Tendi l'orecchio nel buio...",
|
||||
})
|
||||
|
||||
|
||||
def _setup_campaign():
|
||||
conn = db.init_db()
|
||||
camp_id = db.create_campaign("Test WS", "Avventura")
|
||||
char_id = db.create_character(camp_id, "Eroe", "guerriero", "soldato")
|
||||
db.update_character(
|
||||
char_id,
|
||||
level=1, str=15, agi=11, sta=14,
|
||||
per=9, int=8, lck=10,
|
||||
hp_current=12, hp_max=12, ac=15,
|
||||
armor_type="light",
|
||||
)
|
||||
db.seed_adventure_state(camp_id)
|
||||
return camp_id
|
||||
|
||||
|
||||
def _make_mock_ws():
|
||||
ws = AsyncMock()
|
||||
ws.send_json = AsyncMock()
|
||||
ws.send_text = AsyncMock()
|
||||
ws.close = AsyncMock()
|
||||
ws.receive_text = AsyncMock()
|
||||
return ws
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_RESP)
|
||||
async def test_ws_handler_game_loop(mock_ask):
|
||||
from src.web.server import game_ws
|
||||
|
||||
camp_id = _setup_campaign()
|
||||
ws = _make_mock_ws()
|
||||
ws.receive_text.side_effect = [
|
||||
json.dumps({"type": "action", "payload": "Provo ad ascoltare"}),
|
||||
Exception("WS chiuso"),
|
||||
]
|
||||
|
||||
try:
|
||||
await game_ws(ws, camp_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ws.accept.assert_awaited_once()
|
||||
|
||||
calls = [c[0][0] for c in ws.send_json.call_args_list]
|
||||
assert calls[0]["type"] == "state"
|
||||
resp = calls[1]
|
||||
assert resp["type"] == "response"
|
||||
assert resp["tool"] == "roll_dice"
|
||||
assert resp["turn"] == 1
|
||||
assert calls[2]["type"] == "state"
|
||||
assert calls[2]["campaign"]["turn"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_RESP)
|
||||
async def test_ws_handler_unknown_campaign(mock_ask):
|
||||
from src.web.server import game_ws
|
||||
|
||||
ws = _make_mock_ws()
|
||||
await game_ws(ws, 99999)
|
||||
|
||||
ws.accept.assert_awaited_once()
|
||||
ws.send_json.assert_awaited_once_with(
|
||||
{"type": "error", "message": "Campagna non trovata"}
|
||||
)
|
||||
ws.close.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_RESP)
|
||||
async def test_ws_handler_plain_text(mock_ask):
|
||||
from src.web.server import game_ws
|
||||
|
||||
camp_id = _setup_campaign()
|
||||
ws = _make_mock_ws()
|
||||
ws.receive_text.side_effect = [
|
||||
"Attacco il guardiano!",
|
||||
Exception("fine"),
|
||||
]
|
||||
|
||||
try:
|
||||
await game_ws(ws, camp_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
calls = [c[0][0] for c in ws.send_json.call_args_list]
|
||||
assert calls[0]["type"] == "state"
|
||||
assert calls[1]["type"] == "response"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("src.engine.dm.llm_ask", return_value=MOCK_RESP)
|
||||
async def test_ws_handler_error_boundary(mock_ask):
|
||||
from src.web.server import game_ws
|
||||
|
||||
camp_id = _setup_campaign()
|
||||
ws = _make_mock_ws()
|
||||
ws.receive_text.side_effect = [
|
||||
json.dumps({"type": "action", "payload": "test"}),
|
||||
Exception("fine"),
|
||||
]
|
||||
|
||||
with patch("src.web.server.dm_cycle", side_effect=[ValueError("Test errore")]):
|
||||
try:
|
||||
await game_ws(ws, camp_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
calls = [c[0][0] for c in ws.send_json.call_args_list]
|
||||
error_msgs = [c for c in calls if c.get("type") == "error"]
|
||||
assert len(error_msgs) >= 1
|
||||
Reference in new issue
Block a user