Skip to content

Commit 71b9a5f

Browse files
committed
examples/2048: add snapshot-actor integration test
1 parent 1595597 commit 71b9a5f

2 files changed

Lines changed: 117 additions & 8 deletions

File tree

examples/2048/test/check.sh

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,47 @@
11
#!/bin/sh
2-
# Run all 2048 example checks: pure-logic unit tests, end-to-end browser
3-
# test, and the per-call benchmark. Exits non-zero on any failure.
2+
# Run all 2048 example checks: pure-logic unit tests, SSE pipeline,
3+
# snapshot-actor integration, end-to-end browser test, benchmark.
4+
# Exits non-zero on any failure.
45
set -eu
56

67
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
78
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
89
cd "$REPO_ROOT"
910

11+
# The actor integration test and the browser e2e both need the local
12+
# debug binary (the PATH `http-nu` may predate `eval --services`).
13+
if [ ! -x "$REPO_ROOT/target/debug/http-nu" ]; then
14+
echo "missing target/debug/http-nu -- run \`cargo build\` first" >&2
15+
exit 1
16+
fi
17+
1018
echo "=== unit tests (test.nu) ==="
1119
http-nu eval "$SCRIPT_DIR/test.nu"
1220
echo
1321

1422
echo "=== sse pipeline tests (test-sse.nu) ==="
15-
STORE="$(mktemp -d -t 2048-test-sse-XXXXXX)"
16-
trap "rm -rf $STORE" EXIT
23+
STORE_SSE="$(mktemp -d -t 2048-test-sse-XXXXXX)"
24+
STORE_ACTOR="$(mktemp -d -t 2048-test-actor-XXXXXX)"
25+
trap "rm -rf $STORE_SSE $STORE_ACTOR" EXIT
1726
# 15s wraps a potential hang inside the test (e.g. `let s = .cat
1827
# --follow ...` collecting an infinite stream) so the failure mode
1928
# becomes a non-zero exit instead of a CI lockup.
20-
if ! timeout 15 http-nu eval --store "$STORE" "$SCRIPT_DIR/test-sse.nu"; then
29+
if ! timeout 15 http-nu eval --store "$STORE_SSE" "$SCRIPT_DIR/test-sse.nu"; then
2130
echo "test-sse.nu failed (hang or assertion error)" >&2
2231
exit 1
2332
fi
2433
echo
2534

