fix(workflows): fail fan-in loudly on a non-string wait_for entry (#3579)

`FanInStep.execute` already guards a non-list `wait_for` (#3482), and the
engine's load-time validation rejects non-string entries. But the engine does
not auto-validate step config, so on an unvalidated run `execute` iterated the
list's *elements* raw:

- An unhashable entry (a list/dict from a YAML indentation slip like
  `wait_for: [[a, b]]`) crashed the whole run at `context.steps.get(entry, ...)`
  with a raw `TypeError: cannot use 'list' as a dict key`.
- A hashable-but-non-string entry (`wait_for: [123]`) silently joined an empty
  `{}` and still reported COMPLETED — the exact "silent empty result +
  COMPLETED" wiring bug the whole-list guard and the engine's fan-in
  validation both exist to prevent.

Extend the execute() guard to reject any non-string entry with the engine's
"entries must be step-id strings" phrasing, mirroring the sibling non-list
guard right above it. Adds regression coverage for unhashable and
hashable-non-string entries.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Noor ul ain
2026-07-18 01:08:43 +05:00
committed by GitHub
parent c1e5cfa0aa
commit 3d2901eb75
2 changed files with 50 additions and 0 deletions

View File

@@ -2843,6 +2843,34 @@ class TestFanInStep:
assert "'wait_for' must be a list" in (result.error or "")
assert result.output["results"] == []
@pytest.mark.parametrize("bad_entry", [["a", "b"], {"a": 1}, 123, None])
def test_execute_non_string_wait_for_entry_fails_loudly(self, bad_entry):
"""A ``wait_for`` list with a non-string entry must fail the step, not
crash the run or silently produce a bogus join.
The whole-list guard (``test_execute_non_list_wait_for_fails_loudly``)
and the engine's fan-in validation both already reject the list *shape*,
but neither the step's ``execute`` nor the engine's runtime path guarded
the list's *elements*. On an unvalidated run an unhashable entry
(a list/dict from a YAML indentation slip like ``wait_for: [[a, b]]``)
crashed ``context.steps.get(entry, ...)`` with a raw TypeError, while a
hashable-but-non-string entry (``wait_for: [123]``) silently joined an
empty ``{}`` and still reported COMPLETED — the same wiring bug the
list-shape guard exists to prevent. Mirrors the engine's
``test_non_string_wait_for_entry_is_rejected`` load-time check.
"""
from specify_cli.workflows.steps.fan_in import FanInStep
from specify_cli.workflows.base import StepContext, StepStatus
step = FanInStep()
ctx = StepContext(steps={"a": {"output": {"x": 1}}})
# A valid entry alongside the bad one proves it is the entry, not the
# list, that is rejected.
result = step.execute({"id": "collect", "wait_for": ["a", bad_entry]}, ctx)
assert result.status == StepStatus.FAILED
assert "'wait_for' entries must be step-id strings" in (result.error or "")
assert result.output["results"] == []
def test_validate_empty_wait_for(self):
from specify_cli.workflows.steps.fan_in import FanInStep