Skip to content

Commit 1422af0

Browse files
test(workflows): cover reserved-id collision through engine.execute()
The existing collision tests build StepContext(reserved_step_ids=...) by hand, so they bypass _collect_reserved_step_ids and its execute() wiring. Add an end-to-end test (sequential and concurrent) where an outside step and a fan-out template share the id `leaf`, asserting the outside result survives in persisted state and in the live context seen by a later `steps.leaf` reference. Verified it fails (both variants) when the reserved_step_ids wiring in execute() is removed. Assisted-by: Claude Code (model: claude-opus-5-5, autonomous) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 54a03a2 commit 1422af0

1 file changed

Lines changed: 54 additions & 0 deletions

File tree

‎tests/test_workflows.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7168,6 +7168,60 @@ def execute(self, config, context):
71687168
assert state.step_results["first"] == unrelated_result
71697169
assert context.steps["first"] == unrelated_result
71707170

7171+
@pytest.mark.parametrize("max_concurrency", [1, 2])
7172+
def test_fan_out_reserved_id_collision_end_to_end(
7173+
self, project_dir, max_concurrency
7174+
):
7175+
"""End-to-end counterpart of the `..._never_clobbers_unrelated_step`
7176+
tests above: those build `StepContext(reserved_step_ids=...)` by
7177+
hand, so they would still pass if `execute()` stopped wiring
7178+
`_collect_reserved_step_ids(definition.steps)` into the run context.
7179+
7180+
This drives a real workflow through `engine.execute()`: an outside
7181+
step `leaf` runs first, then a fan-out whose template reuses the id
7182+
`leaf` (allowed -- template ids are exempt from global uniqueness
7183+
validation). The outside result must survive in both persisted
7184+
state and the live context, as seen by a later step reading
7185+
`steps.leaf`, on both the sequential (1) and concurrent (2) paths.
7186+
"""
7187+
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
7188+
from specify_cli.workflows.base import RunStatus
7189+
7190+
yaml_str = f"""
7191+
schema_version: "1.0"
7192+
workflow:
7193+
id: "fan-out-reserved-collision"
7194+
name: "Fan Out Reserved Collision"
7195+
version: "1.0.0"
7196+
steps:
7197+
- id: leaf
7198+
type: shell
7199+
run: "echo outside"
7200+
- id: fan
7201+
type: fan-out
7202+
items: "{{{{ ['a', 'b', 'c'] }}}}"
7203+
max_concurrency: {max_concurrency}
7204+
step:
7205+
id: leaf
7206+
type: shell
7207+
run: "echo {{{{ item }}}}"
7208+
- id: after
7209+
type: shell
7210+
run: "echo {{{{ steps.leaf.output.stdout }}}}"
7211+
"""
7212+
definition = WorkflowDefinition.from_string(yaml_str)
7213+
engine = WorkflowEngine(project_dir)
7214+
state = engine.execute(definition)
7215+
7216+
assert state.status == RunStatus.COMPLETED
7217+
assert state.step_results["leaf"]["output"]["stdout"] == "outside\n"
7218+
for idx, item in enumerate(["a", "b", "c"]):
7219+
assert (
7220+
state.step_results[f"fan:leaf:{idx}"]["output"]["stdout"]
7221+
== f"{item}\n"
7222+
)
7223+
assert state.step_results["after"]["output"]["stdout"].strip() == "outside"
7224+
71717225
def test_fan_out_namespaces_nested_descendant_steps(self, project_dir):
71727226
"""A step nested inside a fan-out template's `if`/`switch` branch
71737227
must get a unique namespaced key per item, not just the template's

0 commit comments

Comments
 (0)