Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 33b0c3f1fb | |||
| dc0c133af8 | |||
| 8f1a1110e2 | |||
| 12fef51020 | |||
| af50819a16 | |||
| 16bb87c6b0 | |||
| 8fc9d378e4 | |||
| 3679e4dbc4 | |||
| c33b10ac46 | |||
| 8c54129bcd | |||
| 409e78baa5 | |||
| f8e5de9293 | |||
| 3a7b2ab306 |
@@ -0,0 +1,72 @@
|
||||
# Package Checkpoint 1 (PC1)
|
||||
|
||||
Validated 2026-08-20 against `main` commit `927f9bd`, from a genuinely fresh
|
||||
clone (not a reused agent workspace).
|
||||
|
||||
## Automated validation
|
||||
|
||||
Run with `scripts/run_checkpoint_tests.py` — see
|
||||
`docs/testing/test_inventory.md` for exactly what each suite covers.
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Alembic migrations, fresh DB | PASS |
|
||||
| Backend pytest | 68/68 PASS |
|
||||
| Godot smoke tests (4 files) | ALL PASS |
|
||||
| Linux export (`agp-game.x86_64`) | Builds, verified as a real ELF64 binary |
|
||||
| Windows export (`agp-game.exe`) | Builds, verified as a real PE32+ binary |
|
||||
|
||||
## Problems found during this checkpoint (fixed, tracked as tasks)
|
||||
|
||||
- Roadmap showed the Godot Engine Scaffold feature amber despite 6/6 tasks
|
||||
done — a historical data gap (a review-feedback follow-up task's
|
||||
supersession link was never recorded, predating when that field started
|
||||
being tracked). Backfilled in the PM database; confirmed green.
|
||||
- `docs/testing/test_inventory.md` was stale (said 38 backend tests,
|
||||
actually 68; said no integration tests existed, 4 real ones had landed).
|
||||
Fixed via task `398bf1a22a2d` / PR #40.
|
||||
|
||||
## What's actually testable right now
|
||||
|
||||
Login/register/browse-worlds only — there's no joinable gameplay yet. That's
|
||||
the real scope of PC1; don't expect more when playtesting.
|
||||
|
||||
- Register a new account (18+ only, rejected otherwise)
|
||||
- Log out, log back in with the same credentials
|
||||
- Play → Browse Worlds — loads a (currently empty) world list with no errors
|
||||
|
||||
## Manual playtest: backend on cloudbison, client on pinkbison
|
||||
|
||||
As of 2026-08-20 this is dashboard-controlled — no terminal commands, no
|
||||
env vars, no editing files. Foolproof enough to hand to someone else.
|
||||
|
||||
### 1. On cloudbison — the AGP dashboard
|
||||
|
||||
Open the dashboard (`http://cloudbison:7070`, or its Tailscale address) and
|
||||
go to **PC1** in the nav bar (`/checkpoint/PC1`). Click **Start Backend** —
|
||||
it pulls the latest `main`, stands up Postgres, runs migrations, and starts
|
||||
the API server, all in one step. The page shows the LAN address it's
|
||||
reachable at once it's up (under a minute).
|
||||
|
||||
When you're done playtesting, click **Stop & Clean Up** on the same page —
|
||||
it tears down everything that step started (API server, Postgres
|
||||
container, its volume and network). Nothing is left running or lying
|
||||
around afterward, even if the dashboard itself gets restarted in between.
|
||||
|
||||
### 2. On pinkbison — download and run the .exe
|
||||
|
||||
Same dashboard page: click **Build .exe** the first time (or **Rebuild** to
|
||||
pick up newer changes) — this pulls the latest `main` and exports a Windows
|
||||
binary with cloudbison's backend address already baked in. Click
|
||||
**Download agp-game.exe**, copy it to pinkbison, and just run it. No
|
||||
environment variable, no config file, no build step on pinkbison at all.
|
||||
|
||||
### 3. Tear down when done
|
||||
|
||||
**Stop & Clean Up** on the dashboard page. That's it.
|
||||
|
||||
## Status
|
||||
|
||||
`config/checkpoint_focus_feature_ids` in the AGP framework (`~/agp`) still
|
||||
restricts agent claiming to the 5 PC1-scope features. Agents will not move
|
||||
on to post-PC1 work until that's widened.
|
||||
@@ -7,9 +7,13 @@ 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 (commit `927f9bd`) — full PC1
|
||||
checkpoint run, fresh clone, migrations + pytest + all 4 Godot smoke tests +
|
||||
both platform exports, all clean.
|
||||
Last verified against `main`: 2026-08-20 — full PC1 checkpoint run,
|
||||
migrations + pytest + all 6 Godot smoke tests + both platform exports, all
|
||||
clean. Two 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, and the world
|
||||
list's cards rendered at ~1px wide once real data existed. Both are now
|
||||
covered by the smoke tests below so they can't silently regress.
|
||||
|
||||
## Backend — `backend/tests/` (pytest)
|
||||
|
||||
@@ -48,7 +52,7 @@ these yet):
|
||||
|
||||
## Godot client — `scripts/smoke_test_*.gd`
|
||||
|
||||
**4 smoke tests merged into `main`.** Each needs a live backend —
|
||||
**6 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.
|
||||
@@ -58,7 +62,9 @@ these yet):
|
||||
| `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), under-18 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` | Instantiates `world_list.tscn` as a real scene, adds to tree (catches `@onready` node-path bugs), waits for `_refresh_worlds()` to complete, confirms the screen reaches a real end state (world cards, empty-list message, or error label — not stuck loading). |
|
||||
| `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. |
|
||||
|
||||
Run (once, to build the class cache): `godot --headless --path . --import`,
|
||||
then for each smoke test: `godot --headless --path . --script
|
||||
@@ -66,14 +72,18 @@ res://scripts/smoke_test_<name>.gd` — **no `--quit`**, it kills these before
|
||||
their awaited HTTP calls finish.
|
||||
|
||||
**Known gaps:**
|
||||
- No smoke tests for the main menu scene (`main_menu.tscn`).
|
||||
- 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` is the only one that exercises a real `.tscn`
|
||||
scene instantiation — others only test the `ApiClient` autoload.
|
||||
- `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
|
||||
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ script_export_mode=2
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
@@ -52,7 +52,7 @@ script_export_mode=2
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=false
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
texture_format/etc=false
|
||||
|
||||
+17
-5
@@ -29,6 +29,21 @@ text = "AGP Game"
|
||||
horizontal_alignment = 1
|
||||
theme_override_font_sizes/font_size = 72
|
||||
|
||||
[node name="AuthButtonsRow" type="HBoxContainer" parent="CenterContainer/VBoxContainer"]
|
||||
layout_mode = 1
|
||||
theme_override_constants/separation = 16
|
||||
alignment = 1
|
||||
|
||||
[node name="LoginButton" type="Button" parent="CenterContainer/VBoxContainer/AuthButtonsRow"]
|
||||
layout_mode = 1
|
||||
text = "Login"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="RegisterButton" type="Button" parent="CenterContainer/VBoxContainer/AuthButtonsRow"]
|
||||
layout_mode = 1
|
||||
text = "Register"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="AuthPanel" type="PanelContainer" parent="CenterContainer/VBoxContainer"]
|
||||
layout_mode = 1
|
||||
visible = false
|
||||
@@ -74,10 +89,6 @@ visible = false
|
||||
placeholder_text = "Date of Birth (YYYY-MM-DD)"
|
||||
size_flags_horizontal = 3
|
||||
|
||||
[node name="ModeToggle" type="CheckButton" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = "Register"
|
||||
|
||||
[node name="AuthButton" type="Button" parent="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox"]
|
||||
layout_mode = 1
|
||||
text = "Login"
|
||||
@@ -106,9 +117,10 @@ layout_mode = 1
|
||||
text = "Quit"
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton" to="." method="_on_login_button_pressed"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton" to="." method="_on_register_button_pressed"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/PlayButton" to="." method="_on_play_button_pressed"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/QuitButton" to="." method="_on_quit_button_pressed"]
|
||||
[connection signal="toggled" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle" to="." method="_on_mode_toggle_toggled"]
|
||||
[connection signal="pressed" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton" to="." method="_on_auth_button_pressed"]
|
||||
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit" to="." method="_on_auth_button_pressed"]
|
||||
[connection signal="text_submitted" from="CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit" to="." method="_on_auth_button_pressed"]
|
||||
|
||||
+11
-9
@@ -11,14 +11,17 @@ grow_vertical = 2
|
||||
script = ExtResource("1_wl")
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||
layout_mode = 3
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
theme_override_constants/separation = 8
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
|
||||
[node name="HeaderHBox" type="HBoxContainer" parent="VBoxContainer"]
|
||||
layout_mode = 3
|
||||
size_flags_horizontal = 4
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 16
|
||||
|
||||
[node name="BackButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||
@@ -30,7 +33,7 @@ size_flags_horizontal = 0
|
||||
layout_mode = 3
|
||||
text = "Browse Worlds"
|
||||
horizontal_alignment = 1
|
||||
size_flags_horizontal = 4
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 28
|
||||
|
||||
[node name="RefreshButton" type="Button" parent="VBoxContainer/HeaderHBox"]
|
||||
@@ -40,15 +43,14 @@ size_flags_horizontal = 0
|
||||
|
||||
[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"]
|
||||
layout_mode = 3
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
horizontal_mode = 2
|
||||
vertical_mode = 2
|
||||
|
||||
[node name="WorldList" type="VBoxContainer" parent="VBoxContainer/ScrollContainer"]
|
||||
layout_mode = 3
|
||||
size_flags_horizontal = 4
|
||||
size_flags_vertical = 4
|
||||
size_flags_horizontal = 3
|
||||
theme_override_constants/separation = 8
|
||||
|
||||
[node name="LoadingLabel" type="Label" parent="VBoxContainer"]
|
||||
|
||||
+37
-30
@@ -1,24 +1,27 @@
|
||||
extends Control
|
||||
## Main menu with login/register UI wired to the backend auth API.
|
||||
##
|
||||
## The AuthPanel is hidden by default. When the user clicks "Login" or "Register"
|
||||
## (the existing Play/Quit buttons), the auth panel slides in. The ModeToggle
|
||||
## switches between Login and Register modes, showing/hiding the email and DOB
|
||||
## fields accordingly.
|
||||
## The AuthPanel is hidden by default. When the user clicks the LoginButton or
|
||||
## RegisterButton (AuthButtonsRow, above the panel), the auth panel slides in.
|
||||
## The Login and Register buttons on the AuthButtonsRow directly set the panel's
|
||||
## mode, so the user can switch between Login and Register at any time while
|
||||
## the panel is open.
|
||||
##
|
||||
## On successful auth the token is stored via ApiClient, the welcome label
|
||||
## shows the username, and the Play button becomes enabled.
|
||||
## On successful auth the token is stored via ApiClient, AuthButtonsRow hides,
|
||||
## the welcome label shows the username, and the Play button becomes enabled.
|
||||
## The auth form fields are hidden, leaving only the welcome message visible.
|
||||
|
||||
# ─── UI references ────────────────────────────────────────────────────────────
|
||||
|
||||
# ── UI references ────────────────────────────────────────────────────────────
|
||||
|
||||
var _auth_buttons_row: HBoxContainer
|
||||
var _login_button: Button
|
||||
var _register_button: Button
|
||||
var _auth_panel: PanelContainer
|
||||
var _auth_mode_label: Label
|
||||
var _username_edit: LineEdit
|
||||
var _password_edit: LineEdit
|
||||
var _email_edit: LineEdit
|
||||
var _dob_edit: LineEdit
|
||||
var _mode_toggle: CheckButton
|
||||
var _auth_button: Button
|
||||
var _status_label: Label
|
||||
var _welcome_label: Label
|
||||
@@ -29,17 +32,19 @@ var _is_register_mode: bool = false
|
||||
var _is_authenticating: bool = false
|
||||
|
||||
|
||||
# ── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
# ─── Lifecycle ────────────────────────────────────────────────────────────────
|
||||
|
||||
func _ready() -> void:
|
||||
# Cache node references.
|
||||
_auth_buttons_row = $CenterContainer/VBoxContainer/AuthButtonsRow
|
||||
_login_button = $CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton
|
||||
_register_button = $CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton
|
||||
_auth_panel = $CenterContainer/VBoxContainer/AuthPanel
|
||||
_auth_mode_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel
|
||||
_username_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit
|
||||
_password_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit
|
||||
_email_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit
|
||||
_dob_edit = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit
|
||||
_mode_toggle = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle
|
||||
_auth_button = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton
|
||||
_status_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthStatusLabel
|
||||
_welcome_label = $CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel
|
||||
@@ -56,7 +61,7 @@ func _ready() -> void:
|
||||
_update_auth_mode_ui()
|
||||
|
||||
|
||||
# ── Play / Quit ──────────────────────────────────────────────────────────────
|
||||
# ─── Play / Quit ──────────────────────────────────────────────────────────────
|
||||
|
||||
func _on_play_button_pressed() -> void:
|
||||
if _play_button.disabled:
|
||||
@@ -70,29 +75,24 @@ func _on_quit_button_pressed() -> void:
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
# ── Auth panel toggle ────────────────────────────────────────────────────────
|
||||
# ─── Auth panel toggle ────────────────────────────────────────────────────────
|
||||
|
||||
func _on_login_button_pressed() -> void:
|
||||
"""Show the auth panel (called from a future Login button on the main menu)."""
|
||||
"""Show the auth panel in login mode, called from AuthButtonsRow/LoginButton."""
|
||||
_auth_panel.visible = true
|
||||
_auth_panel.grab_focus()
|
||||
_is_register_mode = false
|
||||
_update_auth_mode_ui()
|
||||
_clear_status()
|
||||
_username_edit.grab_focus()
|
||||
|
||||
|
||||
func _on_register_button_pressed() -> void:
|
||||
"""Show the auth panel in register mode."""
|
||||
"""Show the auth panel in register mode, called from AuthButtonsRow/RegisterButton."""
|
||||
_auth_panel.visible = true
|
||||
_is_register_mode = true
|
||||
_mode_toggle.button_pressed = true
|
||||
_update_auth_mode_ui()
|
||||
_clear_status()
|
||||
|
||||
|
||||
# ── Mode toggle (Login / Register) ───────────────────────────────────────────
|
||||
|
||||
func _on_mode_toggle_toggled(button_pressed: bool) -> void:
|
||||
_is_register_mode = button_pressed
|
||||
_update_auth_mode_ui()
|
||||
_clear_status()
|
||||
_username_edit.grab_focus()
|
||||
|
||||
|
||||
func _update_auth_mode_ui() -> void:
|
||||
@@ -109,7 +109,7 @@ func _update_auth_mode_ui() -> void:
|
||||
_dob_edit.visible = false
|
||||
|
||||
|
||||
# ── Auth submission ──────────────────────────────────────────────────────────
|
||||
# ─── Auth submission ──────────────────────────────────────────────────────────
|
||||
|
||||
func _on_auth_button_pressed() -> void:
|
||||
"""Handle login or register button press."""
|
||||
@@ -166,18 +166,25 @@ func _on_auth_complete(result: Dictionary) -> void:
|
||||
_is_authenticating = false
|
||||
|
||||
if result.ok:
|
||||
# Success — show welcome, enable Play.
|
||||
# Success — hide the auth form fields, show only the welcome label.
|
||||
_username_edit.visible = false
|
||||
_password_edit.visible = false
|
||||
_email_edit.visible = false
|
||||
_dob_edit.visible = false
|
||||
_auth_button.visible = false
|
||||
_auth_mode_label.visible = false
|
||||
_status_label.visible = false
|
||||
_welcome_label.text = "Welcome, %s!" % _username_edit.text.strip_edges()
|
||||
_welcome_label.visible = true
|
||||
_status_label.visible = false
|
||||
_play_button.disabled = false
|
||||
_auth_buttons_row.visible = false
|
||||
print("Auth successful. Token stored.")
|
||||
else:
|
||||
# Failure — show backend error detail.
|
||||
_show_status(result.detail)
|
||||
|
||||
|
||||
# ── Status helpers ───────────────────────────────────────────────────────────
|
||||
# ─── Status helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
func _show_status(message: String) -> void:
|
||||
_status_label.text = message
|
||||
@@ -189,7 +196,7 @@ func _clear_status() -> void:
|
||||
_status_label.visible = false
|
||||
|
||||
|
||||
# ── Validation ───────────────────────────────────────────────────────────────
|
||||
# ─── Validation ───────────────────────────────────────────────────────────────
|
||||
|
||||
func _is_valid_date(date_str: String) -> bool:
|
||||
"""Check if a string looks like a valid YYYY-MM-DD date."""
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
extends SceneTree
|
||||
## Full click-through simulation of the real user flow: press Register,
|
||||
## fill the form exactly as a human would, submit, confirm Play actually
|
||||
## becomes clickable. Also verifies that after successful auth the auth
|
||||
## form fields are hidden, leaving only the welcome message visible.
|
||||
|
||||
var passed := 0
|
||||
var failed := 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
var packed: PackedScene = load("res://scenes/main_menu.tscn")
|
||||
var instance: Control = packed.instantiate()
|
||||
root.add_child(instance)
|
||||
await process_frame
|
||||
|
||||
var register_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton")
|
||||
var auth_buttons_row: HBoxContainer = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow")
|
||||
var username_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/UsernameEdit")
|
||||
var password_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/PasswordEdit")
|
||||
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
||||
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit")
|
||||
var auth_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthButton")
|
||||
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
||||
var welcome_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/WelcomeLabel")
|
||||
var play_button: Button = instance.get_node("CenterContainer/VBoxContainer/PlayButton")
|
||||
|
||||
_check("Play starts disabled", play_button.disabled)
|
||||
|
||||
# Click Register -- a real signal emission is fine here since
|
||||
# _on_register_button_pressed() is synchronous (no await), same as a
|
||||
# real click's GUI event dispatch would drive it.
|
||||
register_button.pressed.emit()
|
||||
await process_frame
|
||||
|
||||
_check("auth panel opened in register mode", instance.get_node("CenterContainer/VBoxContainer/AuthPanel").visible)
|
||||
|
||||
# Fill the form exactly as a user would type into it.
|
||||
var unique_id := int(Time.get_unix_time_from_system() * 1000)
|
||||
username_edit.text = "mainmenu_e2e_%d" % unique_id
|
||||
password_edit.text = "TestPass123!"
|
||||
email_edit.text = "mainmenu_e2e_%d@test.example.com" % unique_id
|
||||
dob_edit.text = "2000-01-01"
|
||||
|
||||
# Call the submit handler directly (awaited) rather than via
|
||||
# auth_button.pressed.emit() -- a synthetic .emit() in --script/
|
||||
# SceneTree headless mode doesn't drive an await-using handler's
|
||||
# coroutine forward the same way real GUI input event dispatch does
|
||||
# (confirmed via a direct-call vs emit() comparison: the direct call
|
||||
# always resolves, the synthetic emit sometimes doesn't advance past
|
||||
# its own await point even after 50 real process_frame waits). This
|
||||
# is a headless-mode test-harness limitation, not a bug in the actual
|
||||
# click-driven flow.
|
||||
await instance._on_auth_button_pressed()
|
||||
|
||||
_check("Play became enabled after real register submission", not play_button.disabled)
|
||||
_check("Welcome label visible", welcome_label.visible)
|
||||
_check("Auth buttons row hid itself after success", not auth_buttons_row.visible)
|
||||
|
||||
# Verify the auth form fields are hidden after successful auth,
|
||||
# leaving only the welcome message visible within the panel.
|
||||
_check("Username edit hidden after auth", not username_edit.visible)
|
||||
_check("Password edit hidden after auth", not password_edit.visible)
|
||||
_check("Email edit hidden after auth", not email_edit.visible)
|
||||
_check("DOB edit hidden after auth", not dob_edit.visible)
|
||||
_check("Auth button hidden after auth", not auth_button.visible)
|
||||
_check("Auth mode label hidden after auth", not auth_mode_label.visible)
|
||||
_check("Welcome label contains username", welcome_label.text.begins_with("Welcome, mainmenu_e2e_"))
|
||||
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
quit(0 if failed == 0 else 1)
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> void:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
passed += 1
|
||||
else:
|
||||
printerr(" FAIL: ", description)
|
||||
failed += 1
|
||||
@@ -0,0 +1,126 @@
|
||||
extends SceneTree
|
||||
## Verifies the main menu's Login/Register buttons correctly open the auth
|
||||
## panel and toggle between Login and Register modes via signal emission.
|
||||
##
|
||||
## Key tests:
|
||||
## - AuthPanel starts hidden.
|
||||
## - LoginButton press opens the panel in Login mode.
|
||||
## - RegisterButton press opens the panel in Register mode.
|
||||
## - Clicking Login while in Register mode switches back to Login mode.
|
||||
## - Clicking Register while in Login mode switches to Register mode.
|
||||
## - The ModeToggle CheckButton no longer exists in the scene.
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
await process_frame
|
||||
|
||||
var passed := 0
|
||||
var failed := 0
|
||||
|
||||
var packed: PackedScene = load("res://scenes/main_menu.tscn")
|
||||
var instance: Control = packed.instantiate()
|
||||
root.add_child(instance)
|
||||
await process_frame
|
||||
|
||||
var login_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/LoginButton")
|
||||
var register_button: Button = instance.get_node("CenterContainer/VBoxContainer/AuthButtonsRow/RegisterButton")
|
||||
var auth_panel: PanelContainer = instance.get_node("CenterContainer/VBoxContainer/AuthPanel")
|
||||
var auth_mode_label: Label = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/AuthModeLabel")
|
||||
var email_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/EmailEdit")
|
||||
var dob_edit: LineEdit = instance.get_node("CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/DOBEdit")
|
||||
|
||||
# --- Test 1: auth panel starts hidden ---
|
||||
if _check("auth panel starts hidden", not auth_panel.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 2: LoginButton press opens panel in Login mode ---
|
||||
login_button.pressed.emit()
|
||||
await process_frame
|
||||
|
||||
if _check("auth panel visible after LoginButton press", auth_panel.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("Login mode label after LoginButton press", auth_mode_label.text == "Login"):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("Email field hidden in Login mode", not email_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("DOB field hidden in Login mode", not dob_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 3: RegisterButton press switches to Register mode ---
|
||||
register_button.pressed.emit()
|
||||
await process_frame
|
||||
|
||||
if _check("auth panel still visible after RegisterButton press", auth_panel.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("Register mode label after RegisterButton press", auth_mode_label.text == "Register"):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("Email field visible in Register mode", email_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("DOB field visible in Register mode", dob_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 4: LoginButton press switches back to Login mode ---
|
||||
login_button.pressed.emit()
|
||||
await process_frame
|
||||
|
||||
if _check("Login mode label after second LoginButton press", auth_mode_label.text == "Login"):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("Email field hidden after switching back to Login", not email_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
if _check("DOB field hidden after switching back to Login", not dob_edit.visible):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Test 5: ModeToggle CheckButton no longer exists ---
|
||||
var mode_toggle_path = "CenterContainer/VBoxContainer/AuthPanel/MarginContainer/AuthVBox/ModeToggle"
|
||||
var has_mode_toggle = instance.has_node(mode_toggle_path)
|
||||
if _check("ModeToggle CheckButton removed from scene", not has_mode_toggle):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
quit(0 if failed == 0 else 1)
|
||||
|
||||
|
||||
func _check(description: String, condition: bool) -> bool:
|
||||
if condition:
|
||||
print(" PASS: ", description)
|
||||
else:
|
||||
printerr(" FAIL: ", description)
|
||||
return condition
|
||||
@@ -16,15 +16,27 @@ extends SceneTree
|
||||
##
|
||||
## Tests:
|
||||
## 1. Register + log in a fresh user (so get_worlds() has a valid token).
|
||||
## 2. Instantiate world_list.tscn, add to tree -- @onready resolution
|
||||
## 2. POST a real world via the API so the populated-card path actually
|
||||
## gets exercised -- run_checkpoint_tests.py always starts from a
|
||||
## genuinely empty DB, so without this the card-rendering code below
|
||||
## would never run in CI at all, only the empty-list path would.
|
||||
## 3. Instantiate world_list.tscn, add to tree -- @onready resolution
|
||||
## must not error.
|
||||
## 3. Wait for _refresh_worlds()'s awaited request to complete.
|
||||
## 4. Confirm the screen reached a real end state (world cards, or the
|
||||
## empty-list message -- either is fine, a crash/stuck-loading isn't).
|
||||
## 4. Wait for _refresh_worlds()'s awaited request to complete.
|
||||
## 5. Confirm it actually reached the populated (not empty) state, and
|
||||
## that the rendered card has a real width -- not just non-zero
|
||||
## child_count. 2026-08-20: a real bug shipped where every card
|
||||
## technically existed in the tree (child_count > 0, no error, no
|
||||
## crash) but rendered at 1.0px wide because size_flags_horizontal/
|
||||
## vertical used the raw integer 4 (Control.SIZE_SHRINK_CENTER)
|
||||
## instead of 3 (Control.SIZE_EXPAND_FILL) on ScrollContainer -- a
|
||||
## child-count check alone would never catch that, only a real size
|
||||
## assertion does.
|
||||
##
|
||||
## Exits 0 on full pass, 1 on any failure.
|
||||
|
||||
const WORLD_LIST_SCENE := "res://scenes/world_list.tscn"
|
||||
const MIN_SANE_CARD_WIDTH := 200.0 # a real screen is at minimum a few hundred px wide
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -52,6 +64,19 @@ func _run() -> void:
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# --- Seed a real world so the populated-card path is actually exercised ---
|
||||
print("\n=== Setup: seed a real world ===")
|
||||
var world_body := JSON.stringify({
|
||||
"name": "Smoke Test World %d" % unique_id,
|
||||
"description": "Seeded by smoke_test_world_list.gd to exercise the populated-card render path.",
|
||||
"visibility": "public",
|
||||
})
|
||||
var create_result: Dictionary = await api.request(HTTPClient.METHOD_POST, "/api/worlds/", world_body, "application/json", true)
|
||||
if not _check("setup: world created", create_result.ok):
|
||||
print(" detail: ", create_result.detail)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
# --- Test 1: Instantiate the real scene ---
|
||||
print("\n=== Test 1: Instantiate world_list.tscn ===")
|
||||
var packed_scene: PackedScene = load(WORLD_LIST_SCENE)
|
||||
@@ -99,10 +124,31 @@ func _run() -> void:
|
||||
|
||||
if error_label.visible:
|
||||
printerr(" Note: ended in error state: ", error_label.text)
|
||||
elif world_list_node.visible:
|
||||
print(" Note: rendered %d world card(s)" % world_list_node.get_child_count())
|
||||
elif empty_label.visible:
|
||||
print(" Note: ended in empty-list state")
|
||||
# We just seeded a world -- ending up empty means GET /api/worlds
|
||||
# isn't actually seeing what POST /api/worlds/ just created.
|
||||
_check("ended in populated state, not empty (a world was just seeded)", false)
|
||||
failed += 1
|
||||
elif world_list_node.visible:
|
||||
var card_count := world_list_node.get_child_count()
|
||||
print(" Note: rendered %d world card(s)" % card_count)
|
||||
|
||||
if _check("at least one card rendered", card_count > 0):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# The actual bug this test exists to catch: a card can be present
|
||||
# in the tree, not crash, and still be visually nothing because
|
||||
# it rendered at ~1px wide. Check every card's real computed size,
|
||||
# not just that it exists.
|
||||
for i in range(card_count):
|
||||
var card: Control = world_list_node.get_child(i)
|
||||
var wide_enough := card.size.x >= MIN_SANE_CARD_WIDTH
|
||||
if _check("card %d has a real width (%.0fpx >= %.0fpx)" % [i, card.size.x, MIN_SANE_CARD_WIDTH], wide_enough):
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
# --- Summary ---
|
||||
print("\n=== SMOKE RESULT: %d/%d passed ===" % [passed, passed + failed])
|
||||
|
||||
+19
-12
@@ -88,10 +88,26 @@ func _refresh_worlds() -> void:
|
||||
|
||||
## Create a visual card for a single world entry.
|
||||
func _create_world_card(world_dict: Dictionary) -> Control:
|
||||
# PanelContainer for the background, not a ColorRect moved to child index
|
||||
# 0 of a VBoxContainer -- a VBoxContainer lays out ALL its children in a
|
||||
# vertical sequence, it has no concept of "float this one child behind
|
||||
# the others as an overlay". That version rendered as an empty dark bar
|
||||
# sitting above the text, not a background behind it.
|
||||
var panel := PanelContainer.new()
|
||||
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
panel.custom_minimum_size = Vector2(0, 80)
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = Color(0.15, 0.15, 0.15, 1.0)
|
||||
style.content_margin_left = 12
|
||||
style.content_margin_right = 12
|
||||
style.content_margin_top = 8
|
||||
style.content_margin_bottom = 8
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var card := VBoxContainer.new()
|
||||
card.size_flags_horizontal = Control.SIZE_FILL
|
||||
card.custom_minimum_size = Vector2(0, 80)
|
||||
card.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
card.add_theme_constant_override("separation", 4)
|
||||
panel.add_child(card)
|
||||
|
||||
var name_label := Label.new()
|
||||
name_label.text = world_dict.get("name", "Unnamed World")
|
||||
@@ -123,13 +139,4 @@ func _create_world_card(world_dict: Dictionary) -> Control:
|
||||
card.add_child(desc_label)
|
||||
card.add_child(meta_label)
|
||||
|
||||
# Subtle background for the card
|
||||
var bg := ColorRect.new()
|
||||
bg.color = Color(0.15, 0.15, 0.15, 1.0)
|
||||
bg.size_flags_horizontal = Control.SIZE_FILL
|
||||
bg.size_flags_vertical = Control.SIZE_FILL
|
||||
bg.custom_minimum_size = Vector2(0, 80)
|
||||
card.add_child(bg)
|
||||
card.move_child(bg, 0)
|
||||
|
||||
return card
|
||||
return panel
|
||||
|
||||
Reference in New Issue
Block a user