mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
fix(workflows): make pipe-filter detection quote-aware in expressions (#3232)
_evaluate_simple_expression used 'if "|" in expr' / expr.split("|", 1) to detect a filter pipe, so a literal '|' inside a quoted operand (e.g. inputs.x == 'a|b') was mistaken for a filter separator and raised a spurious ValueError ('unknown filter') instead of comparing the string. Use the existing quote/bracket-aware _find_top_level helper (added for the operator-splitting fix) so only a top-level pipe is treated as a filter separator.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -230,11 +230,13 @@ def _evaluate_simple_expression(expr: str, namespace: dict[str, Any]) -> Any:
|
||||
if expr[:1] in ("'", '"') and expr.find(expr[0], 1) == len(expr) - 1:
|
||||
return expr[1:-1]
|
||||
|
||||
# Handle pipe filters
|
||||
if "|" in expr:
|
||||
parts = expr.split("|", 1)
|
||||
value = _evaluate_simple_expression(parts[0].strip(), namespace)
|
||||
filter_expr = parts[1].strip()
|
||||
# Handle pipe filters. Detect the pipe at the top level only, so a literal
|
||||
# '|' inside a quoted operand (e.g. `inputs.x == 'a|b'`) or nested brackets is
|
||||
# not mistaken for a filter separator — mirroring the operator parsing below.
|
||||
pipe_idx = _find_top_level(expr, "|")
|
||||
if pipe_idx != -1:
|
||||
value = _evaluate_simple_expression(expr[:pipe_idx].strip(), namespace)
|
||||
filter_expr = expr[pipe_idx + 1:].strip()
|
||||
|
||||
# `from_json` is strict: it takes no arguments and tolerates no
|
||||
# trailing tokens. Match on the leading filter name and require the
|
||||
|
||||
@@ -322,6 +322,27 @@ class TestExpressions:
|
||||
assert evaluate_expression("{{ inputs.a == 9 or inputs.b == 2 }}", plain) is True
|
||||
assert evaluate_expression("{{ inputs.missing | default('a and b') }}", plain) == "a and b"
|
||||
|
||||
def test_pipe_detection_is_quote_aware(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
# A literal '|' inside a quoted operand must not be treated as a filter
|
||||
# pipe: the comparison applies to the whole string.
|
||||
ctx = StepContext(inputs={"x": "a|b"})
|
||||
assert evaluate_expression("{{ inputs.x == 'a|b' }}", ctx) is True
|
||||
assert evaluate_expression("{{ inputs.x == 'a|b' }}", StepContext(inputs={"x": "z"})) is False
|
||||
# membership against a literal containing a pipe
|
||||
assert evaluate_expression("{{ 'a|b' in inputs.s }}", StepContext(inputs={"s": "x a|b y"})) is True
|
||||
# a single quoted literal containing pipes is preserved
|
||||
assert evaluate_expression("{{ 'a|b|c' }}", StepContext()) == "a|b|c"
|
||||
|
||||
# Regression: real filters still work, including a pipe inside a filter arg.
|
||||
ctx2 = StepContext(inputs={"items": ["a", "b"], "s": "xabz"})
|
||||
assert evaluate_expression("{{ inputs.missing | default('y') }}", ctx2) == "y"
|
||||
assert evaluate_expression('{{ inputs.items | join("-") }}', ctx2) == "a-b"
|
||||
assert evaluate_expression("{{ inputs.s | contains('ab') }}", ctx2) is True
|
||||
assert evaluate_expression("{{ inputs.missing | default('a|b') }}", ctx2) == "a|b"
|
||||
|
||||
def test_filter_default(self):
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
from specify_cli.workflows.base import StepContext
|
||||
|
||||
Reference in New Issue
Block a user