fix(loaders): non-list features and blank/non-string entries fail cleanly
features was popped and only re-added when it was a list, so a bare string (or dict/null) loaded with zero feats - silent data loss. Now a present non-list features raises CombatantLoadError, and the list resolution adopts an accept-string-or-error contract: non-string or blank entries raise instead of being silently dropped.
This commit is contained in:
@@ -76,22 +76,24 @@ class TestResolveFeat:
|
||||
|
||||
|
||||
class TestResolveFeatures:
|
||||
def test_filters_empty_and_whitespace(self) -> None:
|
||||
result = _resolve_features(["Power Attack", "", " ", "Dodge"], 1)
|
||||
assert len(result) == 2
|
||||
assert result[0] is POWER_ATTACK
|
||||
assert result[1] is DODGE
|
||||
def test_blank_entry_raises(self, tmp_path: Path) -> None:
|
||||
with pytest.raises(CombatantLoadError, match="non-empty feat name strings"):
|
||||
_resolve_features(["Power Attack", "", " ", "Dodge"], 1, tmp_path)
|
||||
|
||||
def test_filters_non_strings(self) -> None:
|
||||
def test_non_string_entry_raises(self, tmp_path: Path) -> None:
|
||||
raw: list[Json] = ["Iron Will", 42, None, True]
|
||||
result = _resolve_features(raw, 1)
|
||||
assert len(result) == 1
|
||||
with pytest.raises(CombatantLoadError, match="non-empty feat name strings"):
|
||||
_resolve_features(raw, 1, tmp_path)
|
||||
|
||||
def test_empty_list(self) -> None:
|
||||
assert _resolve_features([], 1) == []
|
||||
def test_valid_strings_resolve(self, tmp_path: Path) -> None:
|
||||
result = _resolve_features(["Power Attack", "Dodge"], 1, tmp_path)
|
||||
assert result == [POWER_ATTACK, DODGE]
|
||||
|
||||
def test_all_unknown(self) -> None:
|
||||
result = _resolve_features(["Feat A", "Feat B"], 1)
|
||||
def test_empty_list(self, tmp_path: Path) -> None:
|
||||
assert _resolve_features([], 1, tmp_path) == []
|
||||
|
||||
def test_all_unknown(self, tmp_path: Path) -> None:
|
||||
result = _resolve_features(["Feat A", "Feat B"], 1, tmp_path)
|
||||
assert len(result) == 2
|
||||
assert all(f.effects == () for f in result)
|
||||
|
||||
@@ -159,6 +161,26 @@ class TestFeatures:
|
||||
c = load_combatant(_write(tmp_path, data))
|
||||
assert c.features == []
|
||||
|
||||
def test_string_features_raises(self, tmp_path: Path) -> None:
|
||||
data = _minimal_payload(features="Toughness")
|
||||
with pytest.raises(CombatantLoadError, match="features must be a list"):
|
||||
load_combatant(_write(tmp_path, data))
|
||||
|
||||
def test_dict_features_raises(self, tmp_path: Path) -> None:
|
||||
data = _minimal_payload(features={"a": "Toughness"})
|
||||
with pytest.raises(CombatantLoadError, match="features must be a list"):
|
||||
load_combatant(_write(tmp_path, data))
|
||||
|
||||
def test_null_features_raises(self, tmp_path: Path) -> None:
|
||||
data = _minimal_payload(features=None)
|
||||
with pytest.raises(CombatantLoadError, match="features must be a list"):
|
||||
load_combatant(_write(tmp_path, data))
|
||||
|
||||
def test_non_string_entry_raises(self, tmp_path: Path) -> None:
|
||||
data = _minimal_payload(features=["Toughness", 42])
|
||||
with pytest.raises(CombatantLoadError, match="non-empty feat name strings"):
|
||||
load_combatant(_write(tmp_path, data))
|
||||
|
||||
|
||||
class TestSpells:
|
||||
def test_spells_passed_through(self, tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user