/ test / lightspeed / agent / session_test.gleam
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  }