fix(loaders): resource pools validated consistently before grant merge

_merge_resource_grants silently dropped non-int pool values when features
were present (a "5" pool vanished, leaving only the feat grant) while
Pydantic coerced the same value when features were absent. _validated_resources
now runs the JSON resources through TypeAdapter(dict[str, int]) -- the same
lax coercion as Combatant.resources -- so both paths coerce-or-fail
identically, and grants are added to the coerced ints.
This commit is contained in:
2026-09-20 17:13:09 +02:00
parent 9adc87c9e1
commit a8c7e37f8e
2 changed files with 100 additions and 33 deletions
+40
View File
@@ -221,6 +221,46 @@ class TestSpells:
assert c.spells == []
class TestResourcePools:
def test_string_pool_coerced_without_features(self, tmp_path: Path) -> None:
data = _minimal_payload(resources={"channel_energy": "5"})
c = load_combatant(_write(tmp_path, data))
assert c.resources == {"channel_energy": 5}
def test_string_pool_coerced_with_granting_feature(self, tmp_path: Path) -> None:
data = _minimal_payload(
resources={"channel_energy": "5"},
features=["Exalted of the Society (cleric, pathfinder society)"],
)
c = load_combatant(_write(tmp_path, data))
assert c.resources == {"channel_energy": 6}
def test_non_numeric_pool_value_raises_with_feature(self, tmp_path: Path) -> None:
data = _minimal_payload(
resources={"channel_energy": "lots"}, features=["Iron Will"]
)
with pytest.raises(CombatantLoadError, match="resources must be an object"):
load_combatant(_write(tmp_path, data))
def test_non_numeric_pool_value_raises_without_feature(self, tmp_path: Path) -> None:
data = _minimal_payload(resources={"channel_energy": "lots"})
with pytest.raises(CombatantLoadError, match="resources must be an object"):
load_combatant(_write(tmp_path, data))
def test_null_resources_raises(self, tmp_path: Path) -> None:
data = _minimal_payload(resources=None)
with pytest.raises(CombatantLoadError, match="resources must be an object"):
load_combatant(_write(tmp_path, data))
def test_grant_added_to_int_pool(self, tmp_path: Path) -> None:
data = _minimal_payload(
resources={"channel_energy": 2},
features=["Exalted of the Society (cleric, pathfinder society)"],
)
c = load_combatant(_write(tmp_path, data))
assert c.resources == {"channel_energy": 3}
class TestSpellSlots:
def test_numeric_string_keys_converted(self, tmp_path: Path) -> None:
data = _minimal_payload(spell_slots={"1": 4, "2": 3})