fix: Chain iterations after nested loop expansion (gt-zn35j)

Fixes a bug where outer iteration chaining was lost when nested loops
were expanded. The problem was:

1. chainLoopIterations set: outer.iter2.inner depends on outer.iter1.inner
2. ApplyLoops recursively expanded nested loops
3. outer.iter2.inner became outer.iter2.inner.iter1.work, etc.
4. The dependency was lost (referenced non-existent ID)

The fix:
- Move recursive ApplyLoops BEFORE chaining
- Add chainExpandedIterations that finds iteration boundaries by ID prefix
- Works with variable step counts per iteration (nested loops expand differently)

Now outer.iter2's first step correctly depends on outer.iter1's LAST step,
ensuring sequential execution of outer iterations.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-25 16:35:28 -08:00
parent bce4f8f2d4
commit d2f71773cb
2 changed files with 121 additions and 6 deletions

View File

@@ -668,3 +668,66 @@ func TestApplyLoops_ThreeLevelNesting(t *testing.T) {
t.Errorf("Last step ID wrong: %s", result[7].ID)
}
}
func TestApplyLoops_NestedLoopsOuterChaining(t *testing.T) {
// Verify that outer iterations are chained AFTER nested loop expansion.
// outer.iter2's first step should depend on outer.iter1's LAST step.
steps := []*Step{
{
ID: "outer",
Title: "Outer loop",
Loop: &LoopSpec{
Count: 2,
Body: []*Step{
{
ID: "inner",
Title: "Inner loop",
Loop: &LoopSpec{
Count: 2,
Body: []*Step{
{ID: "work", Title: "Do work"},
},
},
},
},
},
},
}
result, err := ApplyLoops(steps)
if err != nil {
t.Fatalf("ApplyLoops failed: %v", err)
}
// Should have 4 steps
if len(result) != 4 {
t.Fatalf("Expected 4 steps, got %d", len(result))
}
// Expected order:
// 0: outer.iter1.inner.iter1.work
// 1: outer.iter1.inner.iter2.work (depends on above via inner chaining)
// 2: outer.iter2.inner.iter1.work (should depend on step 1 via outer chaining!)
// 3: outer.iter2.inner.iter2.work (depends on above via inner chaining)
// Verify outer chaining: step 2 should depend on step 1
step2 := result[2]
if step2.ID != "outer.iter2.inner.iter1.work" {
t.Fatalf("Step 2 ID wrong: %s", step2.ID)
}
// This is the key assertion: outer.iter2's first step must depend on
// outer.iter1's last step (outer.iter1.inner.iter2.work)
expectedDep := "outer.iter1.inner.iter2.work"
found := false
for _, need := range step2.Needs {
if need == expectedDep {
found = true
break
}
}
if !found {
t.Errorf("outer.iter2 first step should depend on outer.iter1 last step.\n"+
"Expected Needs to contain %q, got %v", expectedDep, step2.Needs)
}
}