From 73093954e283f700cb772b1719f552c0113d824a Mon Sep 17 00:00:00 2001 From: Noor ul ain Date: Tue, 14 Jul 2026 18:07:33 +0500 Subject: [PATCH] fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447) (#3468) * fix(workflows): evaluate 'in'/'not in' safely on a non-iterable right operand (#3447) The `in` / `not in` operators in `_evaluate_simple_expression` only guarded `right is not None`, but `left in right` also raises `TypeError` for any other non-iterable right operand (int, bool, float). So a workflow condition like `{{ inputs.tag in inputs.count }}` where `count` is a number leaked a raw `TypeError: argument of type 'int' is not iterable` and crashed the whole run, instead of evaluating like the None case beside it. This was asymmetric with `_safe_compare`, which already swallows `TypeError` and returns False for the ordering operators. Add a `_safe_contains` helper (mirroring `_safe_compare`) that treats both a None and a non-container right operand as "nothing is contained": `in` -> False, `not in` -> True. Add a regression test covering int/bool/float/None right operands and asserting genuine containment against iterables still works. Co-Authored-By: Claude Opus 4.8 (1M context) * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(workflows): address review feedback on #3468 #3447 was fixed independently by #3448 (merged first), which added the same _safe_membership helper this branch introduced. Per Copilot review: - Revert the redundant _safe_contains rename in expressions.py so the file matches main; the working membership guard already lives there. - Drop the duplicate test_in_operator_non_iterable_right_operand test and fold its only new coverage (not in against float/bool/None right operands, which the base test only checked for the int case) into the existing test_membership_against_non_iterable_is_false_not_error. Also merges latest upstream/main into the branch. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/test_workflows.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_workflows.py b/tests/test_workflows.py index d5a6d2697..d129257db 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -475,11 +475,11 @@ class TestExpressions: # previous `right is not None` guard and mirrors _safe_compare, which # already swallows TypeError for the ordering operators. ctx = StepContext(inputs={"tag": "x", "count": 5, "ratio": 1.5, "flag": True}) - assert evaluate_expression("{{ inputs.tag in inputs.count }}", ctx) is False - assert evaluate_expression("{{ inputs.tag not in inputs.count }}", ctx) is True - assert evaluate_expression("{{ 'a' in inputs.ratio }}", ctx) is False - assert evaluate_expression("{{ 'a' in inputs.flag }}", ctx) is False - assert evaluate_expression("{{ inputs.tag in inputs.missing }}", ctx) is False + # `in` -> False and `not in` -> True for every non-iterable right + # operand (int, float, bool, None), so neither operator can drift. + for right in ("count", "ratio", "flag", "missing"): + assert evaluate_expression(f"{{{{ inputs.tag in inputs.{right} }}}}", ctx) is False + assert evaluate_expression(f"{{{{ inputs.tag not in inputs.{right} }}}}", ctx) is True # A condition that would otherwise crash the run now evaluates cleanly. assert evaluate_condition("{{ inputs.tag in inputs.count }}", ctx) is False