| 1 | package moves |
| 2 | |
| 3 | import ( |
| 4 | "testing" |
| 5 | "time" |
| 6 | |
| 7 | "github.com/carlosframework/platform/internal/provision" |
| 8 | ) |
| 9 | |
| 10 | // The auto-abort bound (spec §7.11, ruling K18): a Phase 1 move still |
| 11 | // preparing AbortAfter after max(RequestedAt, ReadyAt) is due; one that is |
| 12 | // not yet there, or is in any other state, or is a legacy move, is not. |
| 13 | func TestAbortDue(t *testing.T) { |
| 14 | if AbortAfter != 6*time.Hour { |
| 15 | t.Fatalf("AbortAfter = %v, want 6h", AbortAfter) |
| 16 | } |
| 17 | req := time.Date(2026, 10, 9, 8, 0, 0, 0, time.UTC) |
| 18 | rec := func(state string, ready *time.Time) provision.Record { |
| 19 | return provision.Record{Host: "a.go.example.com", Move: &provision.MoveState{ID: "mv_abcdefghijklmnopqrstuvwxyz", State: state, RequestedAt: req, ReadyAt: ready}} |
| 20 | } |
| 21 | at := func(d time.Duration) time.Time { return req.Add(d) } |
| 22 | later := req.Add(2 * time.Hour) |
| 23 | for name, c := range map[string]struct { |
| 24 | rec provision.Record |
| 25 | now time.Time |
| 26 | want bool |
| 27 | }{ |
| 28 | "preparing, just short": {rec(provision.MovePreparing, nil), at(AbortAfter - time.Second), false}, |
| 29 | "preparing, at the bound": {rec(provision.MovePreparing, nil), at(AbortAfter), true}, |
| 30 | "preparing, well past": {rec(provision.MovePreparing, nil), at(3 * AbortAfter), true}, |
| 31 | "the bound is from the later time": {rec(provision.MovePreparing, &later), at(AbortAfter + time.Hour), false}, |
| 32 | "and reached from it": {rec(provision.MovePreparing, &later), later.Add(AbortAfter), true}, |
| 33 | "a non-UTC clock": {rec(provision.MovePreparing, nil), at(AbortAfter).In(time.FixedZone("x", -7*3600)), true}, |
| 34 | "ready": {rec(provision.MoveReady, &later), at(10 * AbortAfter), false}, |
| 35 | "cutting": {rec(provision.MoveCutting, &later), at(10 * AbortAfter), false}, |
| 36 | "aborting": {rec(provision.MoveAborting, nil), at(10 * AbortAfter), false}, |
| 37 | "landing": {rec(provision.MoveLanding, nil), at(10 * AbortAfter), false}, |
| 38 | "legacy": {provision.Record{Move: &provision.MoveState{State: provision.MovePreparing, RequestedAt: req}}, at(10 * AbortAfter), false}, |
| 39 | "no move": {provision.Record{}, at(10 * AbortAfter), false}, |
| 40 | "no request time": {provision.Record{Move: &provision.MoveState{ID: "mv_abcdefghijklmnopqrstuvwxyz", State: provision.MovePreparing}}, at(10 * AbortAfter), false}, |
| 41 | } { |
| 42 | if got := AbortDue(c.rec, c.now); got != c.want { |
| 43 | t.Errorf("%s: AbortDue = %v, want %v", name, got, c.want) |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |