fix(workflows): reject bool max_iterations in while/do-while validation (#3237)

* fix(workflows): reject bool max_iterations in while/do-while validation

while/do-while validate() checked 'not isinstance(max_iter, int) or max_iter < 1'. Since bool is a subclass of int, isinstance(True, int) is True and True < 1 is False, so 'max_iterations: true' passed validation and then ran as a single iteration (range(True) == range(1)) instead of being reported as a type error. Reject bools explicitly, matching the fail-fast-on-bool handling already used for number inputs and gate options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: assert empty error list for the valid do-while max_iterations case

Address Copilot review: the accepted-config assertion only checked that no error mentioned 'max_iterations', which could let an unrelated validation error pass unnoticed. For a known-good config, assert the entire error list is empty (consistent with the other validate tests in this file).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-06-30 20:17:05 +05:00
committed by GitHub
parent 6fb7e77b3e
commit 3571ba72d8
3 changed files with 29 additions and 2 deletions

View File

@@ -48,7 +48,10 @@ class DoWhileStep(StepBase):
)
max_iter = config.get("max_iterations")
if max_iter is not None:
if not isinstance(max_iter, int) or max_iter < 1:
# bool is a subclass of int, so isinstance(True, int) is True and
# True < 1 is False; reject bools explicitly so `max_iterations: true`
# is a type error rather than a silent single iteration.
if isinstance(max_iter, bool) or not isinstance(max_iter, int) or max_iter < 1:
errors.append(
f"Do-while step {config.get('id', '?')!r}: "
f"'max_iterations' must be an integer >= 1."

View File

@@ -55,7 +55,10 @@ class WhileStep(StepBase):
)
max_iter = config.get("max_iterations")
if max_iter is not None:
if not isinstance(max_iter, int) or max_iter < 1:
# bool is a subclass of int, so isinstance(True, int) is True and
# True < 1 is False; reject bools explicitly so `max_iterations: true`
# is a type error rather than a silent single iteration.
if isinstance(max_iter, bool) or not isinstance(max_iter, int) or max_iter < 1:
errors.append(
f"While step {config.get('id', '?')!r}: "
f"'max_iterations' must be an integer >= 1."

View File

@@ -1822,6 +1822,12 @@ class TestWhileStep:
step = WhileStep()
errors = step.validate({"id": "test", "condition": "{{ true }}", "max_iterations": 0, "steps": []})
assert any("must be an integer >= 1" in e for e in errors)
# bool is an int subclass; `max_iterations: true` must be rejected, not
# silently treated as a single iteration.
bool_errors = step.validate(
{"id": "test", "condition": "{{ true }}", "max_iterations": True, "steps": []}
)
assert any("must be an integer >= 1" in e for e in bool_errors)
class TestDoWhileStep:
@@ -1861,6 +1867,21 @@ class TestDoWhileStep:
assert len(result.next_steps) == 1
assert result.output["max_iterations"] == 5
def test_validate_rejects_bool_max_iterations(self):
from specify_cli.workflows.steps.do_while import DoWhileStep
step = DoWhileStep()
# bool is an int subclass; `max_iterations: true` must be rejected.
errors = step.validate(
{"id": "test", "condition": "{{ true }}", "max_iterations": True, "steps": []}
)
assert any("must be an integer >= 1" in e for e in errors)
# a real positive integer is fully valid (no errors at all).
ok = step.validate(
{"id": "test", "condition": "{{ true }}", "max_iterations": 3, "steps": []}
)
assert ok == [], ok
def test_execute_empty_steps(self):
from specify_cli.workflows.steps.do_while import DoWhileStep
from specify_cli.workflows.base import StepContext