Skip to content
Open
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
13 changes: 13 additions & 0 deletions src/specify_cli/workflows/steps/switch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ def validate(self, config: dict[str, Any]) -> list[str]:
f"Switch step {config.get('id', '?')!r} is missing "
f"'expression' field."
)
# Every other control-flow step requires its branch payload: ``if``
# requires ``then``, ``fan-out`` requires ``items`` and ``step``,
# ``fan-in`` a non-empty ``wait_for``, ``gate`` a ``message``. Without
# the same check, a switch whose ``cases:`` block is missing or mistyped
# (``case:`` is the obvious slip) validates clean and then reports
# COMPLETED with ``matched_case: "__default__"`` -- a default it may not
# even declare -- having dispatched nothing. That is the "silent empty
# result + COMPLETED" wiring bug the fan-in guard exists to prevent.
if "cases" not in config:
errors.append(
f"Switch step {config.get('id', '?')!r} is missing "
f"'cases' field."
)
cases = config.get("cases", {})
if not isinstance(cases, dict):
errors.append(
Expand Down
32 changes: 32 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3309,6 +3309,38 @@ def test_validate_missing_expression(self):
errors = step.validate({"id": "test", "cases": {}})
assert any("missing 'expression'" in e for e in errors)

def test_validate_missing_cases(self):
"""`cases` is the switch's branch payload and must be required.

Every other control-flow step requires its own: `if` requires `then`,
`fan-out` requires `items` and `step`, `fan-in` a non-empty `wait_for`,
`gate` a `message`. Without it, a `case:` typo validated clean and then
reported COMPLETED with `matched_case: "__default__"` having dispatched
nothing.
"""
from specify_cli.workflows.steps.switch import SwitchStep

step = SwitchStep()

# Absent entirely.
errors = step.validate({"id": "route", "expression": "{{ inputs.x }}"})
assert any("missing 'cases'" in e for e in errors), errors

# The realistic slip: `case:` instead of `cases:`.
errors = step.validate(
{"id": "route", "expression": "{{ inputs.x }}", "case": {"a": []}}
)
assert any("missing 'cases'" in e for e in errors), errors

def test_validate_accepts_an_empty_cases_mapping(self):
"""An explicitly declared but empty `cases:` is still a declaration."""
from specify_cli.workflows.steps.switch import SwitchStep

errors = SwitchStep().validate(
{"id": "route", "expression": "{{ inputs.x }}", "cases": {}}
)
assert not any("missing 'cases'" in e for e in errors), errors

def test_validate_invalid_cases_and_default(self):
from specify_cli.workflows.steps.switch import SwitchStep

Expand Down