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:
Ali jawwad
2026-06-30 00:55:45 +05:00
committed by GitHub
parent 3036fe6954
commit 9ece347a77
2 changed files with 28 additions and 5 deletions

View File

@@ -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

View File

@@ -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