mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
fix: fail loudly when a fan-out 'items' expression does not resolve to a list (#2957)
A non-list result from the items expression is a wiring error (the template did not resolve to a collection); silently fanning out over zero items hides it until a confusing downstream failure. Fail the step with an error naming the expression instead. An explicit empty list remains valid input. Fixes #2956
This commit is contained in:
@@ -22,12 +22,28 @@ class FanOutStep(StepBase):
|
||||
def execute(self, config: dict[str, Any], context: StepContext) -> StepResult:
|
||||
items_expr = config.get("items", "[]")
|
||||
items = evaluate_expression(items_expr, context)
|
||||
if not isinstance(items, list):
|
||||
items = []
|
||||
|
||||
max_concurrency = config.get("max_concurrency", 1)
|
||||
step_template = config.get("step", {})
|
||||
|
||||
if not isinstance(items, list):
|
||||
# A non-list here is a wiring error (the expression did not
|
||||
# resolve to a collection); silently fanning out over zero
|
||||
# items hides it. An explicit empty list remains valid input.
|
||||
return StepResult(
|
||||
status=StepStatus.FAILED,
|
||||
error=(
|
||||
f"Fan-out step {config.get('id', '?')!r}: 'items' must "
|
||||
f"resolve to a list, got {type(items).__name__} from "
|
||||
f"{items_expr!r}."
|
||||
),
|
||||
output={
|
||||
"items": [],
|
||||
"max_concurrency": max_concurrency,
|
||||
"step_template": step_template,
|
||||
"item_count": 0,
|
||||
},
|
||||
)
|
||||
|
||||
return StepResult(
|
||||
status=StepStatus.COMPLETED,
|
||||
output={
|
||||
|
||||
@@ -1475,9 +1475,9 @@ class TestFanOutStep:
|
||||
assert result.output["item_count"] == 2
|
||||
assert result.output["max_concurrency"] == 3
|
||||
|
||||
def test_execute_non_list_items_resolves_empty(self):
|
||||
def test_execute_non_list_items_fails_loudly(self):
|
||||
from specify_cli.workflows.steps.fan_out import FanOutStep
|
||||
from specify_cli.workflows.base import StepContext
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = FanOutStep()
|
||||
ctx = StepContext()
|
||||
@@ -1487,8 +1487,24 @@ class TestFanOutStep:
|
||||
"step": {"id": "impl", "command": "speckit.implement"},
|
||||
}
|
||||
result = step.execute(config, ctx)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert "'items' must resolve to a list" in (result.error or "")
|
||||
assert result.output["item_count"] == 0
|
||||
|
||||
def test_execute_empty_list_items_is_valid(self):
|
||||
from specify_cli.workflows.steps.fan_out import FanOutStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
step = FanOutStep()
|
||||
ctx = StepContext(steps={"tasks": {"output": {"task_list": []}}})
|
||||
config = {
|
||||
"id": "parallel",
|
||||
"items": "{{ steps.tasks.output.task_list }}",
|
||||
"step": {"id": "impl", "command": "speckit.implement"},
|
||||
}
|
||||
result = step.execute(config, ctx)
|
||||
assert result.status == StepStatus.COMPLETED
|
||||
assert result.output["item_count"] == 0
|
||||
assert result.output["items"] == []
|
||||
|
||||
def test_validate_missing_fields(self):
|
||||
from specify_cli.workflows.steps.fan_out import FanOutStep
|
||||
|
||||
Reference in New Issue
Block a user