|
| 1 | +package coordinator |
| 2 | + |
| 3 | +import ( |
| 4 | + "testing" |
| 5 | + |
| 6 | + agentloop "codeflux.dev/codeflux/internal/agent" |
| 7 | + "codeflux.dev/codeflux/internal/storage" |
| 8 | +) |
| 9 | + |
| 10 | +// TestStepIdentitiesArePairedWithWhatTheStoreHolds is the assumption behind an |
| 11 | +// error that names neither the step nor the cause. |
| 12 | +// |
| 13 | +// The run's step identities and the store's are different vocabularies, paired |
| 14 | +// by position. The pairing used to read the plan that was built to *send* to |
| 15 | +// the store rather than the one the store *returned*. Those are the same object |
| 16 | +// on an ordinary write and need not be on an idempotent one, where the store |
| 17 | +// answers with the revision it already had — and the identities the run then |
| 18 | +// adopts are ones the store never issued. |
| 19 | +// |
| 20 | +// Every later read of such a step is refused with "step does not belong to run |
| 21 | +// plan". That is a true sentence about a situation nothing else reports, and it |
| 22 | +// cost two whole ladder passes on 2026-08-04 before it was even legible. See |
| 23 | +// LAD-003. |
| 24 | +func TestStepIdentitiesArePairedWithWhatTheStoreHolds(t *testing.T) { |
| 25 | + steps := []agentloop.PlanStep{ |
| 26 | + {ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"}, |
| 27 | + } |
| 28 | + stored := []storage.AgentPlanStep{ |
| 29 | + {ID: "stp_a"}, {ID: "stp_b"}, {ID: "stp_c"}, |
| 30 | + } |
| 31 | + |
| 32 | + paired := pairStepIdentities(steps, stored) |
| 33 | + for local, want := range map[string]string{ |
| 34 | + "edit-1": "stp_a", "edit-2": "stp_b", "verify": "stp_c", |
| 35 | + } { |
| 36 | + if paired[local] != want { |
| 37 | + t.Errorf("%s paired to %q, want %q — the run would attribute its "+ |
| 38 | + "work to a step the store never issued", |
| 39 | + local, paired[local], want) |
| 40 | + } |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +// TestAShortRecordedPlanLeavesTheExtraStepsUnpaired is the case that has to |
| 45 | +// stay visible rather than silent. |
| 46 | +// |
| 47 | +// A step with no durable identity cannot be attributed to and every read of it |
| 48 | +// is refused. Pairing must not invent one, and the mismatch must not pass |
| 49 | +// unremarked — the whole cost of this defect was that it did. |
| 50 | +func TestAShortRecordedPlanLeavesTheExtraStepsUnpaired(t *testing.T) { |
| 51 | + steps := []agentloop.PlanStep{ |
| 52 | + {ID: "edit-1"}, {ID: "edit-2"}, {ID: "verify"}, |
| 53 | + } |
| 54 | + stored := []storage.AgentPlanStep{{ID: "stp_a"}} |
| 55 | + |
| 56 | + paired := pairStepIdentities(steps, stored) |
| 57 | + if paired["edit-1"] != "stp_a" { |
| 58 | + t.Errorf("the recorded step was not paired: %v", paired) |
| 59 | + } |
| 60 | + if _, present := paired["edit-2"]; present { |
| 61 | + t.Error("a step the store never wrote was given a durable identity") |
| 62 | + } |
| 63 | + if _, present := paired["verify"]; present { |
| 64 | + t.Error("a step the store never wrote was given a durable identity") |
| 65 | + } |
| 66 | + // adoptDurablePlanSteps drops what pairing leaves out, so the two together |
| 67 | + // mean the run carries only steps the store knows. |
| 68 | + adopted := adoptDurablePlanSteps(steps, durablePlan{Steps: paired}) |
| 69 | + if len(adopted) != 1 || adopted[0].ID != "stp_a" { |
| 70 | + t.Errorf("the run kept steps the store never wrote: %+v", adopted) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// TestPairingAnEmptyPlanPairsNothing keeps the degraded path honest. |
| 75 | +// |
| 76 | +// A store that wrote no steps leaves nothing to pair against. Pairing has to |
| 77 | +// answer with an empty map rather than a partial one, so the caller's own |
| 78 | +// fallback decides what happens rather than inheriting half a mapping. |
| 79 | +func TestPairingAnEmptyPlanPairsNothing(t *testing.T) { |
| 80 | + paired := pairStepIdentities( |
| 81 | + []agentloop.PlanStep{{ID: "edit-1"}}, nil) |
| 82 | + if len(paired) != 0 { |
| 83 | + t.Errorf("want no pairings, got %v", paired) |
| 84 | + } |
| 85 | +} |
0 commit comments