fix(workflows): namespace nested descendant step ids in loops/fan-out - #4338
Open
Noor-ul-ain001 wants to merge 1 commit into
Open
fix(workflows): namespace nested descendant step ids in loops/fan-out#4338Noor-ul-ain001 wants to merge 1 commit into
Noor-ul-ain001 wants to merge 1 commit into
Conversation
`while`/`do-while` loop bodies and `fan-out` templates namespace nested
step ids per iteration/item so logs and `state.step_results` entries stay
unique — but the namespacing only rewrote the id of the *immediate* child
step, not any descendant nested deeper (e.g. a `shell` step inside an `if`
inside a `while` body, or inside a `fan-out` template's `if`/`switch`
branch). That grandchild kept its bare, unnamespaced id across every
iteration/item, so each iteration/item silently overwrote the previous
one's entry in `state.step_results` under that same key — only the last
iteration's or item's result for that nested step ever survived, and no
per-iteration/per-item record of it ever existed.
This is also a correctness gap beyond bookkeeping: nested/template step ids
are deliberately exempted from the workflow's global id-uniqueness
validation, on the assumption that runtime namespacing makes any collision
safe. Since only the top-level child was actually namespaced, a step
nested one level deeper could collide with an unrelated step of the same
id elsewhere in the workflow and silently overwrite its result.
Fix: add `_rename_step_tree_ids`, which recursively rewrites every id in a
step's subtree (walking `then`/`else`/`steps`/`default`/`cases.*` — the
same nesting keys `overlays/merge.py` walks for step-tree attribution) and
returns a `{new_id: original_id}` map. Both the while/do-while loop body
and fan-out's `run_item` now use this helper instead of renaming only the
top-level id, and alias every renamed descendant's result back to its
original id (mirroring the existing single-level aliasing) so sibling
steps within the same iteration/item and code reading `steps.<id>.output`
after the loop/fan-out still see that iteration's/item's value.
## Test plan
- Added `test_while_loop_namespaces_nested_descendant_steps` and
`test_fan_out_namespaces_nested_descendant_steps` to
`tests/test_workflows.py::TestWorkflowEngine`: a `shell` step nested
inside an `if` inside a `while` body (and inside a `fan-out` template)
gets a distinct namespaced `state.step_results` entry per
iteration/item, while the unprefixed key still holds the latest value.
- Verified both fail without the fix (test-the-test): the namespaced keys
(`retry-loop:leaf:1`, `fan:leaf:0`, etc.) were simply absent, and
`step_results` only ever held the last iteration's/item's bare-keyed
entry — reproducing the exact bug.
- Ran the full `tests/test_workflows.py` suite: 926 passed, 20 pre-existing
Windows symlink-elevation failures (need admin rights, unrelated to this
change), 7 skipped. All `While`/`DoWhile`/`FanOut`/`FanOutConcurrency`
tests pass, including the concurrent-execution and per-thread context
isolation tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJHJ2dHP2RVCNncHqN8Qm9
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Initial loop iterations remain unnamespaced, while delayed and shared aliases break sibling references and concurrent fan-out isolation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds recursive runtime namespacing for nested workflow steps in loops and fan-out templates.
Changes:
- Adds recursive descendant ID rewriting and aliasing.
- Adds while and fan-out regression tests.
- Preserves namespaced execution results.
File summaries
| File | Description |
|---|---|
src/specify_cli/workflows/engine.py |
Recursively namespaces nested step IDs. |
tests/test_workflows.py |
Tests nested loop and fan-out results. |
Review details
Suppressed comments (1)
src/specify_cli/workflows/engine.py:1447
- These aliases are created only after the entire renamed subtree finishes. If a branch contains step
afollowed by stepbthat referencessteps.a,bexecutes beforeais aliased and reads the previous iteration's value (or no value), whereas both steps previously used their bare IDs. Alias each descendant immediately after that descendant completes so intra-branch references retain their existing semantics.
for new_id, orig_id in id_map.items():
if new_id in context.steps:
self._record_result(
context, state, orig_id,
context.steps[new_id],
)
- Files reviewed: 2/2 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+1428
to
+1431
| ns_copy, id_map = _rename_step_tree_ids( | ||
| ns, step_id, str(_loop_iter + 1), | ||
| default_id=f"step-{ns_idx}", | ||
| ) |
Comment on lines
+1553
to
+1555
| for new_id, orig_id in id_map.items(): | ||
| if new_id in item_ctx.steps: | ||
| self._record_result(item_ctx, state, orig_id, item_ctx.steps[new_id]) |
mnriem
requested
a balanced review from Copilot
and removed request for
Copilot
September 1, 2026 22:54
Collaborator
|
Please address Copilot feedback |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
while/do-whileloop bodies andfan-outtemplates namespace nested step ids per iteration/item so logs andstate.step_resultsentries stay unique — but the namespacing only rewrote the id of the immediate child step, not any descendant nested deeper (e.g. ashellstep inside anifinside awhilebody, or inside afan-outtemplate'sif/switchbranch). That grandchild kept its bare, unnamespaced id across every iteration/item, so each iteration/item silently overwrote the previous one's entry instate.step_resultsunder that same key — only the last iteration's or item's result for that nested step ever survived, and no per-iteration/per-item record of it ever existed._rename_step_tree_ids, which recursively rewrites every id in a step's subtree (walkingthen/else/steps/default/cases.*— the same nesting keysoverlays/merge.pywalks for step-tree attribution) and returns a{new_id: original_id}map. Both the while/do-while loop body and fan-out'srun_itemnow use this helper instead of renaming only the top-level id, and alias every renamed descendant's result back to its original id (mirroring the existing single-level aliasing) so sibling steps within the same iteration/item and code readingsteps.<id>.outputafter the loop/fan-out still see that iteration's/item's value.Test plan
test_while_loop_namespaces_nested_descendant_stepsandtest_fan_out_namespaces_nested_descendant_stepstotests/test_workflows.py::TestWorkflowEngine: ashellstep nested inside anifinside awhilebody (and inside afan-outtemplate) gets a distinct namespacedstate.step_resultsentry per iteration/item, while the unprefixed key still holds the latest value.retry-loop:leaf:1,fan:leaf:0, etc.) were simply absent, andstep_resultsonly ever held the last iteration's/item's bare-keyed entry — reproducing the exact bug.tests/test_workflows.pysuite: 926 passed, 20 pre-existing Windows symlink-elevation failures (need admin rights, unrelated to this change), 7 skipped. AllWhile/DoWhile/FanOut/FanOutConcurrencytests pass, including the concurrent-execution and per-thread context isolation tests.Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
https://claude.ai/code/session_01PJHJ2dHP2RVCNncHqN8Qm9