Skip to content

refactor: extract resolveForEachStepName helper to DRY up forEach expansion#980

Merged
stack72 merged 1 commit intomainfrom
extract-foreach-step-name-helper
Mar 31, 2026
Merged

refactor: extract resolveForEachStepName helper to DRY up forEach expansion#980
stack72 merged 1 commit intomainfrom
extract-foreach-step-name-helper

Conversation

@keeb
Copy link
Copy Markdown
Contributor

@keeb keeb commented Mar 31, 2026

Summary

  • Extracted resolveForEachStepName() helper to eliminate duplicated ${{ }} expression resolution logic across the array and object iteration branches in expandForEachSteps()
  • Both branches now call the shared helper, which evaluates all expressions via CEL with graceful fallback for failures, or appends a fallback suffix when no expressions are present
  • Added 7 unit tests covering single/multiple expression resolution, failing expression fallback, fallback suffix appending, nested object property access, and mixed resolved/failed expressions

Test Plan

  • 7 new resolveForEachStepName unit tests pass
  • All 30 existing execution_service tests pass (no behavioral changes)
  • Full test suite passes (3892 tests)
  • deno check, deno lint, deno fmt pass
  • deno run compile succeeds

Fixes #978

🤖 Generated with Claude Code

github-actions[bot]
github-actions bot previously approved these changes Mar 31, 2026
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, well-scoped refactoring that successfully DRYs up the forEach step name resolution logic.

Blocking Issues

None.

Suggestions

  1. Minor: The _match parameter in the regex callback uses an underscore prefix (convention for unused params) but is actually used in the catch branch as the fallback return value. Consider renaming to match to signal it's intentionally used. This is pre-existing from the original code, so non-blocking.

Overall: good extraction of a pure helper function with solid test coverage. The DDD boundaries are respected — the function lives alongside the domain service that uses it, and the tests import only from appropriate paths.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None.

Medium

None.

Low

  1. execution_service.ts:1467 — When item is undefined (e.g., sparse array or explicit undefined in the iterated collection), fallbackSuffix becomes "undefined", producing step names like "step-undefined". This is not a regression (original code had the same behavior), but it could produce confusing or duplicate step names if multiple items are undefined. Extremely unlikely in practice.

  2. execution_service.ts:1455-1468fallbackSuffix is always computed even when nameHasExpression is true and it will be ignored by resolveForEachStepName. This is trivially wasteful (a String() call) but has zero correctness impact.

Verdict

PASS — Clean extract-function refactor with no behavioral changes. All code paths in the extracted resolveForEachStepName are equivalent to the original inline logic. The regex, catch-and-preserve-verbatim fallback, and template-suffix fallback all match exactly. Test coverage is thorough across single/multiple expressions, failures, fallback suffixes, and nested property access.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Blocking Issues

  1. Lost warning logs for expression evaluation failures — The original code logged warnings when CEL expression evaluation failed in both the array-iteration and object-iteration branches. These warnings told users which step name had failing expressions and suggested checking their self.<itemName> references. The extracted resolveForEachStepName() helper doesn't log, so these warnings are silently dropped. This is a behavioral regression that hurts observability/debuggability.

    Array branch (removed):

    "forEach step '{stepName}' has expression(s) that failed to evaluate for item at index {index}. Appending index to prevent duplicate names. Check that the expression references valid properties on self.{itemName}."
    

    Object branch (removed):

    "forEach step '{stepName}' has expression(s) that failed to evaluate for key '{key}'. Appending key to prevent duplicate names. Check that the expression references valid properties on self.{itemName}."
    

    To fix: either have resolveForEachStepName() return a flag indicating whether a fallback occurred (so the caller can log with full context), or accept a logger/callback parameter. The caller has context (itemName, index/key) that the helper doesn't, so returning a { name: string, hadFallback: boolean } result object would preserve the clean extraction while keeping the warnings.

