fix(workflows): preserve intra-overlay order for multiple insert_after edits (#3662)

_traverse_and_apply's insert_after loop iterated reversed(edits) over the
flat per-anchor edit list. The reversal is only meant to place a
higher-priority OVERLAY closer to the anchor (mirroring insert_before's
winner-closest behaviour), but reversing the flat list also flipped the
declared order of multiple insert_after edits authored within a SINGLE
overlay: [insert_after a->x, insert_after a->y] produced [a, y, x, b]
instead of [a, x, y, b]. insert_before (a forward loop) already preserves
order, so the two operations were asymmetric.

Group contiguous same-layer edits and reverse the GROUP order only, keeping
each overlay's own inserts in declared order. Cross-overlay priority is
unchanged (higher-priority overlay still lands closest to the anchor).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-23 18:27:29 +05:00
committed by GitHub
parent 0a7f288ae4
commit 4fc0a5b06e
2 changed files with 32 additions and 3 deletions

View File

@@ -292,9 +292,21 @@ def _traverse_and_apply(
cases[case_key] = _traverse_and_apply(case_steps, edits_by_anchor, sources)
result.append(step)
# Insert after (highest priority closest to anchor — reversed merge order).
for layer, edit in reversed(edits):
if edit.operation == "insert_after":
# Insert after: higher-priority overlays land closer to the anchor
# (reversed merge order), but a single overlay's own inserts must keep
# their declared order — mirroring the forward insert_before loop above.
# Reversing the whole flat list would also flip an overlay's own edits,
# so group contiguous same-layer edits and reverse the GROUP order only.
after_groups: list[list[tuple[OverlayLayer, OverlayEdit]]] = []
for layer, edit in edits:
if edit.operation != "insert_after":
continue
if after_groups and after_groups[-1][0][0] is layer:
after_groups[-1].append((layer, edit))
else:
after_groups.append([(layer, edit)])
for group in reversed(after_groups):
for layer, edit in group:
new_step = copy.deepcopy(edit.step)
_record_sources_recursively(new_step, layer.source, sources)
result.append(new_step)

View File

@@ -162,6 +162,23 @@ class TestMergeSteps:
ComposedStep("low-step", "project:low"),
]
def test_merge_steps_multiple_insert_after_same_overlay_preserves_order(self):
# Two insert_after edits from ONE overlay on the same anchor must keep
# their declared order (a, x, y, b) — mirroring insert_before. The old
# reversed(edits) over the flat list flipped them to (a, y, x, b).
base = [_step("a"), _step("b")]
overlay = Overlay(
id="ov1",
extends="wf",
priority=10,
edits=[
OverlayEdit("insert_after", "a", _step("x")),
OverlayEdit("insert_after", "a", _step("y")),
],
)
steps, _ = merge_steps(base, [_layer(overlay, "project:ov1")])
assert [s["id"] for s in steps] == ["a", "x", "y", "b"]
def test_merge_steps_replace_wins_over_insert(self):
"""Overlays apply to the original tree only; targeting an overlay-introduced step raises."""
base = [_step("a")]