Skip to content

Commit d9c867b

Browse files
committed
fix(workflows): reject fan-in wait_for self-reference and non-string entries
Address review feedback on the fan-in wait_for validator: - A fan-in's own id is added to seen_ids before the wait_for check, so `wait_for: [<self>]` passed validation while producing a silent empty join at runtime. Reject self-references explicitly. - Non-string entries (e.g. YAML `wait_for: [123]`) were skipped by the isinstance(str) guard and validated even though they can never match a real step id. Flag them as wiring errors. Add coverage for both cases.
1 parent ef68e50 commit d9c867b

2 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎src/specify_cli/workflows/engine.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,24 @@ def _validate_steps(
307307
wait_for = step_config.get("wait_for")
308308
if isinstance(wait_for, list):
309309
for wid in wait_for:
310-
if isinstance(wid, str) and wid not in seen_ids:
310+
if not isinstance(wid, str):
311+
# A non-string entry (e.g. YAML `wait_for: [123]`) can
312+
# never match a real step id, so the join is silently
313+
# empty at runtime — surface it as a wiring error.
314+
errors.append(
315+
f"Fan-in step {step_id!r}: 'wait_for' entries must "
316+
f"be step-id strings, got {type(wid).__name__} "
317+
f"({wid!r})."
318+
)
319+
elif wid == step_id:
320+
# The fan-in's own id is already in seen_ids by now, so
321+
# a self-reference would pass the membership check below
322+
# while still producing an empty join at runtime.
323+
errors.append(
324+
f"Fan-in step {step_id!r}: 'wait_for' references "
325+
f"itself; a fan-in cannot wait for its own results."
326+
)
327+
elif wid not in seen_ids:
311328
errors.append(
312329
f"Fan-in step {step_id!r}: 'wait_for' references "
313330
f"unknown or not-yet-declared step id {wid!r}."

‎tests/test_workflows.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,6 +2057,41 @@ def test_forward_reference_is_rejected(self):
20572057
"unknown or not-yet-declared step id 'later'" in e for e in errors
20582058
)
20592059

2060+
def test_self_reference_is_rejected(self):
2061+
# A fan-in's own id is in scope by the time it is validated, so a
2062+
# self-reference slips past the membership check while still producing
2063+
# an empty join at runtime.
2064+
errors = self._errors("""
2065+
workflow:
2066+
id: wf
2067+
name: wf
2068+
version: "1.0.0"
2069+
steps:
2070+
- id: collect
2071+
type: fan-in
2072+
wait_for: [collect]
2073+
""")
2074+
assert any(
2075+
"references itself" in e and "collect" in e for e in errors
2076+
)
2077+
2078+
def test_non_string_wait_for_entry_is_rejected(self):
2079+
# A non-string entry (e.g. YAML `wait_for: [123]`) can never match a
2080+
# real step id, so it must be flagged rather than silently ignored.
2081+
errors = self._errors("""
2082+
workflow:
2083+
id: wf
2084+
name: wf
2085+
version: "1.0.0"
2086+
steps:
2087+
- id: collect
2088+
type: fan-in
2089+
wait_for: [123]
2090+
""")
2091+
assert any(
2092+
"must be step-id strings" in e and "int" in e for e in errors
2093+
)
2094+
20602095

20612096
# ===== Workflow Definition Tests =====
20622097

0 commit comments

Comments
 (0)