fix(workflows): make expression operator/literal parsing quote-aware (#3197)

_evaluate_simple_expression split on operator keywords using naive str.find/split, so a keyword INSIDE a quoted operand was treated as an operator: `inputs.mode == 'read and write'` split on the inner ' and ' and evaluated as `(inputs.mode == 'read) and (write')`. The literal short-circuit was also too greedy -- `'a' == 'b'` matched startswith("'")/endswith("'") and was stripped to the garbage truthy string `a' == 'b`, so `'done' == 'failed'` evaluated truthy and gated the wrong branch.

Add a quote/bracket-aware _find_top_level helper (mirroring the existing _split_top_level_commas) and use it for the and/or/comparison/in/not-in splits; tighten the literal short-circuit to fire only when the opening quote's match is the final char. The docstring already lists comparisons + and/or/not + in/not-in + string literals as supported, so this restores the documented contract.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-06-29 19:53:35 +05:00
committed by GitHub
parent fd185c1fd8
commit 2a9db1d350
2 changed files with 89 additions and 19 deletions

View File

@@ -286,6 +286,42 @@ class TestExpressions:
assert evaluate_expression('{{ [["a", "b"], "c"] }}', ctx) == [["a", "b"], "c"]
assert evaluate_expression("{{ [[1, 2], [3, 4]] }}", ctx) == [[1, 2], [3, 4]]
def test_operator_splitting_is_quote_aware(self):
from specify_cli.workflows.expressions import (
evaluate_condition,
evaluate_expression,
)
from specify_cli.workflows.base import StepContext
# An 'and'/'or'/'in' keyword INSIDE a quoted operand must not be treated
# as a boolean/membership operator: the comparison applies to the whole
# string literal.
ctx = StepContext(inputs={"mode": "read and write"})
assert evaluate_expression("{{ inputs.mode == 'read and write' }}", ctx) is True
assert evaluate_expression("{{ inputs.mode == 'read or write' }}", ctx) is False
# ...also when the quoted literal is on the left of the operator.
left_ctx = StepContext(inputs={"x": "approve or reject"})
assert evaluate_expression("{{ 'approve or reject' == inputs.x }}", left_ctx) is True
# membership against a literal that contains a keyword
assert evaluate_expression("{{ 'cat' in 'cat and dog' }}", StepContext()) is True
# Literal-vs-literal equality no longer mis-strips to a garbage string
# (previously `'done' == 'failed'` short-circuited to the truthy string
# "done' == 'failed").
assert evaluate_condition("{{ 'done' == 'failed' }}", StepContext()) is False
assert evaluate_condition("{{ 'done' == 'done' }}", StepContext()) is True
# A single quoted literal that itself contains operator text is preserved.
assert evaluate_expression("{{ 'a == b' }}", StepContext()) == "a == b"
assert evaluate_expression("{{ 'x and y' }}", StepContext()) == "x and y"
# Regression: ordinary (unquoted-keyword) parsing still works.
plain = StepContext(inputs={"a": 1, "b": 2, "mode": "read"})
assert evaluate_expression("{{ inputs.mode == 'read' }}", plain) is True
assert evaluate_expression("{{ inputs.a == 1 and inputs.b == 2 }}", plain) is True
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_filter_default(self):
from specify_cli.workflows.expressions import evaluate_expression
from specify_cli.workflows.base import StepContext