fix(client): the Atlas was replaying a cache from a build that no longer existed (T-1239)
Ferrath's Global map drew no rivers at native resolution: 375 courses arrived and 0 were drawn. The report suspected the D-261 length cull or the water truncation. Both were innocent, and so was the renderer. The client served the canvas from its own disk cache (T-1183). Every payload for GJ820Bc predated T-1237 (4e503c356), which replaced one-course-per-D8-hop with one-course-per-river -- so the map was drawing 375 hop fragments whose longest run was 106 km, all of them under D-261's read-as-a-line floor. Same build, same scenario, same 3440x1440, cache the only difference: stale courses=375 runs=180 longest=6.0px (~106 km) drawn=0 cold courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18 It looked resolution-dependent because it wasn't a resolution at all: 960x540 resolves to an 814x407 canvas, a key never cached, so it missed and re-derived correctly. 3440x1440 resolves to 1080x540, which had an entry from 2026-08-06. During the stale capture the server logged no course production whatsoever -- the canvas never came from it. The cache's only invalidation signal is project.yaml's version, and4e503c356changed how canvases are generated without touching it, so hop-shaped entries stayed valid. All 13 stale entries are stamped 0.4.5. 0.4.6 forces them to miss; that, not clearing a local directory, is what repairs a player's Atlas. The harness let this hide for eight days, in two ways now fixed. It ran against the developer's persistent user:// cache, so a capture could render a canvas built by a build that no longer existed -- and any golden shot in that window silently inherited it; user:// is now isolated per run. And it sent server stderr to /dev/null via an already-unlinked mktemp file, so no tracing from a capture was ever reachable; the log now lives at .cache/visual-server.log. The capture readout gained runs= and longest= between courses= and drawn=, because "375 arrived, 0 drawn" is not one fact but three stages, and telling them apart is what turned a guess between two suspects into a measurement. Follow-ups filed: T-1241 (current_schema_version() returns its ?.?.? fallback in an exported build, so a shipped game never invalidates on version at all) and T-1242 (nothing enforces the generation-change/version-bump pairing -- this is the fourth bump forced after the fact). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+39
-9
@@ -43,6 +43,13 @@ while [[ $# -gt 0 ]]; do
|
||||
--filter) shift 2 ;; # Accept and ignore (run-all compat)
|
||||
--filter=*) shift ;;
|
||||
--interval) INTERVAL="${2:-3}"; shift 2 ;;
|
||||
# Override the config resolution for ONE ad-hoc capture (T-1239). The
|
||||
# Atlas derives canvas extent, and therefore metres-per-gridunit, from
|
||||
# the viewport — so "does this defect depend on resolution?" is a real
|
||||
# diagnostic question, and answering it by hand-editing the committed
|
||||
# tests/visual.json invites leaving it edited. Goldens are shot at the
|
||||
# config resolution; this flag deliberately does not touch them.
|
||||
--resolution) RESOLUTION_OVERRIDE="${2:-}"; shift 2 ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
@@ -70,6 +77,10 @@ fi
|
||||
GOLDEN_DIR="$ROOT/$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('golden_dir','client/tests/golden/visual'))")"
|
||||
TOLERANCE="$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('tolerance', 5))")"
|
||||
RESOLUTION="$(python3 -c "import json; c=json.load(open('$CONFIG')); r=c.get('resolution',[960,540]); print(f'{r[0]}x{r[1]}')")"
|
||||
if [[ -n "${RESOLUTION_OVERRIDE:-}" ]]; then
|
||||
RESOLUTION="$RESOLUTION_OVERRIDE"
|
||||
echo "Note: resolution overridden to $RESOLUTION (goldens are shot at the config resolution)" >&2
|
||||
fi
|
||||
|
||||
# Read scenario names from config
|
||||
SCENARIOS=($(python3 -c "
|
||||
@@ -117,6 +128,21 @@ godot_capture() {
|
||||
local output="$3"
|
||||
local extra_args=("${@:4}")
|
||||
|
||||
# Isolate `user://` per run (T-1239). Godot resolves user:// under
|
||||
# XDG_DATA_HOME, which is how the Atlas disk cache (D-255, T-1183) persisted
|
||||
# across captures — including across the server changes that made its
|
||||
# contents wrong. A capture then rendered a canvas generated by a build that
|
||||
# no longer existed: Ferrath Global replayed a pre-T-1237 canvas from weeks
|
||||
# earlier and showed 375 hop-fragments where the live server produces 73
|
||||
# whole rivers, and every golden shot in that window silently inherited it.
|
||||
# A visual test must exercise the code in the tree, so the cache it warms
|
||||
# must not outlive the run. The dir is recreated fresh each capture; the
|
||||
# user's real cache at ~/.local/share/godot is never touched.
|
||||
local user_data="$ROOT/.cache/visual-user-data"
|
||||
rm -rf "$user_data"
|
||||
mkdir -p "$user_data"
|
||||
|
||||
XDG_DATA_HOME="$user_data" \
|
||||
"${CAPTURE_PREFIX[@]}" "$GODOT" --rendering-driver opengl3 --fixed-fps 60 \
|
||||
--resolution "$RESOLUTION" \
|
||||
--path "$ROOT/client" -s res://tests/visual_capture.gd -- \
|
||||
@@ -154,32 +180,36 @@ ensure_server_built() {
|
||||
|
||||
# Start server with --test-mode --port 0, parse LISTENING:{port}
|
||||
start_server() {
|
||||
local stdout_log
|
||||
stdout_log=$(mktemp)
|
||||
"$SERVER_BIN" --test-mode --port 0 >"$stdout_log" 2>/dev/null &
|
||||
# Keep the server's own output (T-1239). This was `mktemp` + `2>/dev/null`
|
||||
# + `rm` as soon as LISTENING was parsed, which meant two things: every
|
||||
# tracing::warn!/error! the simulation emitted was discarded, and everything
|
||||
# after startup went to an unlinked file. A server quietly degrading mid-
|
||||
# capture looked identical to a healthy one — the capture only ever showed
|
||||
# what the CLIENT thought it received. Now stderr is merged in and the log
|
||||
# lives at a stable path that survives the run for inspection.
|
||||
local stdout_log="$ROOT/.cache/visual-server.log"
|
||||
mkdir -p "$ROOT/.cache"
|
||||
"$SERVER_BIN" --test-mode --port 0 >"$stdout_log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
local attempts=0
|
||||
while [[ $attempts -lt 150 ]]; do
|
||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||
echo " Error: server exited unexpectedly" >&2
|
||||
rm -f "$stdout_log"
|
||||
echo " Error: server exited unexpectedly — see $stdout_log" >&2
|
||||
SERVER_PID=""
|
||||
return 1
|
||||
fi
|
||||
if grep -q "^LISTENING:" "$stdout_log" 2>/dev/null; then
|
||||
SERVER_PORT=$(sed -n 's/^LISTENING://p' "$stdout_log")
|
||||
rm -f "$stdout_log"
|
||||
echo " Server started: pid=$SERVER_PID port=$SERVER_PORT"
|
||||
echo " Server started: pid=$SERVER_PID port=$SERVER_PORT log=$stdout_log"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.1
|
||||
attempts=$((attempts + 1))
|
||||
done
|
||||
|
||||
echo " Error: no LISTENING signal after 15s" >&2
|
||||
echo " Error: no LISTENING signal after 15s — see $stdout_log" >&2
|
||||
kill "$SERVER_PID" 2>/dev/null || true
|
||||
rm -f "$stdout_log"
|
||||
SERVER_PID=""
|
||||
return 1
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user