carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/provision"
11)
12
13// Paul's ruling M4 (2026-10-10; spec §7.5, §19 K8): every move close is
14// record first, then the claim's release. Each box close checks that nothing
15// is owed under the op, clears the record's move into LastMove, and only then
16// releases the claim. A crash between the two halves leaves the record closed
17// and the claim still under the move; the box pass is driven by records with
18// a live move, so it never comes back, and the console's recovery pass
19// releases from LastMove (internal/console, TestRecordFirstCloseCrashIsFinishedByThePass).
20//
21// Each case crashes at "after-close-record", the seam between the halves,
22// and asserts the crash state: the record at rest with the outcome, the
23// claim still in the move's closing op with nothing owed, and a further
24// pass of every box writes nothing.
25
26// closedButHeld asserts the state between the halves of a record-first
27// close: the record closed with outcome, the claim still at kind/phase under
28// the move with nothing owed under it.
29func closedButHeld(t *testing.T, rec provision.Record, c *hostclaim.Claim, outcome, kind, phase string) {
30 t.Helper()
31 if rec.Move != nil || rec.LastMove == nil || rec.LastMove.ID != p1ID || rec.LastMove.Outcome != outcome {
32 t.Fatalf("the record between the halves: move %+v last %+v, want closed %s", rec.Move, rec.LastMove, outcome)
33 }
34 if c.Op.ID != p1ID || c.Op.Kind != kind || c.Op.Phase != phase {
35 t.Fatalf("the claim between the halves: %+v, want %s/%q under %s (the release is second)", c.Op, kind, phase, p1ID)
36 }
37 for _, o := range c.Obligations {
38 if o.Op == p1ID {
39 t.Fatalf("the record closed while %s %s%s was still owed under the op", o.Kind, o.Region, o.Box)
40 }
41 }
42}
43
44func TestBoxClosesAreRecordFirst(t *testing.T) {
45 t.Parallel()
46 t.Run("committed close (movesparks.go)", func(t *testing.T) {
47 h := newCloseHarness(t)
48 h.land(t)
49 h.sourcePass(t)
50 h.clock.Advance(24 * time.Hour)
51 if err := h.regionalAck(t, h.chore(t, "ie", false)); err != nil {
52 t.Fatal(err)
53 }
54 h.landAt = "after-close-record"
55 h.crashLand(t)
56 closedButHeld(t, h.read(t, p1Host), h.claim(t), "done", hostclaim.KindLanding, hostclaim.PhaseCommitted)
57 gen := h.claim(t).Gen
58 h.freshTarget()
59 h.land(t)
60 h.sourcePass(t)
61 if c := h.claim(t); c.Gen != gen {
62 t.Fatalf("a box pass after the record's close wrote the claim (%d to %d): %+v", gen, c.Gen, c.Op)
63 }
64 })
65 t.Run("abort (movesabort.go)", func(t *testing.T) {
66 h := newAbortHarness(t)
67 h.abort(t)
68 h.abAt = "after-abort-record"
69 for i := 0; i < 6 && !h.abDone; i++ {
70 movesPass(context.Background(), h.s, h.w())
71 h.tPass(t)
72 if o := h.laneChore(t); o != nil {
73 h.pastHorizon(t, o)
74 h.tPass(t)
75 }
76 }
77 if !h.abDone {
78 t.Fatalf("never reached after-abort-record (%v)", h.abHits)
79 }
80 closedButHeld(t, h.read(t, p1Host), h.claim(t), "aborted", hostclaim.KindMoving, hostclaim.PhaseAborting)
81 h.ownStateGone(t)
82 gen := h.claim(t).Gen
83 h.worker1 = nil
84 h.pass(t)
85 h.tPass(t)
86 if c := h.claim(t); c.Gen != gen {
87 t.Fatalf("a box pass after the record's close wrote the claim (%d to %d): %+v", gen, c.Gen, c.Op)
88 }
89 })
90 t.Run("recovery close (movesrecover.go)", func(t *testing.T) {
91 h := newRecoverHarness(t, false)
92 h.released(t)
93 h.srcPass(t)
94 h.opened(t)
95 h.rcAt = "after-close-record"
96 h.closeOut(t)
97 if !h.rcDone {
98 t.Fatalf("never reached after-close-record (%v)", h.rcHits)
99 }
100 closedButHeld(t, h.read(t, p1Host), h.claim(t), "rolled_back", hostclaim.KindRestoring, hostclaim.PhaseOpened)
101 if n := strings.Count(strings.Join(h.sEvents, " "), "rolled_back"); n != 1 {
102 t.Fatalf("the terminal event %d times: %v", n, h.sEvents)
103 }
104 gen := h.claim(t).Gen
105 h.sw = nil
106 h.srcPass(t)
107 h.rollbackPass(t)
108 if c := h.claim(t); c.Gen != gen {
109 t.Fatalf("a box pass after the record's close wrote the claim (%d to %d): %+v", gen, c.Gen, c.Op)
110 }
111 })
112 t.Run("revert close (movesrevert.go)", func(t *testing.T) {
113 h := newRevertHarness(t)
114 h.fromStopped(t)
115 h.arm(t)
116 h.pass(t)
117 h.reopened(t)
118 h.rvAt = "after-close-record"
119 o := h.laneChore(t)
120 h.tPass(t)
121 h.pastHorizon(t, o)
122 h.tPass(t)
123 h.pass(t)
124 if !h.rvDone {
125 t.Fatalf("never reached after-close-record (%v)", h.rvHits)
126 }
127 closedButHeld(t, h.read(t, p1Host), h.claim(t), "rolled_back", hostclaim.KindReverting, hostclaim.PhaseOpened)
128 gen := h.claim(t).Gen
129 h.freshWorker()
130 h.pass(t)
131 h.tPass(t)
132 if c := h.claim(t); c.Gen != gen {
133 t.Fatalf("a box pass after the record's close wrote the claim (%d to %d): %+v", gen, c.Gen, c.Op)
134 }
135 })
136}
137