fix: append index/key to forEach step names on eval failure#981
Conversation
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Test assertions: The new tests use
assertEquals(arr.includes(x), true)— consider usingassert(arr.includes(x))from@std/assertfor slightly cleaner assertion messages on failure (the current form reportsfalse !== truerather than showing the missing value). Non-blocking. -
Code duplication: The array and object branches share nearly identical eval-failure logic (flag + suffix + warning). This is pre-existing duplication that the PR inherits — not something to address here, but worth noting for a future refactor if the forEach expansion logic grows further.
Overall: Clean, focused fix with good test coverage. The defensive fallback of appending index/key on eval failure is a sensible approach that prevents the CyclicDependencyError without masking the underlying misconfiguration (thanks to the warning log).
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
-
Partial eval collision — theoretical but harmless (
execution_service.ts:1439,1502): If a step name has multiple expressions and only some fail, the expanded name is a hybrid likeresolvedValue-${{ self.item.missing }}-0. The index/key suffix guarantees uniqueness within the loop, so this is correct. However, the unexpanded${{ }}tokens remain in the name, which could look odd in logs/UI. This is a pre-existing behavior (the catch block already returned_matchbefore this PR), so not introduced here — just noting it. -
No test for partial expression failure (
execution_service_test.ts): The tests cover the case where the only expression fails. There's no test for a step name like${{ self.item.validField }}-${{ self.item.missingField }}where one expression succeeds and another fails. The code handles this correctly (thehadEvalFailureflag catches it), but a test would provide extra confidence.
Low
- Redundant suffix when partial eval already produces unique names (
execution_service.ts:1438-1439): If items are[{a: "x"}, {a: "y"}]and the step name is${{ self.item.a }}-${{ self.item.missing }}, theapart already makes names unique (x-...-0vsy-...-1). The index suffix is redundant but harmless — correctness over cleverness is the right call here.
Verdict
PASS — The fix is minimal, correct, and well-tested. It addresses the exact root cause (duplicate expanded names → cyclic dependency error) with a straightforward uniqueness suffix. Both the array and object iteration paths are covered symmetrically. No new failure modes introduced.
42140c9 to
aa9f42b
Compare
…n fails When CEL expression evaluation fails for forEach step names (e.g., referencing a missing property like `self.item.missingField`), all items produced the same unexpanded template string, causing duplicate names that triggered CyclicDependencyError in TopologicalSortService. The fix tracks evaluation failures during the replace callback and appends the loop index (array iteration) or object key (object iteration) to the expanded name as a uniqueness suffix. A warning is logged so the misconfiguration is visible to users. Fixes #975 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
aa9f42b to
9947052
Compare
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
-
Minor test style: The tests use
assertEquals(executor.executedSteps.includes("..."), true)— using a directassert()or a helper likeassertArrayIncludeswould give clearer failure messages if a test ever breaks (e.g., showing whatexecutedStepsactually contained). Non-blocking. -
Pre-existing duplication: The expression-evaluation-and-expand logic is nearly identical between the array and object iteration branches (~30 lines each). This PR correctly applies the fix to both, but extracting a shared helper (e.g.,
expandStepNameExpressions(stepName, context, celEvaluator, suffix)) would reduce future maintenance surface. This is out of scope for this bug fix PR.
Overall: clean, focused fix with good regression tests. LGTM.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
Partial expression failure with colliding successful evaluations (
execution_service.ts:1438-1439,1501-1502): If a step name has multiple expressions like${{ self.item.a }}-${{ self.item.b }}and onlybfails whileaevaluates to the same value for different items, the unexpanded portion${{ self.item.b }}is identical across items, and the expanded portion fromais also identical — producing duplicate names before the index suffix is applied. The fix does handle this correctly becausehadEvalFailureis set totrueand the index/key suffix is appended. However, ifasucceeds with different values per item butbfails, the names are already unique without the suffix — the suffix is harmlessly redundant. This is a cosmetic over-correction, not a bug. -
Successful evaluation producing duplicate names is not addressed (pre-existing): If expressions evaluate successfully but produce identical values (e.g., items
[{name: "foo"}, {name: "foo"}]with stepprocess-${{ self.item.name }}), both getprocess-foowith no index suffix. This would still triggerCyclicDependencyError. This is out of scope for this PR (no regression), but worth noting as a remaining gap.
Low
- Code duplication between array and object branches (
execution_service.ts:1424-1446vs1487-1509): The eval-failure logic is copy-pasted between the two branches. If the logic needs to change (e.g., to address Medium #2 above), both branches must be updated in lockstep. Not a correctness issue today.
Verdict
PASS — The fix is minimal, correctly targeted, and well-tested. The hadEvalFailure flag reliably detects when CEL evaluation throws, and appending the loop index (array) or object key (object) guarantees unique expanded names in the failure case. The three regression tests cover the key scenarios. No critical or high severity issues found.
…ansion
Extracts the duplicated ${{ }} expression resolution logic from both
the array and object iteration branches in expandForEachSteps() into a
shared resolveForEachStepName() helper. The helper incorporates the
eval-failure tracking from #981, appending the fallback suffix when any
expression fails to evaluate.
Fixes #978
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ansion
Extracts the duplicated ${{ }} expression resolution logic from both
the array and object iteration branches in expandForEachSteps() into a
shared resolveForEachStepName() helper. The helper incorporates the
eval-failure tracking from #981, appending the fallback suffix when any
expression fails to evaluate.
Fixes #978
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
self.item.missingFieldon items that lack that property), all iterations produced the same unexpanded template string, causing duplicateexpandedNamevalues that triggeredCyclicDependencyErrorinTopologicalSortService.sort().replacecallback and, if so, appends the loop index (array iteration) or object key (object iteration) as a uniqueness suffix. A warning is also logged so the misconfiguration is visible to users.Fixes #975
Test Plan
expandForEachSteps: appends index when array item expression evaluation failsexpandForEachSteps: appends key when object item expression evaluation failsexpandForEachSteps: does not append index when expression evaluates successfullydeno check,deno lint,deno fmtpassdeno run compilesucceeds🤖 Generated with Claude Code