Two real bugs from the last change, both only visible in a real run:
1. Controls reversed: the third-person SpringArm3D had a 180° Y rotation
that put the camera IN FRONT of the player looking back -- a mirror, so
W read as backward and mouse pitch as inverted. SpringArm3D extends its
child along +Z, not -Z; with no rotation the camera sits behind the
player (+Z world) and looks -Z straight at them. Verified the vectors
headless: camera behind, W decreases Z, mouse-up looks up.
2. Bare capsule instead of an avatar in the .exe: avatar_catalog.gd reads
assets/avatars/catalog.json with FileAccess.open(), but the export used
`all_resources` + empty include_filter, which does NOT pack plain .json
files. ensure_catalog() failed -> no loadout -> build_current_avatar()
returned null -> capsule stayed visible. From source it worked because
res:// is the real filesystem. Fixed: include_filter="*.json" in both
export presets (verified catalog.json + the .glb pack are now in the
exported .pck).
- scripts/verify_export_contents.gd + a checkpoint-runner step: export a
pack and assert the non-resource runtime files are inside it. This is
the guard for the "works from source, broken in the .exe" class.
Client-side -- rebuild the exe.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
Every checkpoint smoke test registers users against one backend from one
client IP, so the per-IP auth limit tripped mid-suite (main_menu_full_flow
-> 2/12). Add RATE_LIMIT_ENABLED (default true); run_checkpoint_tests.py
sets it false for its backend. test_rate_limit.py forces it on in-fixture
so the limiter is still really tested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
- backend/tests/conftest.py: pytest_configure brings up an ephemeral
Postgres via docker compose + runs migrations when DATABASE_URL isn't
already reachable, and tears it down after. `cd backend && pytest` now
works from a clean checkout with zero manual setup (72 passed in ~28s);
no docker and no DB -> the whole suite skips with a clear message instead
of erroring 60+ times.
- scripts/verify_fresh_clone.sh: exports a pristine tree (git archive, so no
.godot), imports from scratch, headless-boots the main menu + game scene,
fails on any SCRIPT ERROR / failed autoload. Catches the stale-class-cache
class of bug. PASS.
- run_checkpoint_tests.py runs both the 2p multiplayer test and the
fresh-clone check.
- .forgejo/workflows/checkpoint.yml + docs/ci.md: the CI workflow (runs the
checkpoint suite on push/PR) and how to enable it -- it needs a Forgejo
Actions `host` runner registered on the AGP box, a one-time operator step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
- backend/app/services/game_servers.py: an in-process supervisor. One
headless Godot dedicated-server process per world, keyed by world_id,
spawned on demand from the join endpoint, port-allocated from a
configured range, with a background reaper that kills a world's server
once its status file reports 0 players for the idle timeout. Off by
default (GAME_SERVERS_ENABLED); the checkpoint/playtest launcher enables it.
- POST /api/worlds/{id}/join -> {world_id, host, port}; 404 for
unknown/hidden worlds, 503 when orchestration is disabled. Auth required.
- main.py lifespan starts/stops the supervisor.
- config.py: GODOT_BIN, GAME_PROJECT_DIR, GAME_SERVER_HOST, port range,
idle timeout.
- tests/test_world_join.py (auth / 404 / visibility / 503) -- suite 72 passed.
- checkpoint runner also runs the 2p multiplayer shell test.
Verified end to end: enabled orchestration, POST /join spawned the Godot
server, a real client connected to the returned address and moved; backend
shutdown reaped the process.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
smoke_test_network_client.gd needs a separate `godot scenes/server.tscn`
process on ENet 7777; the runner never started one, so that test has been
failing in every checkpoint run (the only failure in an otherwise-green
suite). Start it just for that test, tear it down after.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jtxAiu5dAW4WxCNdGkrkJ
docs/testing/test_inventory.md: honest, current list of what automated
coverage actually exists (38 backend pytest tests across 2 files, 0 Godot
tests merged yet) and what doesn't (no integration/endpoint tests at all,
no coverage yet for the in-review RS256/moderation work) -- verified
against a genuinely fresh checkout + fresh Postgres, not just read off the
file tree.
scripts/run_checkpoint_tests.py: runs the whole inventory in one shot --
throwaway Postgres, migrations, pytest, then starts the backend server and
runs every scripts/smoke_test_*.gd against it headlessly. Verified working
both with and without a smoke test present (temporarily tested against
PC1's not-yet-merged scripts/api_client.gd + smoke_test_api.gd, removed
before this commit -- that PR carries its own files).
docs/testing/README.md: what running this at a Package Checkpoint review
does and doesn't prove, and how to grow the inventory as real coverage
gets added.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>