26-
echo "=== browser e2e (test.mjs) ==="
27-
if [ ! -x "$REPO_ROOT/target/debug/http-nu" ]; then
28-
echo "missing target/debug/http-nu -- run \`cargo build\` first" >&2
35+
echo "=== snapshot-actor integration (test-snapshot-actor.nu) ==="
36+
# Needs --services so the actor dispatcher spawns alongside the eval;
37+
# uses the local debug build (PATH http-nu may predate that flag).
38+
if ! timeout 30 "$REPO_ROOT/target/debug/http-nu" eval --services --store "$STORE_ACTOR" "$SCRIPT_DIR/test-snapshot-actor.nu"; then
39+
echo "test-snapshot-actor.nu failed" >&2
2940
exit 1
3041
fi
42+
echo
43+
44+
echo "=== browser e2e (test.mjs) ==="
3145
if [ ! -d "$SCRIPT_DIR/node_modules" ]; then
3246
echo "missing node_modules -- run \`npm install\` in $SCRIPT_DIR first" >&2
3347
exit 1
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
use std/assert
2+
3+
# Snapshot-actor integration check for examples/2048. Run via
4+
# `http-nu eval --services --store <path>` so the actor dispatcher
5+
# spawns alongside the eval (without --services there is no
6+
# dispatcher and an appended frame produces nothing).
7+
#
8+
# Asserts the snapshot-actor's I/O contract end-to-end across the
9+
# core game flow:
10+
# 1. new game -- a games frame yields the root snapshot
11+
# 2. real move -- a state-changing move yields one new snapshot
12+
# with the right state and req_id
13+
# 3. no-op move -- a move into a wall yields no new snapshot
14+
# 4. undo -- yields a snapshot whose state matches the
15+
# parent (walks `prev`)
16+
17+
const SCRIPT_DIR = path self | path dirname
18+
19+
# Register what serve.nu registers at startup: the game.nu module the
20+
# actor `use`s, and the snapshot-actor itself. The dispatcher picks up
21+
# the .register frame and spawns the actor.
22+
open ($SCRIPT_DIR | path join ".." "tfe" "game.nu") | .append game.nu --ttl last:1
23+
open ($SCRIPT_DIR | path join ".." "tfe" "snapshot-actor.nu") | .append snapshot-actor.register --ttl last:1
24+
sleep 500ms
25+
26+
# /new appends a `player.<uid>.games` frame. The actor responds with the
27+
# root snapshot for that game (game_id = the games frame's own id).
28+
let g = (null | .append "player.test-uid.games")
29+
sleep 800ms
30+
let root = .last $"game.($g.id).snapshot"
31+
assert ($root != null) "new game: root snapshot exists"
32+
assert ($root.meta.last_move_id == $g.id) "new game: root snapshot's last_move_id == game_id"
33+
34+
# --- 2. real move ----------------------------------------------------------
35+
# Pick a direction guaranteed to change state on the 2-tile root board: if
36+
# any tile is below the top row, `k` (up) slides; otherwise `j` (down) does.
37+
let tiles = $root.meta.state.tiles
38+
let intent = if ($tiles | any {|t| $t.r > 0 }) { "k" } else { "j" }
39+
let move = (null | .append $"game.($g.id).move" --meta {
40+
user_id: "test-uid"
41+
session_id: "s"
42+
req_id: "real-move"
43+
intent: $intent
44+
})
45+
sleep 500ms
46+
let after_real = .last $"game.($g.id).snapshot"
47+
assert ($after_real.id != $root.id) "real move: a new snapshot was written"
48+
assert ($after_real.meta.last_move_id == $move.id) "real move: snapshot's last_move_id == move frame id"
49+
assert ($after_real.meta.req_id == "real-move") "real move: snapshot carries the move's req_id"
50+
assert ($after_real.meta.prev == $root.id) "real move: snapshot's prev == root snapshot id"
51+
assert ($after_real.meta.intent == $intent) "real move: snapshot's intent == the move's intent"
52+
53+
# --- 3. no-op move ---------------------------------------------------------
54+
# Feed the same direction repeatedly until one feed produces no new snapshot
55+
# -- that's the no-op. With a bounded board (max 16 tiles) and a spawn per
56+
# state change, repeated same-direction moves saturate within a small budget.
57+
mut saw_noop = false
58+
mut count = (.cat | where topic == $"game.($g.id).snapshot" | length)
59+
for i in 0..30 {
60+
null | .append $"game.($g.id).move" --meta {
61+
user_id: "test-uid"
62+
session_id: "s"
63+
req_id: $"noop-($i)"
64+
intent: $intent
65+
}
66+
sleep 350ms
67+
let now = (.cat | where topic == $"game.($g.id).snapshot" | length)
68+
if $now == $count {
69+
$saw_noop = true
70+
break
71+
}
72+
$count = $now
73+
}
74+
assert $saw_noop "no-op move: a repeated same-direction move eventually produces no new snapshot"
75+
76+
# --- 4. undo ---------------------------------------------------------------
77+
# Undo walks back via `meta.prev`. The new snapshot's tiles should match the
78+
# parent's tiles (the actor clears spawned/merged flags on the way back).
79+
let head_before_undo = .last $"game.($g.id).snapshot"
80+
let parent = .get $head_before_undo.meta.prev
81+
null | .append $"game.($g.id).move" --meta {
82+
user_id: "test-uid"
83+
session_id: "s"
84+
req_id: "undo-1"
85+
kind: "undo"
86+
}
87+
sleep 500ms
88+
let after_undo = .last $"game.($g.id).snapshot"
89+
assert ($after_undo.id != $head_before_undo.id) "undo: a new snapshot was written"
90+
assert ($after_undo.meta.intent == "undo") "undo: snapshot's intent == \"undo\""
91+
let undo_tiles = $after_undo.meta.state.tiles | each {|t| {r: $t.r, c: $t.c, value: $t.value}} | sort-by r c
92+
let parent_tiles = $parent.meta.state.tiles | each {|t| {r: $t.r, c: $t.c, value: $t.value}} | sort-by r c
93+
assert ($undo_tiles == $parent_tiles) "undo: snapshot's tiles match the parent's"
94+
95+
print "examples/2048/test/test-snapshot-actor.nu: all assertions passed"

0 commit comments

Comments
 (0)