Files
transcience/tools/smoke.sh
T
claude 4a98cf4b0e
ci / verify (push) Successful in 48s
The Cantor gets its own portal; bullets stop strobing
A dungeon now names the boss arena it ends in, and the arena decides the boss.
Three entrances in the hub: Warden's Descent, The Choir Vault, Proving Grounds.

That replaces the seed coin-flip from the last commit, which was the wrong
call. Which boss you are about to fight is the single thing a player decides
before walking into a dungeon, and rolling it for them makes that decision
unavailable. Only the Proving Grounds still leaves its arena open, because a
harness you re-enter every couple of minutes wants whichever fight comes up
first -- and it is the one place where "either will do" is true.

test_every_boss_has_a_dungeon_that_reaches_it is the check that matters here:
adding a boss and forgetting to give it a way in is now a failing test rather
than a boss nobody meets. The smoke test asserts all three portals open over a
real socket -- bots pick theirs by account id, so a run exercises each.

Bullets no longer animate. The sheet's eight frames are a COLOUR cycle rather
than a shape change, so running it had every bullet on screen strobing through
a palette in unison, which with a few hundred in the air is exactly as hard to
look at as it sounds. Each bullet holds one frame and turns slowly instead,
offset by its own id so a ring of twenty does not rotate as one rigid wheel.
The rate is slow enough that nothing completes a full turn inside its own
lifetime, so it reads as drift rather than spin, and a test pins that against
the longest-lived bullet in the game. The renderer slices four textures at
startup now instead of thirty-two.

check.sh clean, 414 tests, SMOKE PASS (22 assertions), all four diagnostics
green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 10:36:06 +02:00

