Files
modbison/docs/testing/test_inventory.md
claude-checkpoint-tooling 1c64dd4967 Fix broken main_menu.gd on main + land Play->Back session persistence
Two things, found together while investigating a stuck blocker task:

1. URGENT: main_menu.gd currently fails to parse at all on main
   ("Identifier '_dob_edit' not declared in the current scope"). PR #44
   (hide-all-fields-on-auth-success) and PR #45 (DOBEdit -> Over18Check
   checkbox) each merged cleanly on their own -- Gitea's merge reported
   mergeable:true for both -- but together they left a reference to the
   old _dob_edit var on a line neither PR's diff touched, so no conflict
   was ever flagged. Same failure mode in smoke_test_main_menu_full_flow.gd
   (dob_edit, would have failed to even parse in the checkpoint suite).
   Fixed both to reference _over_18_check / over_18_check.

2. Lands the Play -> Back session-persistence fix (PR #43, task
   28357bca192d) that was stuck behind review the whole time main didn't
   have it yet: signal auth_state_ready, _show_authenticated() /
   _reset_to_logged_out() helpers, and _ready() restoring the
   authenticated UI (not resetting it) when ApiClient already holds a
   valid token. Plus the real fix a review round caught on top of that
   (task 9186405a6373): _show_authenticated() was hiding AuthPanel while
   claiming the welcome label inside it was visible -- a no-op UI-wise,
   since WelcomeLabel.visible=true does nothing once its parent is
   hidden. AuthPanel now stays visible; the regression test
   (smoke_test_main_menu_back.gd, new) asserts is_visible_in_tree(), not
   just .visible, so this class of bug can't pass silently again.

Verified against a live backend: 61/61 pytest, 7/7 Godot smoke tests
(including the new smoke_test_main_menu_back.gd, 12/12).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6rUEynmv2QmweocAQYARN
2026-08-20 18:15:32 +00:00

10 KiB

Test Inventory

What automated coverage actually exists, what it actually verifies, and what doesn't have coverage yet. Keep this current as tests are added — it's the thing a Package Checkpoint review (see docs/testing/README.md) reads first to know what "run the tests" actually checked.

Run everything below in one shot with scripts/run_checkpoint_tests.py.

Last verified against main: 2026-08-20 — full PC1 checkpoint run, migrations + pytest + all 7 Godot smoke tests + both platform exports, all clean. Real bugs found and fixed via real playtesting this same day (not caught by the checkpoint suite as it stood before today): the main menu had no way to actually open the login/register form, the world list's cards rendered at ~1px wide once real data existed, and navigating Play → Back logged the user out despite a still-valid session. All three are now covered by the smoke tests below so they can't silently regress.

Backend — backend/tests/ (pytest)

Run: cd backend && python -m pytest -v (needs DATABASE_URL pointed at a reachable Postgres — see the runner script).

Current total: 61 tests, all passing on a clean main checkout.

