|
| 1 | +package coordinator |
| 2 | + |
| 3 | +import ( |
| 4 | + "testing" |
| 5 | + |
| 6 | + agentloop "codeflux.dev/codeflux/internal/agent" |
| 7 | +) |
| 8 | + |
| 9 | +// TestAStepTheRecordedPlanDoesNotKnowIsDropped is the silent mismatch that |
| 10 | +// ended two ladder passes. |
| 11 | +// |
| 12 | +// Adoption maps each step's local identity onto the one the store recorded. A |
| 13 | +// step with no mapping used to be kept with its local identity, and the store |
| 14 | +// then refuses it on every read: "load plan step state: database constraint: |
| 15 | +// step does not belong to run plan". That arrives mid-attempt, names no step, |
| 16 | +// and reaches the coordinator as an unrecognised loop error — which costs an |
| 17 | +// attempt each time and, three times over, the run. |
| 18 | +// |
| 19 | +// Ladder rung 18 on 2026-08-04 lost two whole passes to it, and the trace said |
| 20 | +// only "the loop refused the attempt". |
| 21 | +func TestAStepTheRecordedPlanDoesNotKnowIsDropped(t *testing.T) { |
| 22 | + steps := []agentloop.PlanStep{ |
| 23 | + {ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"}, |
| 24 | + } |
| 25 | + plan := durablePlan{Steps: map[string]string{ |
| 26 | + "edit-1": "stp_one", |
| 27 | + "verify": "stp_verify", |
| 28 | + }} |
| 29 | + |
| 30 | + adopted := adoptDurablePlanSteps(steps, plan) |
| 31 | + if len(adopted) != 2 { |
| 32 | + t.Fatalf("want the two recorded steps, got %d: %+v", |
| 33 | + len(adopted), adopted) |
| 34 | + } |
| 35 | + for _, step := range adopted { |
| 36 | + if step.ID == "edit-2" { |
| 37 | + t.Error("a step the plan never recorded survived adoption, so " + |
| 38 | + "every read of its state will be refused by the store") |
| 39 | + } |
| 40 | + } |
| 41 | + if adopted[0].ID != "stp_one" || adopted[1].ID != "stp_verify" { |
| 42 | + t.Errorf("the recorded steps did not take their durable identities: %+v", |
| 43 | + adopted) |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +// TestEveryMappedStepIsKept is the control. |
| 48 | +// |
| 49 | +// The ordinary case is that every step maps, and dropping must not become a way |
| 50 | +// to lose work: a plan that records all its steps has to come back whole and in |
| 51 | +// order. |
| 52 | +func TestEveryMappedStepIsKept(t *testing.T) { |
| 53 | + steps := []agentloop.PlanStep{ |
| 54 | + {ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"}, |
| 55 | + } |
| 56 | + plan := durablePlan{Steps: map[string]string{ |
| 57 | + "edit-1": "stp_one", "edit-2": "stp_two", "verify": "stp_verify", |
| 58 | + }} |
| 59 | + |
| 60 | + adopted := adoptDurablePlanSteps(steps, plan) |
| 61 | + if len(adopted) != 3 { |
| 62 | + t.Fatalf("a fully recorded plan lost a step: %+v", adopted) |
| 63 | + } |
| 64 | + for index, want := range []string{"stp_one", "stp_two", "stp_verify"} { |
| 65 | + if adopted[index].ID != want { |
| 66 | + t.Errorf("step %d is %q, want %q", index, adopted[index].ID, want) |
| 67 | + } |
| 68 | + } |
| 69 | +} |
| 70 | + |
| 71 | +// TestAnUnrecordedPlanLeavesTheStepsAlone keeps the no-plan path working. |
| 72 | +// |
| 73 | +// When nothing was recorded at all — the store refused the plan, or there is no |
| 74 | +// store — the run proceeds on its own identities. Dropping every step there |
| 75 | +// would turn a degraded run into no run. |
| 76 | +func TestAnUnrecordedPlanLeavesTheStepsAlone(t *testing.T) { |
| 77 | + steps := []agentloop.PlanStep{{ID: "edit-1"}, {ID: "verify"}} |
| 78 | + if adopted := adoptDurablePlanSteps(steps, durablePlan{}); len(adopted) != 2 { |
| 79 | + t.Errorf("a run with no recorded plan lost its steps: %+v", adopted) |
| 80 | + } |
| 81 | +} |
0 commit comments