Suggestions

  1. DDD placement — The function is a pure stateless transformation, which fits well as a standalone domain helper. However, placing it at module scope in execution_service.ts (a 1500+ line file) slightly muddies the domain service boundary. If this function grows or gains siblings, consider extracting it to a dedicated value-object or utility module (e.g., step_name_resolver.ts). Not blocking since it's small and cohesive with its single caller.

  2. as string casts — Lines 989 and 993 use as string casts on the regex callback parameters (expr as string, _match as string). These were inherited from the original code, so not introduced by this PR, but they could be typed properly via the regex replace callback signature if you're touching this code anyway.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Medium

  1. Lost warning logs on expression evaluation failure (execution_service.ts:982-997)

    The old code logged warnings when CEL expression evaluation failed in both the array branch (warning about index fallback) and the object branch (warning about key fallback). The extracted resolveForEachStepName helper silently swallows failures — it preserves the raw expression and appends the fallback suffix, but never logs.

    Breaking example: A user misconfigures a forEach step name with ${{ self.ep.nonexistent }}. In the old code, they'd see a warning like "forEach step has expression(s) that failed to evaluate for item at index 2". In the new code, the name silently gets a fallback suffix with no log output, making it harder to debug why step names look wrong.

    Suggested fix: Add the warning log calls back in the calling code (in expandForEachSteps), conditioned on whether the returned name differs from what a successful resolution would produce — or have the helper return a richer result ({ name: string, hadFailure: boolean }) so callers can decide whether to log.

  2. Changed fallback suffix for primitive array items when expressions fail (execution_service.ts:1455-1468)

    The old array-branch code always used index as the fallback suffix when expression evaluation failed. The new code computes fallbackSuffix before calling the helper: for objects it uses String(index), but for primitives it uses String(item).

    Breaking example: Given items: ["a", "a"] where both items are identical strings and the expression ${{ self.missing }} fails to evaluate:

    • Old behavior: names are step-${{ self.missing }}-0 and step-${{ self.missing }}-1 (unique via index)
    • New behavior: names are step-${{ self.missing }}-a and step-${{ self.missing }}-a (duplicate!)

    Whether duplicate expanded names cause downstream issues depends on how they're consumed, but the old code was strictly more correct here by guaranteeing uniqueness via index.

    Suggested fix: For the expression-failure path in the array branch, always use String(index) as the fallback (matching old behavior). One way: compute a separate failureFallback = String(index) and pass it to the helper, or keep the current fallbackSuffix for the no-expression path and use the index for the expression-failure path.

Low

  1. PR description says "no behavioral changes" but there are two — the lost warnings and changed fallback suffix described above. Neither is catastrophic, but the claim should be corrected to avoid misleading reviewers.

Verdict

PASS — This is a clean DRY refactor with good test coverage. The two medium findings (lost logs, changed fallback suffix on duplicate primitive items with failing expressions) are worth addressing but don't block the merge since they only affect uncommon error/fallback paths.

…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>
@keeb keeb force-pushed the extract-foreach-step-name-helper branch from 9268780 to e577052 Compare March 31, 2026 02:12
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Blocking Issues

None.

Suggestions

  1. Minor: _match in the regex replacer callback (line 996) is prefixed with underscore suggesting it's unused, but it's actually used in the catch branch (line 1003). Consider renaming to match for clarity. This is carried over from the original code, so not a concern for this PR.

Clean refactoring that correctly extracts the duplicated expression resolution logic into a well-typed pure function with a proper ResolvedStepName value object. The 7 new unit tests provide good coverage of the extracted helper. Behavior is preserved across both array and object iteration branches. No DDD, security, or convention concerns.

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None.

Medium

None.

Low

  1. execution_service.ts:1474String(item) when item is undefined or null (both primitives per the type guard) produces "undefined" or "null" as a suffix, which could collide if an array contains both null and the string "null". This is pre-existing behavior unchanged by this refactor, not a regression.

  2. execution_service.ts:1474 — Similarly, String(item) for mixed-type arrays like [1, "1"] produces identical suffixes "1", causing duplicate step names. Again pre-existing, not introduced here.

Verdict

PASS — This is a clean, behavior-preserving extract-function refactoring. Every code path in the extracted resolveForEachStepName maps exactly to the original inline logic. The fallback suffix computation at the call sites correctly reproduces the original branching. Tests cover single/multiple expression resolution, failure fallback, no-expression paths, nested property access, and mixed success/failure. No regressions.

@stack72 stack72 merged commit 725548e into main Mar 31, 2026
10 checks passed
@stack72 stack72 deleted the extract-foreach-step-name-helper branch March 31, 2026 02:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

extract resolveStepNameExpressions helper to DRY up forEach expansion

2 participants