|
| 1 | +use std/assert |
| 2 | + |
| 3 | +# /sse-wc integration check for examples/2048. Run via |
| 4 | +# `http-nu eval --services --store <path>` so actors run alongside. |
| 5 | +# |
| 6 | +# Drives the SSE handler end-to-end with the snapshot-actor turning |
| 7 | +# fed `.move` frames into snapshots, and asserts the per-event SSE |
| 8 | +# contract the upcoming no-op-ack refactor will modify: |
| 9 | +# |
| 10 | +# threshold flush -> a patch with boardState (the latest state) |
| 11 | +# no-op move -> a lastReqId-only echo |
| 12 | +# state-changing move -> echo, then a full boardState patch |
| 13 | +# undo -> echo, then a full boardState patch |
| 14 | +# |
| 15 | +# Determinism trick: after the actor writes the root snapshot, we |
| 16 | +# append a synthetic snapshot whose state has two value-2 tiles at |
| 17 | +# (0,0) and (3,0). On that head, `h` is a guaranteed no-op (both |
| 18 | +# already at c=0); `j` merges them (state-changing); undo walks |
| 19 | +# back to the synthetic. Avoids guessing the actor's random spawn. |
| 20 | +# |
| 21 | +# The consumer takes exactly N `data:` lines (N = length of the |
| 22 | +# expectation list); `first N` is what cleanly drops the upstream |
| 23 | +# `.cat --follow` (same shutdown path test-sse.nu's hang-guard uses). |
| 24 | +# `generate { ... {} }` looked promising for state + termination but |
| 25 | +# returning empty doesn't propagate the drop signal -- the script |
| 26 | +# hangs. We resolve the per-line expectation by index instead. |
| 27 | + |
| 28 | +const SCRIPT_DIR = path self | path dirname |
| 29 | + |
| 30 | +# --- setup ----------------------------------------------------------------- |
| 31 | + |
| 32 | +open ($SCRIPT_DIR | path join ".." "tfe" "game.nu") | .append game.nu --ttl last:1 |
| 33 | +open ($SCRIPT_DIR | path join ".." "tfe" "snapshot-actor.nu") | .append snapshot-actor.register --ttl last:1 |
| 34 | +sleep 500ms |
| 35 | + |
| 36 | +let g = (null | .append "player.test-uid.games") |
| 37 | +sleep 800ms |
| 38 | +let root = .last $"game.($g.id).snapshot" |
| 39 | +assert ($root != null) "harness: root snapshot exists" |
| 40 | + |
| 41 | +# Synthetic head: two value-2 tiles, one at top-left, one at bottom-left. |
| 42 | +let controlled = $root.meta.state |
| 43 | + | upsert tiles [ |
| 44 | + {id: 100 r: 0 c: 0 value: 2 spawned: false merged: false} |
| 45 | + {id: 101 r: 3 c: 0 value: 2 spawned: false merged: false} |
| 46 | + ] |
| 47 | + | upsert ghosts [] |
| 48 | + | upsert next_id 102 |
| 49 | + | upsert score 0 |
| 50 | + | upsert game_over false |
| 51 | + |
| 52 | +null | .append $"game.($g.id).snapshot" --meta { |
| 53 | + state: $controlled |
| 54 | + last_move_id: $g.id |
| 55 | + prev: $root.id |
| 56 | + intent: "setup" |
| 57 | + player_id: "test-uid" |
| 58 | + req_id: "setup" |
| 59 | + score: 0 |
| 60 | + max_tile: 2 |
| 61 | + moves: 0 |
| 62 | + game_over: false |
| 63 | +} |
| 64 | +sleep 200ms |
| 65 | + |
| 66 | +# --- the contract we're pinning -------------------------------------------- |
| 67 | + |
| 68 | +let expectations = [ |
| 69 | + { kind: "initial" } # threshold flush of the synthetic |
| 70 | + { kind: "lastReqId-only" req_id: "req-noop" } # no-op 'h' echo |
| 71 | + { kind: "lastReqId-only" req_id: "req-real" } # state-change 'j' echo |
| 72 | + { kind: "boardState" req_id: "req-real" } # state-change 'j' snapshot |
| 73 | + { kind: "lastReqId-only" req_id: "req-undo" } # undo echo |
| 74 | + { kind: "boardState" req_id: "req-undo" } # undo snapshot |
| 75 | +] |
| 76 | + |
| 77 | +# --- background feeder ----------------------------------------------------- |
| 78 | +# 600ms head start so the consumer is past the threshold flush; 300ms gap |
| 79 | +# so the actor processes each move before the next one is appended. |
| 80 | + |
| 81 | +let feeder = job spawn { |
| 82 | + sleep 600ms |
| 83 | + null | .append $"game.($g.id).move" --meta { user_id: "test-uid" session_id: "s" req_id: "req-noop" intent: "h" } |
| 84 | + sleep 300ms |
| 85 | + null | .append $"game.($g.id).move" --meta { user_id: "test-uid" session_id: "s" req_id: "req-real" intent: "j" } |
| 86 | + sleep 300ms |
| 87 | + null | .append $"game.($g.id).move" --meta { user_id: "test-uid" session_id: "s" req_id: "req-undo" kind: "undo" } |
| 88 | +} |
| 89 | + |
| 90 | +# --- consumer -------------------------------------------------------------- |
| 91 | + |
| 92 | +let handler = source ($SCRIPT_DIR | path join ".." "serve.nu") |
| 93 | +let sse_req = { |
| 94 | + method: "GET" |
| 95 | + uri: $"/sse-wc/($g.id)" |
| 96 | + path: $"/sse-wc/($g.id)" |
| 97 | + headers: {} |
| 98 | + query: {} |
| 99 | + mount_prefix: "" |
| 100 | +} |
| 101 | + |
| 102 | +let data_lines = ( |
| 103 | + do $handler $sse_req |
| 104 | + | lines |
| 105 | + | where ($it | str starts-with "data:") |
| 106 | + | first ($expectations | length) |
| 107 | +) |
| 108 | + |
| 109 | +$data_lines | enumerate | each {|p| |
| 110 | + let signals = $p.item | str replace -r '^data:\s*signals\s*' '' | from json |
| 111 | + let want = $expectations | get $p.index |
| 112 | + match $want.kind { |
| 113 | + "initial" => { |
| 114 | + assert ("boardState" in $signals) $"step ($p.index) initial: has boardState" |
| 115 | + } |
| 116 | + "boardState" => { |
| 117 | + assert ("boardState" in $signals) $"step ($p.index) boardState: has boardState" |
| 118 | + assert (($signals.lastReqId? | default "") == $want.req_id) $"step ($p.index) boardState: lastReqId == ($want.req_id)" |
| 119 | + } |
| 120 | + "lastReqId-only" => { |
| 121 | + assert (not ("boardState" in $signals)) $"step ($p.index) echo: boardState absent" |
| 122 | + assert (($signals.lastReqId? | default "") == $want.req_id) $"step ($p.index) echo: lastReqId == ($want.req_id)" |
| 123 | + } |
| 124 | + } |
| 125 | +} | ignore |
| 126 | + |
| 127 | +# The feeder finishes naturally once its last `.append` returns -- the |
| 128 | +# consumer can't have observed the undo events without it. No explicit |
| 129 | +# join (Nushell has no `job wait`); the job is reaped on script exit. |
| 130 | + |
| 131 | +print "examples/2048/test/test-sse-wc.nu: all assertions passed" |
0 commit comments