diff --git a/src/specify_cli/workflows/overlays/merge.py b/src/specify_cli/workflows/overlays/merge.py index 5521b10a76..bf28a1f133 100644 --- a/src/specify_cli/workflows/overlays/merge.py +++ b/src/specify_cli/workflows/overlays/merge.py @@ -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) diff --git a/tests/workflows/test_overlay_merge.py b/tests/workflows/test_overlay_merge.py index 17790d6801..c924d1c271 100644 --- a/tests/workflows/test_overlay_merge.py +++ b/tests/workflows/test_overlay_merge.py @@ -162,6 +162,23 @@ def test_merge_steps_higher_priority_wins(self): 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")]