Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/specify_cli/workflows/steps/gate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,14 @@ def validate(self, config: dict[str, Any]) -> list[str]:
f"Gate step {config.get('id', '?')!r}: 'on_reject' must be "
f"'abort', 'skip', or 'retry'."
)
if on_reject in ("abort", "retry") and isinstance(options, list):
# Only inspect option text when every option is a string; otherwise the
# `o.lower()` below would raise AttributeError on a non-string option
# (already reported above) and break validate_workflow's never-raise contract.
if (
on_reject in ("abort", "retry")
and isinstance(options, list)
and all(isinstance(o, str) for o in options)
):
reject_choices = {"reject", "abort"}
if not any(o.lower() in reject_choices for o in options):
errors.append(
Expand Down
17 changes: 17 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,23 @@ def test_validate_invalid_on_reject(self):
})
assert any("on_reject" in e for e in errors)

def test_validate_non_string_options_does_not_raise(self):
"""Non-string options with on_reject=abort/retry must be REPORTED as an
error, not crash: the reject-choice check calls o.lower() on each option,
which previously raised AttributeError on a non-string option and broke
validate_workflow's 'return errors, never raise' contract."""
from specify_cli.workflows.steps.gate import GateStep

step = GateStep()
# on_reject defaults to "abort", which triggers the option-text check.
errors = step.validate({"id": "test", "message": "Review", "options": [123]})
assert any("must be strings" in e for e in errors)
# also with an explicit retry on_reject
errors = step.validate(
{"id": "test", "message": "Review", "options": [True], "on_reject": "retry"}
)
assert any("must be strings" in e for e in errors)

def test_interactive_prompt_renders_show_file(self, tmp_path, monkeypatch, capsys):
from specify_cli.workflows.steps.gate import GateStep
from specify_cli.workflows.base import StepContext, StepStatus
Expand Down