Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/specify_cli/workflows/overlays/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions tests/workflows/test_overlay_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down