Skip to content

Commit 295ac54

Browse files
committed
examples/2048: add /sse-wc integration test
1 parent 71b9a5f commit 295ac54

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

examples/2048/test/check.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ echo
2222
echo "=== sse pipeline tests (test-sse.nu) ==="
2323
STORE_SSE="$(mktemp -d -t 2048-test-sse-XXXXXX)"
2424
STORE_ACTOR="$(mktemp -d -t 2048-test-actor-XXXXXX)"
25-
trap "rm -rf $STORE_SSE $STORE_ACTOR" EXIT
25+
STORE_SSEWC="$(mktemp -d -t 2048-test-ssewc-XXXXXX)"
26+
trap "rm -rf $STORE_SSE $STORE_ACTOR $STORE_SSEWC" EXIT
2627
# 15s wraps a potential hang inside the test (e.g. `let s = .cat
2728
# --follow ...` collecting an infinite stream) so the failure mode
2829
# becomes a non-zero exit instead of a CI lockup.
@@ -41,6 +42,16 @@ if ! timeout 30 "$REPO_ROOT/target/debug/http-nu" eval --services --store "$STOR
4142
fi
4243
echo
4344

45+
echo "=== /sse-wc integration (test-sse-wc.nu) ==="
46+
# Drives /sse-wc end-to-end with actors -- the consumer waits on a
47+
# fixed number of `data:` lines, so a missing patch becomes a clean
48+
# `timeout` exit rather than a hang.
49+
if ! timeout 30 "$REPO_ROOT/target/debug/http-nu" eval --services --store "$STORE_SSEWC" "$SCRIPT_DIR/test-sse-wc.nu"; then
50+
echo "test-sse-wc.nu failed" >&2
51+
exit 1
52+
fi
53+
echo
54+
4455
echo "=== browser e2e (test.mjs) ==="
4556
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
4657
echo "missing node_modules -- run \`npm install\` in $SCRIPT_DIR first" >&2

examples/2048/test/test-sse-wc.nu

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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

Comments
 (0)