160 lines
7.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# End-to-end integration test: boot a dedicated server, connect two scripted
# bot clients over a real ENet socket, and assert that the whole loop happened --
# handshake, lobby, portal into a dungeon, and the emergency escape back.
#
# This is the test that catches everything unit tests structurally cannot: RPC
# wiring, codec round-trips over the wire, instance transfers, and the client's
# reconciliation loop. Run it before calling any networking change done.
#
# tools/smoke.sh # ~35s
# KEEP=1 tools/smoke.sh # keep the logs and print where they are
set -uo pipefail
cd "$(dirname "$0")/.."
GODOT="${GODOT:-godot}"
PORT="${PORT:-27099}"
SERVER_TICKS="${SERVER_TICKS:-2000}"
CLIENT_TICKS="${CLIENT_TICKS:-1700}"
OUT="$(mktemp -d -t transcience-smoke-XXXXXX)"
PIDS=()
cleanup() {
for pid in "${PIDS[@]:-}"; do
kill "$pid" 2>/dev/null
done
wait 2>/dev/null
if [[ "${KEEP:-0}" == "1" ]]; then
echo "logs kept in $OUT"
else
rm -rf "$OUT"
fi
}
trap cleanup EXIT
echo "smoke: server on port $PORT, logs in $OUT"
# A scratch character store: without it a rerun resumes the characters the
# previous run created, and "a character was created" stops being true.
"$GODOT" --headless --path . -- --server --port "$PORT" --autoquit "$SERVER_TICKS" \
--store "$OUT/characters.json" \
> "$OUT/server.log" 2>&1 &
PIDS+=($!)
for _ in $(seq 1 60); do
grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null && break
sleep 0.25
done
if ! grep -q "SERVER_READY" "$OUT/server.log" 2>/dev/null; then
echo "FAIL: server never became ready"; cat "$OUT/server.log"; exit 1
fi
for n in 1 2; do
"$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \
--name "bot$n" --account "$((7000 + n))" \
--autoquit "$CLIENT_TICKS" > "$OUT/bot$n.log" 2>&1 &
PIDS+=($!)
sleep 0.4
done
# A fourth bot that leaves *politely* -- the same Net.shutdown() the in-game
# menu's "Disconnect" button calls. The escape channel is keyed on the socket
# closing, not on how it closed, so a clean exit must be caught exactly like the
# SIGKILL below. If someone ever adds a "clean leave" message that bypasses the
# channel, this is what catches it.
"$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \
--name "leavebot" --account 7101 --leave-after 120 --autoquit "$CLIENT_TICKS" \
> "$OUT/leavebot.log" 2>&1 &
PIDS+=($!)
sleep 0.4
# A third bot that gets SIGKILLed the moment it is inside a dungeon. This is the
# anti-disconnect-cheese path: the server must keep its body in the world,
# channel it out over the same one second the escape button costs, and only then
# forget the peer -- never delete it instantly on socket close.
"$GODOT" --headless --path . -- --bot --host 127.0.0.1 --port "$PORT" \
--name "dropbot" --account 7102 --autoquit "$CLIENT_TICKS" > "$OUT/dropbot.log" 2>&1 &
DROP_PID=$!
for _ in $(seq 1 80); do
grep -q "entered instance .*DUNGEON" "$OUT/dropbot.log" 2>/dev/null && break
sleep 0.25
done
if grep -q "entered instance .*DUNGEON" "$OUT/dropbot.log" 2>/dev/null; then
# Reaped here so bash does not print its own "Killed" job-control line; the
# kill is the point of the test, not a failure.
{ kill -9 "$DROP_PID"; wait "$DROP_PID"; } 2>/dev/null || true
else
echo " WARN dropbot never reached a dungeon; drop assertions will fail"
PIDS+=("$DROP_PID")
fi
wait 2>/dev/null
fails=0
check() { # check <label> <file> <pattern>
if grep -qE "$3" "$2"; then
echo " ok $1"
else
echo " FAIL $1 (no match for /$3/ in $(basename "$2"))"
fails=$((fails + 1))
fi
}
refute() { # refute <label> <file> <pattern>
local hits
hits=$(grep -cE "$3" "$2" || true)
if [[ "$hits" == "0" ]]; then
echo " ok $1"
else
echo " FAIL $1 ($hits line(s) matched /$3/ in $(basename "$2"))"
grep -E "$3" "$2" | head -5 | sed 's/^/ /'
fails=$((fails + 1))
fi
}
echo "assertions:"
check "bot1 authenticated" "$OUT/server.log" "authenticated as account 7001"
check "bot2 authenticated" "$OUT/server.log" "authenticated as account 7002"
check "a character was created" "$OUT/server.log" "created 'bot1'"
check "and persisted to the store" "$OUT/characters.json" "bot1"
check "and are played" "$OUT/server.log" "playing 'bot1'"
# The bots press interact and the use key while in a dungeon, so the item half
# of the input frame crosses a real socket on every run. What they almost never
# do is produce a drop to act on -- that end of it is tools/diag_loot.tscn.
check "inventories reach the save file" "$OUT/characters.json" "\"inventory\""
check "a dungeon instance opened" "$OUT/server.log" "opened dungeon instance"
# Which arena a run gets is a coin flip on its seed, so this only asserts that
# SOME boss spawned and that it is one the content knows about -- which is what
# catches an arena wired to a boss id that does not resolve.
check "a boss spawned" "$OUT/server.log" "BOSS_SPAWNED (warden|cantor) in instance"
# Bots pick their entrance from their account id, so a run with several of them
# opens one of each kind. This is what catches a portal wired to the wrong
# dungeon, or an instance matcher that ignores which dungeon was asked for.
check "the real run opened" "$OUT/server.log" "opened dungeon instance [0-9]+ \(warden_descent\)"
check "the choir vault opened" "$OUT/server.log" "opened dungeon instance [0-9]+ \(choir_vault\)"
check "the proving grounds opened" "$OUT/server.log" "opened dungeon instance [0-9]+ \(proving_grounds\)"
check "and each ended with its own boss" "$OUT/server.log" "BOSS_SPAWNED cantor in instance"
check "emergency escape completed" "$OUT/server.log" "escaped to lobby"
check "bot1 reached a dungeon" "$OUT/bot1.log" "entered instance .*DUNGEON"
check "bot1 returned to the lobby" "$OUT/bot1.log" "entered instance .*LOBBY"
check "a hard drop is channelled, not instant" \
"$OUT/server.log" "dropped in instance [0-9]+, channelling out"
check "the dropped body is released" "$OUT/server.log" "released from instance [0-9]+ after drop"
check "a polite disconnect leaves too" "$OUT/leavebot.log" "BOT_GRACEFUL_LEAVE"
check "a polite disconnect is also channelled" \
"$OUT/server.log" "'leavebot' dropped in instance [0-9]+, channelling out"
# No assertion for upgrades here on purpose. Bots never walk to the
# quartermaster, so anything written would either duplicate a check above or
# assert nothing at all. The upgrade-state message DOES cross the wire on every
# login, so a malformed one shows up in the script-error refutations below --
# and tools/diag_upgrades.tscn covers the loop itself.
refute "no server script errors" "$OUT/server.log" "SCRIPT ERROR|Parse Error|USER ERROR"
refute "no client script errors" "$OUT/bot1.log" "SCRIPT ERROR|Parse Error|USER ERROR"
echo
if [[ $fails -eq 0 ]]; then
echo "SMOKE PASS"
else
echo "SMOKE FAIL ($fails)"
echo "--- server.log (tail) ---"; tail -40 "$OUT/server.log"
echo "--- bot1.log (tail) ---"; tail -40 "$OUT/bot1.log"
fi
exit $fails