session_test.gleam
1 import gleam/list 2 import gleeunit/should 3 import lightspeed/agent/session 4 import lightspeed/agent/typestate 5 import lightspeed/diff 6 7 pub fn owner_mismatch_is_rejected_test() { 8 let session = 9 session.start("s-1", "proc-a", session.Rehydrate, 0, 100) 10 |> send( 11 "proc-b", 12 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 13 ) 14 15 session 16 |> session.lifecycle 17 |> should.equal(typestate.DisconnectedLabel) 18 19 session 20 |> telemetry_labels 21 |> should.equal(["ownership_rejected"]) 22 } 23 24 pub fn connect_queues_initial_patch_test() { 25 let session = 26 session.start("s-1", "proc-a", session.Rehydrate, 0, 100) 27 |> send( 28 "proc-a", 29 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 30 ) 31 32 session 33 |> session.lifecycle 34 |> should.equal(typestate.LiveLabel) 35 36 session 37 |> session.counter 38 |> should.equal(0) 39 40 session 41 |> session.pending_patches 42 |> list.length 43 |> should.equal(1) 44 45 let assert [patch] = session.pending_patches(session) 46 patch 47 |> session.patch_ref 48 |> should.equal("1") 49 50 patch 51 |> session.patch 52 |> diff.operation 53 |> should.equal("replace_segments") 54 } 55 56 pub fn ack_removes_pending_patch_test() { 57 let session = 58 session.start("s-1", "proc-a", session.Rehydrate, 0, 100) 59 |> send( 60 "proc-a", 61 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 62 ) 63 |> send("proc-a", session.Increment) 64 |> send("proc-a", session.Ack(ref: "1")) 65 66 session 67 |> session.pending_patches 68 |> list.map(session.patch_ref) 69 |> should.equal(["2"]) 70 71 let assert [patch] = session.pending_patches(session) 72 patch 73 |> session.patch 74 |> diff.operation 75 |> should.equal("update_segments") 76 77 session 78 |> telemetry_labels 79 |> should.equal([ 80 "session_mounted", 81 "patch_queued", 82 "counter_updated", 83 "patch_queued", 84 "patch_acked", 85 ]) 86 } 87 88 pub fn reconnect_rehydrate_keeps_counter_test() { 89 let session = 90 session.start("s-1", "proc-a", session.Rehydrate, 0, 100) 91 |> send( 92 "proc-a", 93 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 94 ) 95 |> send("proc-a", session.Increment) 96 |> send("proc-a", session.Crash(reason: "boom")) 97 |> send("proc-a", session.Restart(now_ms: 25)) 98 |> send("proc-a", session.Reconnect(route: "/counter", now_ms: 40)) 99 100 session 101 |> session.counter 102 |> should.equal(1) 103 104 session 105 |> session.lifecycle 106 |> should.equal(typestate.LiveLabel) 107 108 session 109 |> telemetry_labels 110 |> should.equal([ 111 "session_mounted", 112 "patch_queued", 113 "counter_updated", 114 "patch_queued", 115 "session_crashed", 116 "session_restarted", 117 "session_rehydrated", 118 "patch_queued", 119 ]) 120 } 121 122 pub fn reconnect_remount_resets_counter_test() { 123 let session = 124 session.start("s-1", "proc-a", session.Remount, 0, 100) 125 |> send( 126 "proc-a", 127 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 128 ) 129 |> send("proc-a", session.Increment) 130 |> send("proc-a", session.Crash(reason: "boom")) 131 |> send("proc-a", session.Restart(now_ms: 25)) 132 |> send("proc-a", session.Reconnect(route: "/counter", now_ms: 40)) 133 134 session 135 |> session.counter 136 |> should.equal(0) 137 138 session 139 |> telemetry_labels 140 |> should.equal([ 141 "session_mounted", 142 "patch_queued", 143 "counter_updated", 144 "patch_queued", 145 "session_crashed", 146 "session_restarted", 147 "session_remounted", 148 "patch_queued", 149 ]) 150 } 151 152 pub fn heartbeat_timeout_transitions_to_draining_test() { 153 let session = 154 session.start("s-1", "proc-a", session.Rehydrate, 0, 100) 155 |> send( 156 "proc-a", 157 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 158 ) 159 |> send("proc-a", session.Heartbeat(now_ms: 20)) 160 |> send("proc-a", session.Tick(now_ms: 121)) 161 162 session 163 |> session.lifecycle 164 |> should.equal(typestate.DrainingLabel) 165 166 session 167 |> session.heartbeat_deadline_ms 168 |> should.equal(120) 169 170 session 171 |> telemetry_labels 172 |> should.equal([ 173 "session_mounted", 174 "patch_queued", 175 "heartbeat_received", 176 "heartbeat_timed_out", 177 ]) 178 } 179 180 pub fn crash_isolation_between_sessions_test() { 181 let session_a = 182 session.start("a", "proc-a", session.Rehydrate, 0, 100) 183 |> send( 184 "proc-a", 185 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 186 ) 187 |> send("proc-a", session.Crash(reason: "boom")) 188 189 let session_b = 190 session.start("b", "proc-b", session.Rehydrate, 0, 100) 191 |> send( 192 "proc-b", 193 session.Connect(route: "/counter", csrf_token: "csrf", now_ms: 0), 194 ) 195 |> send("proc-b", session.Increment) 196 197 session_a 198 |> session.lifecycle 199 |> should.equal(typestate.TerminatedLabel) 200 201 session_b 202 |> session.lifecycle 203 |> should.equal(typestate.LiveLabel) 204 205 session_b 206 |> session.counter 207 |> should.equal(1) 208 } 209 210 fn send( 211 session_state: session.Session, 212 owner: String, 213 event: session.InboxEvent, 214 ) -> session.Session { 215 session.handle( 216 session_state, 217 session.InboxMessage(owner: owner, event: event), 218 ) 219 } 220 221 fn telemetry_labels(session_state: session.Session) -> List(String) { 222 session_state 223 |> session.telemetry 224 |> list.map(session.telemetry_label) 225 }