mirror of
https://github.com/github/spec-kit.git
synced 2026-08-03 06:26:30 +08:00
fix(workflows): raise a clear error, not a cryptic crash, on non-string filter args (#3522)
The `map`, `join`, and `contains` expression filters assumed their
argument was a string. A non-string argument — an authoring mistake such
as `| map(5)`, `| join(5)`, or `| contains(5)` — reached an operation
that only strings support and raised a cryptic exception that escaped the
evaluator entirely:
* `map(5)` -> `attr.split(".")` -> AttributeError
* `join(5)` -> `separator.join(...)` -> AttributeError
* `contains(5)` on a string value -> `x in str` -> TypeError
The engine wraps neither expression evaluation nor `step_impl.execute()`
in a try/except, so each of these took down the whole run with a message
that names none of the real problem.
Validate the argument type up front and raise a `ValueError` naming the
filter and the offending type instead, mirroring the strict argument
handling already in `from_json`. `contains` guards only the string-value
branch: for a list value, membership of any element type is legitimate
(`5 in [1, 2, 5]`), so that branch is intentionally left unguarded.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -586,6 +586,51 @@ class TestExpressions:
|
||||
with pytest.raises(ValueError, match="unknown filter 'upper'"):
|
||||
evaluate_expression("{{ inputs.text | upper('x') }}", ctx)
|
||||
|
||||
def test_filter_map_non_string_attr_raises(self):
|
||||
# A non-string attribute (authoring mistake like `map(5)`) must raise a
|
||||
# ValueError naming the problem, not leak the cryptic AttributeError
|
||||
# from attr.split() that would escape the evaluator and crash the run.
|
||||
import pytest
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"rows": [{"id": "a"}, {"id": "b"}]})
|
||||
with pytest.raises(ValueError, match="map: expected a string attribute name"):
|
||||
evaluate_expression("{{ inputs.rows | map(5) }}", ctx)
|
||||
|
||||
def test_filter_join_non_string_separator_raises(self):
|
||||
# A non-string separator (authoring mistake like `join(5)`) must raise a
|
||||
# ValueError, not leak the cryptic AttributeError from str.join.
|
||||
import pytest
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"tags": ["a", "b"]})
|
||||
with pytest.raises(ValueError, match="join: expected a string separator"):
|
||||
evaluate_expression("{{ inputs.tags | join(5) }}", ctx)
|
||||
|
||||
def test_filter_contains_non_string_arg_on_string_raises(self):
|
||||
# For a string value, `contains` requires a string argument: `x in y` on
|
||||
# a string needs a string left operand. A non-string argument must raise
|
||||
# a ValueError, not leak the cryptic TypeError that would crash the run.
|
||||
import pytest
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"text": "hello"})
|
||||
with pytest.raises(ValueError, match="contains: expected a string argument"):
|
||||
evaluate_expression("{{ inputs.text | contains(5) }}", ctx)
|
||||
|
||||
def test_filter_contains_non_string_arg_on_list_ok(self):
|
||||
# For a list value, membership of any element type is legitimate, so a
|
||||
# non-string argument stays valid and is not rejected.
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
ctx = StepContext(inputs={"nums": [1, 2, 5]})
|
||||
assert evaluate_expression("{{ inputs.nums | contains(5) }}", ctx) is True
|
||||
assert evaluate_expression("{{ inputs.nums | contains(9) }}", ctx) is False
|
||||
|
||||
def test_registered_filters_unaffected(self):
|
||||
# Regression: all five registered filters keep working unchanged.
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
|
||||
Reference in New Issue
Block a user