| index 0e459dcec..d5411b5b1 100644 |
| --- a/internal/console/apiinstancemove_j17_test.go |
| +++ b/internal/console/apiinstancemove_j17_test.go |
| @@ -170,3 +170,64 @@ func TestJ17DoorRefusesWithoutRegistration(t *testing.T) { |
| // through SetRecordMove (covered by the Phase 1 door tests), and a host-claim |
| // console without its runtime still refuses with HC5-N1 |
| // (TestMoveDoorPhase1RefusedUnderHostClaim, TestResolveRetryRefusedUnderHostClaim). |
| + |
| +// Final review I5 (preflight L5, claim-on half): on a host-claim console, |
| +// once the cut has started (cutting past requested, landing, committed, |
| +// rolling back, reverting) the abort door answers M-C13 before the typed |
| +// grant, which after the seal would refuse with "the move changed". Nothing |
| +// is written. A stuck move keeps the grant's own answer, and an abort from |
| +// cutting/requested before the seal is still the typed abort. |
| +func TestJ17DoorAbortAfterTheCutStartedSaysSo(t *testing.T) { |
| + ctx := context.Background() |
| + fx, token, moveURL, _ := j17StartedMove(t, "code-j17-mc13") |
| + setPhase := func(phase string) { |
| + t.Helper() |
| + obs, err := hostclaim.Read(ctx, fx.rel, "acme.go.tito.io") |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := hostclaim.Update(ctx, fx.rel, obs, func(c *hostclaim.Claim) error { |
| + c.Op.Phase = phase |
| + return nil |
| + }); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + setPhase(hostclaim.PhaseSealed) |
| + for _, c := range []struct { |
| + name string |
| + mut func(*provision.MoveState) |
| + }{ |
| + {"cutting/admitted", func(m *provision.MoveState) { m.State, m.CutPhase = provision.MoveCutting, provision.CutAdmitted }}, |
| + {"cutting/staged", func(m *provision.MoveState) { m.State, m.CutPhase = provision.MoveCutting, provision.CutStaged }}, |
| + {"landing", func(m *provision.MoveState) { m.State = provision.MoveLanding }}, |
| + {"committed", func(m *provision.MoveState) { m.State = provision.MoveCommitted }}, |
| + {"rolling back", func(m *provision.MoveState) { m.State = provision.MoveRollingBack }}, |
| + {"reverting", func(m *provision.MoveState) { m.State = provision.MoveReverting }}, |
| + } { |
| + j17SetRecord(t, fx, func(r *provision.Record) { c.mut(r.Move) }) |
| + claim, before := claimBytes(t, fx, "acme.go.tito.io"), j17MoveState(t, fx) |
| + if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusConflict || errorText(out) != moveCutStartedSentence { |
| + t.Fatalf("abort in %s = %d %s, want M-C13", c.name, code, out) |
| + } |
| + if after := j17MoveState(t, fx); after.State != before.State || !after.UpdatedAt.Equal(before.UpdatedAt) || claimBytes(t, fx, "acme.go.tito.io") != claim { |
| + t.Fatalf("a refused abort in %s wrote", c.name) |
| + } |
| + } |
| + j17SetRecord(t, fx, func(r *provision.Record) { |
| + r.Move.State, r.Move.CutPhase, r.Move.StuckReason = provision.MoveStuck, provision.CutArchived, provision.StuckBaselineMismatch |
| + }) |
| + if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusConflict || errorText(out) == moveCutStartedSentence { |
| + t.Fatalf("abort of a stuck move = %d %s, want the grant's own refusal", code, out) |
| + } |
| + setPhase(hostclaim.PhaseMoving) |
| + j17SetRecord(t, fx, func(r *provision.Record) { |
| + r.Move.State, r.Move.CutPhase, r.Move.StuckReason = provision.MoveCutting, provision.CutRequested, "" |
| + }) |
| + if code, _, out := rawReq(t, http.MethodDelete, moveURL, token, ""); code != http.StatusAccepted { |
| + t.Fatalf("abort from cutting/requested = %d %s", code, out) |
| + } |
| + if m := j17MoveState(t, fx); m.State != provision.MoveAborting { |
| + t.Fatalf("record move = %+v", m) |
| + } |
| +} |