File Tests What it covers
test_auth_age_gate.py 11 calculate_age() date-math correctness (exact boundary, leap years, birthday-not-yet-this-year — kept as a tested utility for a future real-DOB pass even though /register no longer calls it) + RegisterRequest.over_18 field tests (true accepted, false accepted, missing rejected). 2026-08-20: the register flow was simplified from collecting/validating a date of birth to a plain client-side "I am 18 or older" confirmation, so the old date-validation parametrized cases were replaced. Backs the 18+ registration gate (PR #23, revised PC1).
test_gltf_metadata.py 21 glTF/GLB binary + JSON parsing: magic-byte detection, chunk-padding handling, mesh/animation/texture-reference metadata extraction, malformed-input rejection. Backs the asset-import pipeline (PR #27).
test_auth_endpoints.py 10 Integration tests for POST /api/auth/register and POST /api/auth/login against the real FastAPI app + real Postgres. Covers: successful register (201), duplicate username (409), duplicate email (409), over_18=false rejection (400), missing over_18 field (422), successful login with correct credentials (200), wrong password (401), nonexistent user (401), form-encoded login (OAuth2), and JWT validity of returned tokens.
test_refresh_endpoint.py 8 Integration tests for POST /api/auth/refresh. Covers: successful refresh (200, new access token, same refresh token returned), refresh produces different access tokens on repeated calls, invalid refresh token (401), empty body (422), missing field (422), refresh token from login also works, returned access token is valid JWT decodable with public key, access_token_expires_minutes in response.
test_refresh_token_jti.py 6 Unit tests for refresh token uniqueness (jti claim) and model column length. Covers: two calls with same sub produce different tokens (jti uniqueness), jti is valid UUID, rapid repeated calls all produce unique jtis, jti survives decode, RefreshToken.token column is String(512), column type is String.
test_auth_expired_token.py 5 Integration tests for expired-token rejection across auth endpoints. Covers: expired access JWT rejected by GET /api/auth/me (401), garbage access token rejected by /me (401), missing bearer token returns 401, expired refresh token (DB expires_at in past) rejected by /refresh (401), expired refresh JWT (past exp claim) rejected before DB lookup (401).

What these are, honestly: a mix of pure unit tests (test_auth_age_gate.py, test_gltf_metadata.py, test_refresh_token_jti.py) and real integration tests (test_auth_endpoints.py, test_refresh_endpoint.py, test_auth_expired_token.py) that hit the FastAPI app against a real Postgres database via TestClient. The integration tests exercise the full stack: Pydantic validation → DB → JWT → response. The suite is also re-runnable without manual cleanup — backend/tests/conftest.py has a session-scoped autouse fixture that truncates all app tables once before the suite runs.

Known coverage gaps (real gaps, not placeholders — nothing enforces filling these yet):

  • No endpoint tests for GET /api/worlds, GET /api/assets/*, or any world/asset CRUD routes — only auth endpoints have integration coverage (GET /api/worlds is covered indirectly via the Godot smoke tests below).
  • No tests for the moderation schema/migration beyond "the migration runs."
  • No tests for the GET /api/auth/me endpoint's response body (only its auth-header validation is covered via expired-token tests).
  • No tests for session management (create/validate/revoke sessions).

Godot client — scripts/smoke_test_*.gd

7 smoke tests merged into main. Each needs a live backend — AGP_API_BASE_URL env var, falling back to the application/agp/api_base_url project setting (default http://127.0.0.1:8000) if unset.

File What it covers
smoke_test_api.gd Exercises ApiClient.health_check() against a live backend. Verifies the real HTTP request path (HTTPRequest node lifecycle, JSON parse, result-dictionary contract). Exits 1 on failure.
smoke_test_auth.gd Tests the full auth flow via ApiClient autoload: register unique user (201), duplicate username (409), over_18=false registration (400), correct login (200), wrong password (401), nonexistent user (401).
smoke_test_e2e.gd End-to-end flow: health check → register → login → worlds list. Verifies the full request chain with a fresh user, including token storage and get_worlds() with auth header.
smoke_test_world_list.gd Registers a user, seeds a real world via POST /api/worlds/ (without this, run_checkpoint_tests.py's always-fresh DB means the populated-card render path never actually runs in CI, only the empty-list path would), instantiates world_list.tscn as a real scene, waits for _refresh_worlds() to complete, confirms it reached the populated (not empty) state, and asserts each rendered card's real computed width is >= 200px. That last check is the one that matters: 2026-08-20, a real bug shipped where every card existed in the tree with a non-zero child count and no crash, but rendered at 1.0px wide (ScrollContainer's size_flags_horizontal/vertical used the raw integer 4 = Control.SIZE_SHRINK_CENTER instead of 3 = SIZE_EXPAND_FILL) — invisible on screen, and a child-count-only check would never have caught it. Confirmed by deliberately reintroducing the bug: card width check correctly fails at 25px.
smoke_test_main_menu_ui.gd Instantiates main_menu.tscn, confirms the auth panel starts hidden, and confirms a real signal emission on LoginButton/RegisterButton (not calling the handler method directly) actually opens it. This is the check that would have caught 2026-08-20's real bug: the auth panel and its logic were fully correct, but nothing in the scene was ever wired to open it, so Login/Register were completely unreachable from the UI.
smoke_test_main_menu_full_flow.gd Full click-through: open the register form, fill it exactly as a user would, submit, confirm Play actually becomes enabled, the welcome message shows, and the auth buttons hide themselves. Calls the submit handler directly (awaited) rather than via auth_button.pressed.emit() — see the note at the top of the file: a synthetic .emit() on an await-using handler doesn't get driven forward reliably in --script/SceneTree headless mode the way real GUI input event dispatch does, confirmed by comparing direct-call vs .emit() behavior. Not a bug in the actual click-driven flow, just a headless-test-harness quirk to route around when writing tests.
smoke_test_main_menu_back.gd Regression test for Play → Back logging the user out even with a valid token still held. Registers a user, loads main_menu.tscn (asserts it restores the authenticated state via a real auth_state_ready signal wait, not a frame-count guess), clicks Play → world_list.tscn, clicks Back → main_menu.tscn, and asserts the menu is still authenticated. Checks WelcomeLabel.is_visible_in_tree(), not just .visible — the label lives inside AuthPanel, so .visible alone is a false positive if the panel itself is hidden (a real bug briefly present: _show_authenticated() hid AuthPanel while claiming the welcome label was visible, which rendered nothing).

Run (once, to build the class cache): godot --headless --path . --import, then for each smoke test: godot --headless --path . --script res://scripts/smoke_test_<name>.gd — no --quit, it kills these before their awaited HTTP calls finish.

Known gaps:

  • No smoke tests for chunk loading/streaming (chunk.gd, chunk_scene.gd).
  • No smoke tests for dynamic/static object nodes.
  • All smoke tests require a live backend; no offline/unit smoke tests exist.
  • smoke_test_world_list.gd and smoke_test_main_menu_*.gd are the only ones that exercise a real .tscn scene instantiation — others only test the ApiClient autoload directly.
  • get_me() (GET /auth/me) is implemented in scripts/api_client.gd but has no smoke coverage yet.
  • The Login path (as opposed to Register) through main_menu.gd isn't separately smoke-tested — it shares the same _on_auth_button_pressed()/ _on_auth_complete() code path already exercised for Register, but that's inference, not a direct check.

Client export

scripts/build.sh produces real Linux (agp-game.x86_64) and Windows (agp-game.exe) binaries via --export-release. Verified 2026-08-20: both exports complete clean from a fresh checkout and produce valid binaries (confirmed via file — real ELF64/PE32+, not empty or truncated). Neither export is exercised by an automated GUI test (headless Godot can't drive a window) — actually launching and clicking through the exported binary is a manual step, see docs/checkpoints/PC1.md for playtest instructions.

What "full coverage" should eventually mean here

Not defined precisely yet — the honest target as of 2026-08-20 is "integration tests for every endpoint + smoke tests for every major scene." As real integration/endpoint tests get added, update the tables above rather than letting this doc drift stale; a wrong inventory is worse than no inventory.