rebase
This commit is contained in:
166
tests/unit/test_state_validator.py
Normal file
166
tests/unit/test_state_validator.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Tests for `app.core.state_validator`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from app.core.state_validator import apply_patch, validate_state, validate_world
|
||||
|
||||
|
||||
def _character_schema():
|
||||
return [
|
||||
{
|
||||
"name": "player",
|
||||
"type": "object",
|
||||
"required": True,
|
||||
"properties": [
|
||||
{"name": "name", "type": "string", "required": True},
|
||||
{
|
||||
"name": "stats",
|
||||
"type": "object",
|
||||
"required": True,
|
||||
"properties": [
|
||||
{"name": "health", "type": "integer", "required": True, "min": 0, "max": 100},
|
||||
{"name": "mana", "type": "integer", "required": False, "min": 0, "max": 100},
|
||||
],
|
||||
},
|
||||
{"name": "inventory", "type": "array", "required": False},
|
||||
],
|
||||
},
|
||||
{"name": "current_location", "type": "string", "required": True},
|
||||
{
|
||||
"name": "plot_rails",
|
||||
"type": "object",
|
||||
"required": True,
|
||||
"properties": [
|
||||
{"name": "hooks", "type": "array", "required": True},
|
||||
{"name": "current_goals", "type": "array", "required": True},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def test_validate_state_ok():
|
||||
state = {
|
||||
"player": {
|
||||
"name": "Eric",
|
||||
"stats": {"health": 100, "mana": 10},
|
||||
"inventory": [],
|
||||
},
|
||||
"current_location": "Tavern",
|
||||
"plot_rails": {"hooks": [], "current_goals": ["survive"]},
|
||||
}
|
||||
ok, errors = validate_state(state, _character_schema())
|
||||
assert ok, errors
|
||||
|
||||
|
||||
def test_validate_state_missing_required():
|
||||
state = {"player": {}, "current_location": "Tavern"}
|
||||
ok, errors = validate_state(state, _character_schema())
|
||||
assert not ok
|
||||
assert any("player" in e for e in errors) or any("plot_rails" in e for e in errors)
|
||||
|
||||
|
||||
def test_validate_state_wrong_type():
|
||||
state = {
|
||||
"player": {"name": "Eric", "stats": {"health": "not a number"}},
|
||||
"current_location": "Tavern",
|
||||
"plot_rails": {"hooks": [], "current_goals": []},
|
||||
}
|
||||
ok, errors = validate_state(state, _character_schema())
|
||||
assert not ok
|
||||
assert any("health" in e for e in errors)
|
||||
|
||||
|
||||
def test_validate_state_range_violation():
|
||||
state = {
|
||||
"player": {"name": "Eric", "stats": {"health": 200, "mana": 10}},
|
||||
"current_location": "Tavern",
|
||||
"plot_rails": {"hooks": [], "current_goals": []},
|
||||
}
|
||||
ok, errors = validate_state(state, _character_schema())
|
||||
assert not ok
|
||||
assert any("health" in e for e in errors)
|
||||
|
||||
|
||||
def test_apply_patch_set_value():
|
||||
state = {"player": {"name": "Eric"}}
|
||||
new, errors = apply_patch(state, {"player.name": "Erik"})
|
||||
assert not errors
|
||||
assert new["player"]["name"] == "Erik"
|
||||
|
||||
|
||||
def test_apply_patch_inc():
|
||||
state = {"player": {"stats": {"health": 50}}}
|
||||
new, errors = apply_patch(state, {"player.stats.health": {"op": "inc", "by": -10}})
|
||||
assert not errors
|
||||
assert new["player"]["stats"]["health"] == 40
|
||||
|
||||
|
||||
def test_apply_patch_dec():
|
||||
state = {"player": {"stats": {"mana": 30}}}
|
||||
new, errors = apply_patch(state, {"player.stats.mana": {"op": "dec", "by": 5}})
|
||||
assert not errors
|
||||
assert new["player"]["stats"]["mana"] == 25
|
||||
|
||||
|
||||
def test_apply_patch_append_to_list():
|
||||
state = {"player": {"inventory": []}}
|
||||
new, errors = apply_patch(
|
||||
state,
|
||||
{"player.inventory": {"op": "append", "value": {"item_id": "sword_01", "qty": 1}}},
|
||||
)
|
||||
assert not errors
|
||||
assert new["player"]["inventory"] == [{"item_id": "sword_01", "qty": 1}]
|
||||
|
||||
|
||||
def test_apply_patch_remove_key():
|
||||
state = {"player": {"name": "Eric", "backstory": "mysterious"}}
|
||||
new, errors = apply_patch(state, {"player.backstory": {"op": "remove"}})
|
||||
assert not errors
|
||||
assert "backstory" not in new["player"]
|
||||
|
||||
|
||||
def test_apply_patch_creates_nested_path():
|
||||
state = {"player": {}}
|
||||
new, errors = apply_patch(state, {"player.stats": {"health": 100, "mana": 10}})
|
||||
assert not errors
|
||||
assert new["player"]["stats"]["health"] == 100
|
||||
|
||||
|
||||
def test_apply_patch_does_not_mutate_input():
|
||||
state = {"player": {"stats": {"health": 100}}}
|
||||
_ = apply_patch(state, {"player.stats.health": {"op": "inc", "by": -10}})
|
||||
# Original state unchanged
|
||||
assert state["player"]["stats"]["health"] == 100
|
||||
|
||||
|
||||
def test_validate_world_ok():
|
||||
world = {
|
||||
"name": "Test world",
|
||||
"language": "en",
|
||||
"schemas": [{"type": "character", "properties": []}],
|
||||
"environment_schema": _character_schema(),
|
||||
"environment": {
|
||||
"player": {"name": "Eric", "stats": {"health": 100, "mana": 10}, "inventory": []},
|
||||
"current_location": "Tavern",
|
||||
"plot_rails": {"hooks": [], "current_goals": [], "completed_goals": []},
|
||||
},
|
||||
"plot_rails": {"hooks": [], "current_goals": [], "completed_goals": []},
|
||||
"current_time": "day_1_hour_8",
|
||||
}
|
||||
ok, errors = validate_world(world)
|
||||
assert ok, errors
|
||||
|
||||
|
||||
def test_validate_world_invalid_time():
|
||||
world = {
|
||||
"name": "X",
|
||||
"language": "en",
|
||||
"schemas": [],
|
||||
"environment_schema": [],
|
||||
"environment": {},
|
||||
"plot_rails": {"hooks": [], "current_goals": [], "completed_goals": []},
|
||||
"current_time": "invalid_time",
|
||||
}
|
||||
ok, errors = validate_world(world)
|
||||
assert not ok
|
||||
assert any("time" in e.lower() for e in errors)
|
||||
Reference in New Issue
Block a user