| index 08bec8262..53038d40a 100644 |
| --- a/internal/provision/move_test.go |
| +++ b/internal/provision/move_test.go |
| @@ -1562,3 +1562,62 @@ func TestRecoveryInputsRecordedAtProbing(t *testing.T) { |
| t.Fatalf("stuck: %v", err) |
| } |
| } |
| + |
| +// A revert's probe records the inputs it renders before it renders them |
| +// (Plan 4 Task 6; J18, §7.7): MoveRecordRevertInputs writes the move's own |
| +// RecoveryInputs on a reverting record that has not reopened, and nothing |
| +// else. A later probe replaces them; they are never compared with |
| +// move.inputs. It is refused on any other state, after the reopen, without a |
| +// release and under another guard. |
| +func TestRevertInputsRecordedBeforeTheRender(t *testing.T) { |
| + g := cutTestGuard() |
| + now := time.Date(2026, 10, 6, 9, 0, 0, 0, time.UTC) |
| + reverting := func() *Record { |
| + return &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveReverting, CutPhase: CutStopping, |
| + Reason: RevertStopEvidenceBudget, Inputs: &MoveInputs{BundleETags: map[string]string{"default": "e1"}, Release: "v7"}}} |
| + } |
| + first := MoveInputs{BundleETags: map[string]string{"default": "e2"}, OverlayETag: "o2", Release: "v8"} |
| + rec := reverting() |
| + if err := MoveRecordRevertInputs(g, first)(rec); err != nil { |
| + t.Fatal(err) |
| + } |
| + m := rec.Move |
| + if m.RecoveryInputs == nil || m.RecoveryInputs.BundleETags["default"] != "e2" || m.RecoveryInputs.OverlayETag != "o2" || m.RecoveryInputs.Release != "v8" { |
| + t.Fatalf("recorded %+v", m.RecoveryInputs) |
| + } |
| + if m.Inputs.BundleETags["default"] != "e1" || m.Inputs.Release != "v7" || m.State != MoveReverting || m.Reason != RevertStopEvidenceBudget || m.CutPhase != CutStopping { |
| + t.Fatalf("the record changed beyond the inputs: %+v", m) |
| + } |
| + first.BundleETags["default"] = "mutated" |
| + if m.RecoveryInputs.BundleETags["default"] != "e2" { |
| + t.Fatal("the record aliases the caller's map") |
| + } |
| + if err := MoveRecordRevertInputs(g, MoveInputs{BundleETags: map[string]string{"default": "e3"}, Release: "v8"})(rec); err != nil || m.RecoveryInputs.BundleETags["default"] != "e3" { |
| + t.Fatalf("a later probe: %v %+v", err, m.RecoveryInputs) |
| + } |
| + for _, state := range []string{MoveCutting, MoveLanding, MoveRollingBack, MoveStuck, MoveCommitted, MoveAborting} { |
| + rec := reverting() |
| + rec.Move.State = state |
| + if err := MoveRecordRevertInputs(g, first)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.RecoveryInputs != nil { |
| + t.Fatalf("from %s: %v", state, err) |
| + } |
| + } |
| + rec = reverting() |
| + rec.Move.ReopenedAt = &now |
| + if err := MoveRecordRevertInputs(g, first)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.RecoveryInputs != nil { |
| + t.Fatalf("after the reopen: %v", err) |
| + } |
| + rec = reverting() |
| + if err := MoveRecordRevertInputs(g, MoveInputs{BundleETags: map[string]string{"default": "e2"}})(rec); err == nil || rec.Move.RecoveryInputs != nil { |
| + t.Fatalf("no release: %v", err) |
| + } |
| + if err := MoveRecordRevertInputs(MoveGuard{ID: g.ID, Incarnation: strings.Repeat("cd", 32)}, first)(rec); !errors.Is(err, ErrMoveConflict) || rec.Move.RecoveryInputs != nil { |
| + t.Fatalf("another guard: %v", err) |
| + } |
| + if raw, _ := json.Marshal(MoveState{}); strings.Contains(string(raw), "recovery_inputs") { |
| + t.Fatalf("recovery_inputs is not omitempty: %s", raw) |
| + } |
| + if raw, _ := json.Marshal(MoveState{RecoveryInputs: &MoveInputs{Release: "v8"}}); !strings.Contains(string(raw), `"recovery_inputs":{`) { |
| + t.Fatalf("recovery_inputs missing: %s", raw) |
| + } |
| +} |