Skip to content

fix(workflows): require a cases block on switch steps - #4144

Open
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/switch-require-cases
Open

fix(workflows): require a cases block on switch steps#4144
jawwad-ali wants to merge 1 commit into
github:mainfrom
jawwad-ali:fix/switch-require-cases

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Problem

SwitchStep.validate requires expression and type-checks cases/default, but never checks that cases is present. It is the only control-flow step whose branch payload is optional:

step required branch payload
if then
fan-out items and step
fan-in non-empty wait_for
gate message
while / do-while condition
switch — none —

Reproduction on current main (bf88c9f)

sibling contrast:
  if   missing then : ["If step 'x' is missing 'then' field."]
  fanout missing all: ["Fan-out step 'y' is missing 'items' field.",
                       "Fan-out step 'y' is missing 'step' field ..."]

switch:
  typo `case:` not `cases:` -> []          <-- validates clean
  no cases key at all       -> []          <-- validates clean

  execute -> status=completed matched='__default__' next=[]

So a case:/cases: typo passes specify workflow validate with zero errors, then at run time reports COMPLETED with matched_case: "__default__" — a default it does not even declare — having dispatched nothing. The whole run reports success while the branch never executed.

That is precisely the "silent empty result + COMPLETED" wiring bug the fan-in wait_for guard and the engine's fan-in check exist to prevent.

Fix

One presence check, mirroring the sibling steps' wording.

Narrow. An explicitly declared but empty cases: {} is still a declaration and stays valid — pinned by test_validate_accepts_an_empty_cases_mapping. Only configs that never declared the block at all become errors, and those already do nothing at run time.

Verification

  • Fail-before / pass-after on a targeted scope (-k "Switch or validate or Validat"): 1 failed → 202 passed.
  • tests/workflows → 171 passed, 10 failed, matching the clean-main baseline exactly.
  • uvx ruff@0.15.0 check src tests → clean
  • Tests go into the existing TestSwitchStep class, next to test_validate_missing_expression.

A note on the full-file run. tests/test_workflows.py is intermittently flaky on Windows — a rotating handful of tests (TestFanOutConcurrency, TestContextWorkflowDir, TestContinueOnError, TestWorkflowRunExitCodes) fail on different runs and pass in isolation, from os.replace PermissionError. I confirmed this is unrelated to this change: test_sequential_and_concurrent_agree fails 2 of 4 runs on clean main and passes 4/4 with this patch applied, and the two other flagged tests pass in isolation. Hence the targeted verification above.


Written with assistance from Claude Code. Bug found, reproduced, and verified by me on current main.

`SwitchStep.validate` requires `expression` and type-checks `cases`, but
never checks that `cases` is PRESENT. It is the only control-flow step whose
branch payload is optional:

  if       -> requires 'then'
  fan-out  -> requires 'items' and 'step'
  fan-in   -> requires a non-empty 'wait_for'
  gate     -> requires 'message'
  switch   -> cases optional

So a switch whose branch table is absent or mistyped — `case:` for `cases:`
is the obvious slip — passes validation with zero errors:

  if   missing then : ["If step 'x' is missing 'then' field."]
  fanout missing all: ["Fan-out step 'y' is missing 'items' field.", ...]
  switch typo case: : []
  switch no cases   : []

and then at run time reports COMPLETED with
`matched_case: "__default__"` — a default it does not even declare — having
dispatched nothing, so the whole run "succeeds". That is the "silent empty
result + COMPLETED" wiring bug the fan-in guard exists to prevent.

An explicitly declared but empty `cases: {}` is still a declaration and
stays valid, pinned by a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jawwad-ali
jawwad-ali requested a review from mnriem as a code owner August 15, 2026 15:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant