fix(workflows): if-step validate accepts falsy non-list else (#3264)

* fix(workflows): if-step validate accepts falsy non-list else

IfThenStep.validate() guarded the 'else' branch with
'if else_branch and not isinstance(else_branch, list)'. The leading
truthiness check short-circuits for falsy non-list values (False, 0,
'', {}), so a malformed else-branch passes validation and is then
silently skipped at runtime. The sibling 'then' branch is validated
strictly; 'else' now matches by switching to an 'is not None' guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(workflows): cover explicit else:None and missing-else separately

Per Copilot feedback: the parametrized valid-else test omitted the
'else' key when the value was None, so it covered only the missing-else
case, not an explicit 'else: None'. Set 'else' explicitly (including
None) in the parametrized test and add a dedicated missing-else test, so
both accepted shapes are pinned.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-13 10:14:55 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7ff4522cf3
commit 55c66125f0
2 changed files with 42 additions and 2 deletions
@@ -47,8 +47,8 @@ class IfThenStep(StepBase):
errors.append(
f"If step {config.get('id', '?')!r}: 'then' must be a list of steps."
)
else_branch = config.get("else", [])
if else_branch and not isinstance(else_branch, list):
else_branch = config.get("else")
if else_branch is not None and not isinstance(else_branch, list):
errors.append(
f"If step {config.get('id', '?')!r}: 'else' must be a list of steps."
)
+40
View File
@@ -1988,6 +1988,46 @@ class TestIfThenStep:
errors = step.validate({"id": "test", "then": []})
assert any("missing 'condition'" in e for e in errors)
@pytest.mark.parametrize("bad_else", [False, 0, "", {}, 42])
def test_validate_rejects_non_list_else(self, bad_else):
"""A non-list 'else' must be rejected even when it is falsy.
The original guard used ``if else_branch and ...`` which
short-circuits for falsy non-list values (False/0/''/{}), letting a
malformed else-branch pass validation only to be silently skipped at
runtime. ``then`` is already strictly validated; ``else`` must match.
"""
from specify_cli.workflows.steps.if_then import IfThenStep
step = IfThenStep()
errors = step.validate(
{"id": "i", "condition": "true", "then": [], "else": bad_else}
)
assert any("'else' must be a list of steps" in e for e in errors)
@pytest.mark.parametrize("ok_else", [None, [], [{"id": "x", "command": "/y"}]])
def test_validate_accepts_valid_else(self, ok_else):
"""An explicit 'else' of None or a list stays valid.
``else`` is set explicitly here (including ``else: None``) so the
explicit-None case is exercised, not just the missing-key case.
"""
from specify_cli.workflows.steps.if_then import IfThenStep
step = IfThenStep()
errors = step.validate(
{"id": "i", "condition": "true", "then": [], "else": ok_else}
)
assert not any("'else'" in e for e in errors)
def test_validate_accepts_missing_else(self):
"""A missing 'else' key stays valid (no else branch)."""
from specify_cli.workflows.steps.if_then import IfThenStep
step = IfThenStep()
errors = step.validate({"id": "i", "condition": "true", "then": []})
assert not any("'else'" in e for e in errors)
class TestSwitchStep:
"""Test the switch step type."""