From 477dc97cee023a84fa2583fdfb707a35f8dfe7c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:26:54 +0000 Subject: [PATCH 01/32] Bump actions/upload-artifact from 6 to 7 Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/browser_test.yml | 6 +++--- .github/workflows/gdunit4_tests.yml | 2 +- .github/workflows/gut_tests.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/browser_test.yml b/.github/workflows/browser_test.yml index 20fa46b7..63aa0c52 100644 --- a/.github/workflows/browser_test.yml +++ b/.github/workflows/browser_test.yml @@ -150,7 +150,7 @@ jobs: - name: "Upload LCOV Artifact" if: always() - uses: "actions/upload-artifact@v6" + uses: "actions/upload-artifact@v7" with: name: lcov-report path: "./coverage/lcov/lcov.info" @@ -176,14 +176,14 @@ jobs: - name: "Upload Test Report Artifact" if: always() - uses: "actions/upload-artifact@v6" + uses: "actions/upload-artifact@v7" with: name: test-report path: junit.xml - name: "Upload Screenshot and Coverage Artifacts" if: always() - uses: "actions/upload-artifact@v6" + uses: "actions/upload-artifact@v7" with: name: test-screenshots path: | diff --git a/.github/workflows/gdunit4_tests.yml b/.github/workflows/gdunit4_tests.yml index 7fc8fd34..f782f413 100644 --- a/.github/workflows/gdunit4_tests.yml +++ b/.github/workflows/gdunit4_tests.yml @@ -51,7 +51,7 @@ jobs: ls -la ${{ steps.find_report.outputs.latest_report }}/ - name: "Upload Test Reports Artifacts" if: always() - uses: "actions/upload-artifact@v6" + uses: "actions/upload-artifact@v7" with: name: gdunit-reports path: reports/** diff --git a/.github/workflows/gut_tests.yml b/.github/workflows/gut_tests.yml index ec662dc0..8bd40ca2 100644 --- a/.github/workflows/gut_tests.yml +++ b/.github/workflows/gut_tests.yml @@ -53,7 +53,7 @@ jobs: ls -la ${{ steps.find_report.outputs.latest_report }}/ - name: "Upload Test Reports Artifacts" if: always() - uses: "actions/upload-artifact@v6" + uses: "actions/upload-artifact@v7" with: name: gut-reports path: gut-reports/** From 9650b1129fef8f57f79e68dfb6963ea872f4eabd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 09:28:10 +0000 Subject: [PATCH 02/32] Bump github/codeql-action from 4.32.3 to 4.32.4 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.32.3 to 4.32.4. - [Release notes](https://github.com/github/codeql-action/releases) - [Commits](https://github.com/github/codeql-action/compare/v4.32.3...v4.32.4) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.32.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/snyk.yml | 4 ++-- .github/workflows/trivy.yml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 07a1ea12..2f8896c0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -62,7 +62,7 @@ jobs: run: | bash ./.github/scripts/patch_index_js.sh "export/web" - name: "Initialize CodeQL" - uses: "github/codeql-action/init@v4.32.3" + uses: "github/codeql-action/init@v4.32.4" with: # yamllint disable rule:quoted-strings languages: ${{ matrix.language }} @@ -74,10 +74,10 @@ jobs: debug: "false" - name: "Autobuild (optional for JS but included for completeness)" - uses: "github/codeql-action/autobuild@v4.32.3" + uses: "github/codeql-action/autobuild@v4.32.4" - name: "Perform CodeQL Analysis" - uses: "github/codeql-action/analyze@v4.32.3" + uses: "github/codeql-action/analyze@v4.32.4" - name: "Post-scan summary (optional)" if: "always()" # Run even if previous steps fail diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 11b8f8de..ccf0d067 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -43,7 +43,7 @@ jobs: continue-on-error: true - name: "Upload Snyk Code SARIF to GitHub" - uses: "github/codeql-action/upload-sarif@ef618feace3c4838ae42b239ab86e8fb46437508" + uses: "github/codeql-action/upload-sarif@0ec47d036c68ae0cf94c629009b1029407111281" if: "always() && hashFiles('snyk-code.sarif') != ''" with: sarif_file: "snyk-code.sarif" @@ -54,7 +54,7 @@ jobs: continue-on-error: true - name: "Upload Snyk Open Source SARIF to GitHub" - uses: "github/codeql-action/upload-sarif@ef618feace3c4838ae42b239ab86e8fb46437508" + uses: "github/codeql-action/upload-sarif@0ec47d036c68ae0cf94c629009b1029407111281" if: "always() && hashFiles('snyk-os.sarif') != ''" with: sarif_file: "snyk-os.sarif" diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 6de2c574..8d3d30df 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -36,7 +36,7 @@ jobs: ignore-unfixed: true # Ignore vulns without fixes available - name: "Upload Trivy scan results to GitHub Security tab" - uses: "github/codeql-action/upload-sarif@ef618feace3c4838ae42b239ab86e8fb46437508" # Pinned to SHA for v4.32.2 + uses: "github/codeql-action/upload-sarif@0ec47d036c68ae0cf94c629009b1029407111281" # Pinned to SHA for v4.32.2 if: always() # Upload even if scan fails with: sarif_file: 'trivy-results.sarif' From 21e5a02de68340eb2497d09f81f52e4d33d8f23c Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:39:51 -0800 Subject: [PATCH 03/32] [BUG] SCRIPT ERROR: Parse Error: Could not resolve external class member "settings". #444 Based on the logs and files, moving those resources to res://config_resources/ and breaking that circular dependency between the Globals and the GameSettingsResource was the key to stabilizing the engine's compilation process. By resolving the naming conflict between the folder and the Autoload, we've ensured that Godot can clearly distinguish between the static Settings singleton and the actual filesystem paths. Summary of the Resolution: Namespace Clarity: Renaming the folder to res://config_resources/ stopped the collision with the Settings Autoload. Broken Circularity: Removing the Globals logging call from the Resource script allows the resource to compile independently. Safe Loading: Updating globals.gd to use the correct path ensured that the @export variable actually finds its data. --- .../default_settings.tres | 0 scripts/game_settings_resource.gd | 6 ++---- scripts/globals.gd | 10 ++++++++-- scripts/input_remap_button.gd | 17 ++++++++++++----- scripts/key_mapping.gd | 1 + scripts/settings.gd | 8 +++++++- 6 files changed, 30 insertions(+), 12 deletions(-) rename {settings => config_resources}/default_settings.tres (100%) diff --git a/settings/default_settings.tres b/config_resources/default_settings.tres similarity index 100% rename from settings/default_settings.tres rename to config_resources/default_settings.tres diff --git a/scripts/game_settings_resource.gd b/scripts/game_settings_resource.gd index d4ce78c3..79fcac00 100644 --- a/scripts/game_settings_resource.gd +++ b/scripts/game_settings_resource.gd @@ -22,10 +22,8 @@ extends Resource @export var difficulty: float = 1.0: set(value): if value < 0.5 or value > 2.0: - Globals.log_message( - "Invalid difficulty loaded (" + str(value) + ") - clamping to valid range.", - Globals.LogLevel.WARNING - ) + # BREAK THE CYCLE: Use push_warning instead of Globals.log_message + push_warning("Invalid difficulty loaded (" + str(value) + ") - clamping.") _difficulty = clamp(value, 0.5, 2.0) get: return _difficulty diff --git a/scripts/globals.gd b/scripts/globals.gd index 38b3aa31..6147d877 100644 --- a/scripts/globals.gd +++ b/scripts/globals.gd @@ -19,8 +19,7 @@ enum LogLevel { DEBUG, INFO, WARNING, ERROR, NONE = 4 } # @export var difficulty: float = 1.0 # Multiplier: 1.0=Normal, <1=Easy, >1=Hard # Add the resource reference here -@export var settings: GameSettingsResource = preload("res://settings/default_settings.tres") - +@export var settings: GameSettingsResource # In globals.gd (add after @export vars) var options_instance: CanvasLayer = null # var hidden_menu: Node = null @@ -37,6 +36,13 @@ var current_input_device: String = "keyboard" # "keyboard" or "gamepad" func _ready() -> void: + # Load the resource here instead of preloading at the top + settings = load("res://config_resources/default_settings.tres") + if not settings: + # Use push_error since Globals logging might not be ready + push_error("CRITICAL: 'GameSettingsResource' failed to load at path.") + return + if Engine.is_editor_hint() or settings.enable_debug_logging: settings.current_log_level = LogLevel.DEBUG log_message("Log level set to: " + LogLevel.keys()[settings.current_log_level], LogLevel.DEBUG) diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index 3b67ad9b..f709cf86 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -116,12 +116,19 @@ func _ready() -> void: func _on_pressed() -> void: listening = button_pressed if listening: + # Safely check if Globals and settings are ready + if is_instance_valid(Globals) and Globals.settings: # FIXED: Check the actual current_device to return the correct prompt - text = ( - Globals.settings.remap_prompt_keyboard - if current_device == DeviceType.KEYBOARD - else Globals.settings.remap_prompt_gamepad - ) + text = ( + Globals.settings.remap_prompt_keyboard + if current_device == DeviceType.KEYBOARD + else Globals.settings.remap_prompt_gamepad + ) + else: + Globals.log_message( + "'Globals.settings' resource is NULL'", + Globals.LogLevel.ERROR + ) else: update_button_text() diff --git a/scripts/key_mapping.gd b/scripts/key_mapping.gd index b7d7100b..91270277 100644 --- a/scripts/key_mapping.gd +++ b/scripts/key_mapping.gd @@ -179,6 +179,7 @@ func _on_reset_pressed() -> void: # Function resets only the selected device type (keyboard or gamepad) var device_type: String = "keyboard" if keyboard.button_pressed else "gamepad" Settings.reset_to_defaults(device_type) + # You must update the UI buttons so they show the new defaults! update_all_remap_buttons() Globals.log_message("Resetting " + device_type + " controls.", Globals.LogLevel.DEBUG) # NEW: Clear critical warning flag when player fixes unbound (once-per-session) diff --git a/scripts/settings.gd b/scripts/settings.gd index 65ec3dd7..85bc84b0 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -386,10 +386,16 @@ func deserialize_event(serialized: String) -> InputEvent: match parts[0]: "key": - if parts[1].is_valid_int(): + if parts.size() >= 2 and parts[1].is_valid_int(): + # if parts[1].is_valid_int(): var ev := InputEventKey.new() + # You saved as physical_keycode, so you MUST load as physical_keycode ev.physical_keycode = parts[1].to_int() + if ev.physical_keycode == 0: + return + # print("DEBUG: Deserializing '", serialized, "' -> keycode: ", ev.physical_keycode) # Logic for combinations (Shift + Tab etc) + # Modifiers must be checked explicitly in the 'parts' array if "shift" in parts: ev.shift_pressed = true if "ctrl" in parts: From f990af22e48bcadf5e5831b2fc192f26d255f94a Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:47:03 -0800 Subject: [PATCH 04/32] Update settings.gd --- scripts/settings.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/settings.gd b/scripts/settings.gd index 85bc84b0..9921a742 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -387,7 +387,7 @@ func deserialize_event(serialized: String) -> InputEvent: match parts[0]: "key": if parts.size() >= 2 and parts[1].is_valid_int(): - # if parts[1].is_valid_int(): + # if parts[1].is_valid_int(): var ev := InputEventKey.new() # You saved as physical_keycode, so you MUST load as physical_keycode ev.physical_keycode = parts[1].to_int() From 0deb2257a08ee5e3c446823c15b48b3bf27d79e3 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:47:29 -0800 Subject: [PATCH 05/32] Update globals.gd --- scripts/globals.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/globals.gd b/scripts/globals.gd index 6147d877..d04cb77a 100644 --- a/scripts/globals.gd +++ b/scripts/globals.gd @@ -42,7 +42,7 @@ func _ready() -> void: # Use push_error since Globals logging might not be ready push_error("CRITICAL: 'GameSettingsResource' failed to load at path.") return - + if Engine.is_editor_hint() or settings.enable_debug_logging: settings.current_log_level = LogLevel.DEBUG log_message("Log level set to: " + LogLevel.keys()[settings.current_log_level], LogLevel.DEBUG) From 4b977b91f22d596a22147e61c55bb6cdbbd144fe Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:47:47 -0800 Subject: [PATCH 06/32] Update input_remap_button.gd --- scripts/input_remap_button.gd | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index f709cf86..823d951a 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -118,17 +118,14 @@ func _on_pressed() -> void: if listening: # Safely check if Globals and settings are ready if is_instance_valid(Globals) and Globals.settings: - # FIXED: Check the actual current_device to return the correct prompt + # FIXED: Check the actual current_device to return the correct prompt text = ( Globals.settings.remap_prompt_keyboard if current_device == DeviceType.KEYBOARD else Globals.settings.remap_prompt_gamepad ) else: - Globals.log_message( - "'Globals.settings' resource is NULL'", - Globals.LogLevel.ERROR - ) + Globals.log_message("'Globals.settings' resource is NULL'", Globals.LogLevel.ERROR) else: update_button_text() From 67ea986694bffc194bb4ee24a57fc9f142bb1980 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:54:48 -0800 Subject: [PATCH 07/32] Update settings.gd --- scripts/settings.gd | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/scripts/settings.gd b/scripts/settings.gd index 9921a742..04ac87e4 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -372,7 +372,9 @@ func _remove_event_from_conflicts(event: InputEvent, conflicts: Array[String]) - ## Deserializes a string back to InputEvent. ## Handles "key:code", "joybtn:index:device", "joyaxis:axis:value:device". func deserialize_event(serialized: String) -> InputEvent: - # 1. Reject plain integers or empty strings immediately + var event_to_return: InputEvent = null + + # 1. Reject invalid prefixes immediately if not ( serialized.begins_with("key:") or serialized.begins_with("joybtn:") @@ -387,30 +389,21 @@ func deserialize_event(serialized: String) -> InputEvent: match parts[0]: "key": if parts.size() >= 2 and parts[1].is_valid_int(): - # if parts[1].is_valid_int(): var ev := InputEventKey.new() - # You saved as physical_keycode, so you MUST load as physical_keycode ev.physical_keycode = parts[1].to_int() - if ev.physical_keycode == 0: - return - # print("DEBUG: Deserializing '", serialized, "' -> keycode: ", ev.physical_keycode) - # Logic for combinations (Shift + Tab etc) - # Modifiers must be checked explicitly in the 'parts' array - if "shift" in parts: - ev.shift_pressed = true - if "ctrl" in parts: - ev.ctrl_pressed = true - if "alt" in parts: # NEW: Restore Alt - ev.alt_pressed = true - if "meta" in parts: # NEW: Restore Meta - ev.meta_pressed = true - return ev + + if ev.physical_keycode != 0: + ev.shift_pressed = "shift" in parts + ev.ctrl_pressed = "ctrl" in parts + ev.alt_pressed = "alt" in parts + ev.meta_pressed = "meta" in parts + event_to_return = ev "joybtn": if parts.size() == 3 and parts[1].is_valid_int() and parts[2].is_valid_int(): var ev := InputEventJoypadButton.new() ev.button_index = parts[1].to_int() ev.device = parts[2].to_int() - return ev + event_to_return = ev "joyaxis": if ( parts.size() == 4 @@ -422,9 +415,9 @@ func deserialize_event(serialized: String) -> InputEvent: ev.axis = parts[1].to_int() ev.axis_value = parts[2].to_float() ev.device = parts[3].to_int() - return ev + event_to_return = ev - return null + return event_to_return ## Deserializes a string to an InputEvent and adds it to the specified action. From 25c9a0e0dbec889dba49db96f10f15b6f7e0a9ca Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:58:08 -0800 Subject: [PATCH 08/32] issue (bug_risk): Guard for Globals/Globals.settings still calls Globals.log_message even when Globals is invalid, and the error string has a stray quote. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n the else branch you’re still calling Globals.log_message when is_instance_valid(Globals) is false, which means Globals may be freed or uninitialized and can cause a crash. Either return early when Globals is invalid, or only call Globals.log_message when it’s valid and fall back to push_error/print_error otherwise. Also, the error message has mismatched quotes: "'Globals.settings' resource is NULL'" (extra trailing '). --- scripts/input_remap_button.gd | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index 823d951a..fa6aa5d4 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -125,7 +125,8 @@ func _on_pressed() -> void: else Globals.settings.remap_prompt_gamepad ) else: - Globals.log_message("'Globals.settings' resource is NULL'", Globals.LogLevel.ERROR) + # Globals.log_message("'Globals.settings' resource is NULL", Globals.LogLevel.ERROR) + push_error("ERROR: 'Globals.settings' resource is NULL") else: update_button_text() From ecef73b90a00e03aa342695ab07f09f2bd335a26 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 22:59:35 -0800 Subject: [PATCH 09/32] Version comment is incorrect and creates inconsistency across workflows. The comment states v4.32.2, but the SHA 0ec47d036c68ae0cf94c629009b1029407111281 does not correspond to v4.32.2 or v4.32.4. This commit is part of v4.32.5 (released Mar 2, 2026). Meanwhile, codeql.yml uses v4.32.4 explicitly, creating a version mismatch. Either pin trivy.yml and snyk.yml to match the v4.32.4 tag used in codeql.yml, or update all workflows to use the same release version consistently. --- .github/workflows/trivy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml index 8d3d30df..f1ecb9f4 100644 --- a/.github/workflows/trivy.yml +++ b/.github/workflows/trivy.yml @@ -21,7 +21,7 @@ jobs: persist-credentials: false - name: "Run Trivy FS Scan" uses: "aquasecurity/trivy-action@master" - # uses: "aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284" # Pinned to SHA for v0.34.0 + # uses: "aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284" env: # Suppress version check notification TRIVY_SKIP_VERSION_CHECK: 'true' with: From 1328cd979ca4d349310e91e73b5ea48eb7ce36c0 Mon Sep 17 00:00:00 2001 From: Egor Kostan <20955183+ikostan@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:00:07 -0800 Subject: [PATCH 10/32] Update scripts/globals.gd Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- scripts/globals.gd | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/globals.gd b/scripts/globals.gd index d04cb77a..0db08d18 100644 --- a/scripts/globals.gd +++ b/scripts/globals.gd @@ -37,11 +37,13 @@ var current_input_device: String = "keyboard" # "keyboard" or "gamepad" func _ready() -> void: # Load the resource here instead of preloading at the top - settings = load("res://config_resources/default_settings.tres") - if not settings: + settings = load("res://config_resources/default_settings.tres") as GameSettingsResource + if settings == null: # Use push_error since Globals logging might not be ready push_error("CRITICAL: 'GameSettingsResource' failed to load at path.") - return + # Fallback to in-memory defaults so Globals remains operational + settings = GameSettingsResource.new() + settings.current_log_level = LogLevel.WARNING if Engine.is_editor_hint() or settings.enable_debug_logging: settings.current_log_level = LogLevel.DEBUG From 03a1dd6a9f7b3410479354847544074a6d592454 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 23:16:30 -0800 Subject: [PATCH 11/32] Update game_settings_resource.gd --- scripts/game_settings_resource.gd | 3 --- 1 file changed, 3 deletions(-) diff --git a/scripts/game_settings_resource.gd b/scripts/game_settings_resource.gd index 79fcac00..80a35c72 100644 --- a/scripts/game_settings_resource.gd +++ b/scripts/game_settings_resource.gd @@ -21,9 +21,6 @@ extends Resource # game_settings_resource.gd @export var difficulty: float = 1.0: set(value): - if value < 0.5 or value > 2.0: - # BREAK THE CYCLE: Use push_warning instead of Globals.log_message - push_warning("Invalid difficulty loaded (" + str(value) + ") - clamping.") _difficulty = clamp(value, 0.5, 2.0) get: return _difficulty From b136e3c5a9c93b88cc0cadf2e274fa90a844a2b3 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 23:19:57 -0800 Subject: [PATCH 12/32] Update test_globals.gd --- test/gdunit4/test_globals.gd | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/test/gdunit4/test_globals.gd b/test/gdunit4/test_globals.gd index cd2c6069..f6f3b256 100644 --- a/test/gdunit4/test_globals.gd +++ b/test/gdunit4/test_globals.gd @@ -10,11 +10,15 @@ extends GdUnitTestSuite var globals: Node var test_path: String = "user://test_globals.cfg" # Temp for isolation + func before_test() -> void: - ## Per-test setup: Instantiate globals. - ## - ## :rtype: void + # Instantiate the script globals = auto_free(load("res://scripts/globals.gd").new()) + + # FIX: Manually initialize the settings resource + # because _ready() hasn't run yet. + globals.settings = GameSettingsResource.new() + func after_test() -> void: ## Per-test cleanup: Remove test file. @@ -23,6 +27,7 @@ func after_test() -> void: if FileAccess.file_exists(test_path): DirAccess.remove_absolute(test_path) + func test_save_settings_preserves_other_sections() -> void: ## Tests settings save preserves unrelated sections (e.g., "audio"). ## @@ -42,6 +47,7 @@ func test_save_settings_preserves_other_sections() -> void: assert_float(config.get_value("Settings", "difficulty", 1.0)).is_equal(1.2) assert_float(config.get_value("audio", "master_volume", 1.0)).is_equal(0.6) + func test_load_settings_with_other_sections() -> void: ## Tests load ignores/preserves other sections. ## From 5d002c6f4669f77625355a6cae04f981b4f8512a Mon Sep 17 00:00:00 2001 From: Egor Kostan <20955183+ikostan@users.noreply.github.com> Date: Mon, 2 Mar 2026 23:31:40 -0800 Subject: [PATCH 13/32] Update scripts/input_remap_button.gd Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- scripts/input_remap_button.gd | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index fa6aa5d4..bfcfd348 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -127,6 +127,12 @@ func _on_pressed() -> void: else: # Globals.log_message("'Globals.settings' resource is NULL", Globals.LogLevel.ERROR) push_error("ERROR: 'Globals.settings' resource is NULL") + # Fallback prompt so the UI still reflects that we are listening + text = ( + "Press a key" + if current_device == DeviceType.KEYBOARD + else "Press a button" + ) else: update_button_text() From 1266ea20fff5e185182abbb0ac7c31fac4fb016f Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Mon, 2 Mar 2026 23:33:42 -0800 Subject: [PATCH 14/32] Update input_remap_button.gd --- scripts/input_remap_button.gd | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/scripts/input_remap_button.gd b/scripts/input_remap_button.gd index bfcfd348..a3eeec68 100644 --- a/scripts/input_remap_button.gd +++ b/scripts/input_remap_button.gd @@ -128,11 +128,7 @@ func _on_pressed() -> void: # Globals.log_message("'Globals.settings' resource is NULL", Globals.LogLevel.ERROR) push_error("ERROR: 'Globals.settings' resource is NULL") # Fallback prompt so the UI still reflects that we are listening - text = ( - "Press a key" - if current_device == DeviceType.KEYBOARD - else "Press a button" - ) + text = ("Press a key" if current_device == DeviceType.KEYBOARD else "Press a button") else: update_button_text() From 33c348b49816bbab0cea4481b284e4648f031cfc Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 20:59:03 -0800 Subject: [PATCH 15/32] suggestion: Exported settings is always overwritten in _ready, which makes the export effectively non-configurable from the inspector. Because _ready() always replaces settings with load(...) (or GameSettingsResource.new()), any value set in the inspector is ignored at runtime. If you want inspector overrides to work, only load when settings == null (or gate it with a flag), or instead export a resource path and keep the current hard-coded load behavior. --- scripts/globals.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/globals.gd b/scripts/globals.gd index 0db08d18..c8396aca 100644 --- a/scripts/globals.gd +++ b/scripts/globals.gd @@ -19,7 +19,7 @@ enum LogLevel { DEBUG, INFO, WARNING, ERROR, NONE = 4 } # @export var difficulty: float = 1.0 # Multiplier: 1.0=Normal, <1=Easy, >1=Hard # Add the resource reference here -@export var settings: GameSettingsResource +var settings: GameSettingsResource # In globals.gd (add after @export vars) var options_instance: CanvasLayer = null # var hidden_menu: Node = null From cdbfbaf5673d4aae5fd94c66dcbd033dcca4a3d2 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:03:35 -0800 Subject: [PATCH 16/32] Create no_error_logs_test.py --- tests/no_error_logs_test.py | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/no_error_logs_test.py diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py new file mode 100644 index 00000000..8523e51e --- /dev/null +++ b/tests/no_error_logs_test.py @@ -0,0 +1,82 @@ +# Copyright (C) 2025 Egor Kostan +# SPDX-License-Identifier: GPL-3.0-or-later +# tests/no_error_logs_test.py +""" +Console Error Integrity Test (Playwright + UI Automation) +========================================================= + +Overview +-------- +Verifies that the SkyLockAssault HTML5 build loads without triggering +any 'error' level logs in the browser console. + +Test Flow +--------- +- Listen to all console messages. +- Navigate to the index page and wait for network idle. +- Wait for window.godotInitialized (Godot _ready() signal). +- Assert that no logs with type="error" exist. +""" + +import os +import time +import json +from playwright.sync_api import Page + + +def test_no_error_logs_after_load(page: Page) -> None: + """ + E2E test to ensure zero console errors on initial load. + + :param page: The Playwright page object. + :type page: Page + :rtype: None + """ + logs: list[dict[str, str]] = [] + cdp_session = None + + def on_console(msg) -> None: + """Capture all console messages for inspection.""" + logs.append({"type": msg.type, "text": msg.text}) + + # Attach the listener before navigation + page.on("console", on_console) + + try: + # Start CDP session for coverage (consistent with your other tests) + cdp_session = page.context.new_cdp_session(page) + cdp_session.send("Profiler.enable") + cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) + + # Navigate and wait for the game to initialize + page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) + page.wait_for_function("() => window.godotInitialized", timeout=5000) + + # Allow a short buffer for any delayed post-load errors + page.wait_for_timeout(1000) + + # Filter for error logs + error_logs = [log for log in logs if log["type"] == "error"] + + # Detailed assertion message for easier debugging + error_details = "\n".join([f"[{err['type']}] {err['text']}" for err in error_logs]) + assert len(error_logs) == 0, f"Found {len(error_logs)} error(s) in console:\n{error_details}" + + except Exception as e: + print(f"Test: 'test_no_error_logs_after_load' failed: {str(e)}") + os.makedirs("artifacts", exist_ok=True) + timestamp = int(time.time()) + page.screenshot(path=f"artifacts/test_error_logs_failure_{timestamp}.png") + + # Save logs for inspection + with open(f"artifacts/test_error_logs_console_{timestamp}.txt", "w") as f: + for log in logs: + f.write(f"[{log['type']}] {log['text']}\n") + raise + finally: + if cdp_session: + coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + cdp_session.send("Profiler.stopPreciseCoverage") + cdp_session.send("Profiler.disable") + with open("v8_coverage_no_error_logs_test.json", "w") as f: + json.dump(coverage, f) From 1124a5cf2931ce6d5f89fdd1887f3bfc3890d1f8 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:04:24 +0000 Subject: [PATCH 17/32] style: format code with Black and isort This commit fixes the style issues introduced in cdbfbaf according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/no_error_logs_test.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 8523e51e..5c9badf8 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -18,9 +18,10 @@ - Assert that no logs with type="error" exist. """ +import json import os import time -import json + from playwright.sync_api import Page @@ -46,10 +47,14 @@ def on_console(msg) -> None: # Start CDP session for coverage (consistent with your other tests) cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") - cdp_session.send("Profiler.startPreciseCoverage", {"callCount": True, "detailed": True}) + cdp_session.send( + "Profiler.startPreciseCoverage", {"callCount": True, "detailed": True} + ) # Navigate and wait for the game to initialize - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) page.wait_for_function("() => window.godotInitialized", timeout=5000) # Allow a short buffer for any delayed post-load errors @@ -59,8 +64,12 @@ def on_console(msg) -> None: error_logs = [log for log in logs if log["type"] == "error"] # Detailed assertion message for easier debugging - error_details = "\n".join([f"[{err['type']}] {err['text']}" for err in error_logs]) - assert len(error_logs) == 0, f"Found {len(error_logs)} error(s) in console:\n{error_details}" + error_details = "\n".join( + [f"[{err['type']}] {err['text']}" for err in error_logs] + ) + assert ( + len(error_logs) == 0 + ), f"Found {len(error_logs)} error(s) in console:\n{error_details}" except Exception as e: print(f"Test: 'test_no_error_logs_after_load' failed: {str(e)}") @@ -75,7 +84,7 @@ def on_console(msg) -> None: raise finally: if cdp_session: - coverage = cdp_session.send("Profiler.takePreciseCoverage")['result'] + coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open("v8_coverage_no_error_logs_test.json", "w") as f: From 92fa42f07ecfc9d07d5dc456ef1bb48e9d67e315 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:07:13 -0800 Subject: [PATCH 18/32] Create validate_clean_load_test.py --- tests/validate_clean_load_test.py | 78 +++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/validate_clean_load_test.py diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py new file mode 100644 index 00000000..5efbba3d --- /dev/null +++ b/tests/validate_clean_load_test.py @@ -0,0 +1,78 @@ +# Copyright (C) 2025 Egor Kostan +# SPDX-License-Identifier: GPL-3.0-or-later +# tests/validate_clean_load_test.py +""" +Console Error Integrity Test (Playwright + UI Automation) +========================================================= + +Overview +-------- +Specific E2E test to catch GDScript compilation and runtime errors +identified in the browser console during Godot engine initialization. + +Test Flow +--------- +- Listen for specific error patterns: "SCRIPT ERROR", "Compile Error", "Parse Error". +- Monitor for 'Uncaught (in promise)' exceptions. +- Navigate to index.html and wait for engine initialization signal. +- Fail if any critical engine or script errors are detected. +""" + +import os +import time +import json +from playwright.sync_api import Page + + +def test_no_critical_errors_on_load(page: Page) -> None: + """ + Verifies that the game loads without script compilation or engine errors. + + :param page: The Playwright page object. + :type page: Page + :rtype: None + """ + logs: list[dict[str, str]] = [] + + def on_console(msg) -> None: + """Capture all console messages for inspection.""" + logs.append({"type": msg.type, "text": msg.text}) + + page.on("console", on_console) + + try: + # 1. Navigate to the game + page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) + + # 2. Wait for the engine's ready signal + page.wait_for_function("() => window.godotInitialized", timeout=5000) + + # 3. Analyze captured logs for the specific patterns seen in the screenshot + critical_errors = [ + log["text"] for log in logs + if log["type"] == "error" or + any(pattern in log["text"] for pattern in [ + "SCRIPT ERROR", + "Compile Error", + "Parse Error", + "Failed to load script", + "Uncaught (in promise)" + ]) + ] + + # 4. Detailed assertion + if critical_errors: + error_summary = "\n".join([f" - {err}" for err in critical_errors]) + assert not critical_errors, f"Critical errors detected during load:\n{error_summary}" + + except Exception as e: + print(f"Load validation failed: {str(e)}") + os.makedirs("artifacts", exist_ok=True) + timestamp = int(time.time()) + page.screenshot(path=f"artifacts/test_load_error_screenshot_{timestamp}.png") + + # Save logs for debugging the script failures + with open(f"artifacts/test_load_error_logs_{timestamp}.txt", "w") as f: + for log in logs: + f.write(f"[{log['type']}] {log['text']}\n") + raise From 3eea81ebf0c174a4a6adce49d541d4b2118766be Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:08:20 +0000 Subject: [PATCH 19/32] style: format code with Black and isort This commit fixes the style issues introduced in dea4852 according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/validate_clean_load_test.py | 33 ++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index 5efbba3d..e6ea682a 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -18,9 +18,10 @@ - Fail if any critical engine or script errors are detected. """ +import json import os import time -import json + from playwright.sync_api import Page @@ -42,28 +43,36 @@ def on_console(msg) -> None: try: # 1. Navigate to the game - page.goto("http://localhost:8080/index.html", wait_until="networkidle", timeout=5000) + page.goto( + "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + ) # 2. Wait for the engine's ready signal page.wait_for_function("() => window.godotInitialized", timeout=5000) # 3. Analyze captured logs for the specific patterns seen in the screenshot critical_errors = [ - log["text"] for log in logs - if log["type"] == "error" or - any(pattern in log["text"] for pattern in [ - "SCRIPT ERROR", - "Compile Error", - "Parse Error", - "Failed to load script", - "Uncaught (in promise)" - ]) + log["text"] + for log in logs + if log["type"] == "error" + or any( + pattern in log["text"] + for pattern in [ + "SCRIPT ERROR", + "Compile Error", + "Parse Error", + "Failed to load script", + "Uncaught (in promise)", + ] + ) ] # 4. Detailed assertion if critical_errors: error_summary = "\n".join([f" - {err}" for err in critical_errors]) - assert not critical_errors, f"Critical errors detected during load:\n{error_summary}" + assert ( + not critical_errors + ), f"Critical errors detected during load:\n{error_summary}" except Exception as e: print(f"Load validation failed: {str(e)}") From 86d79c658f6c5a29499ce5e3494ed344dc622491 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:11:39 -0800 Subject: [PATCH 20/32] Unused import json --- tests/validate_clean_load_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index e6ea682a..33620dbc 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -18,7 +18,6 @@ - Fail if any critical engine or script errors are detected. """ -import json import os import time From 42616f54acc1ff63d713786bde617a47b1434764 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:15:54 -0800 Subject: [PATCH 21/32] Apply Ruff RUF010 explicit f-string conversion. Replace {str(e)} with {e!s} on line 75 to use the idiomatic f-string conversion flag instead of explicit str() call. --- tests/no_error_logs_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 5c9badf8..c59bd993 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -72,7 +72,8 @@ def on_console(msg) -> None: ), f"Found {len(error_logs)} error(s) in console:\n{error_details}" except Exception as e: - print(f"Test: 'test_no_error_logs_after_load' failed: {str(e)}") + # print(f"Test: 'test_no_error_logs_after_load' failed: {str(e)}") + print(f"Test: 'test_no_error_logs_after_load' failed: {e!s}") os.makedirs("artifacts", exist_ok=True) timestamp = int(time.time()) page.screenshot(path=f"artifacts/test_error_logs_failure_{timestamp}.png") From 4660c53ee75d52d7654bd14c5222b6a71f7a6e7e Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:18:18 -0800 Subject: [PATCH 22/32] Use explicit UTF-8 when writing artifacts and coverage files. These writes currently rely on platform default encodings; Unicode log text can fail on some runners and obscure the original test failure. --- tests/no_error_logs_test.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index c59bd993..060e981b 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -79,7 +79,11 @@ def on_console(msg) -> None: page.screenshot(path=f"artifacts/test_error_logs_failure_{timestamp}.png") # Save logs for inspection - with open(f"artifacts/test_error_logs_console_{timestamp}.txt", "w") as f: + with open( + f"artifacts/test_error_logs_console_{timestamp}.txt", + "w", + encoding="utf-8", + ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") raise @@ -88,5 +92,5 @@ def on_console(msg) -> None: coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") - with open("v8_coverage_no_error_logs_test.json", "w") as f: + with open("v8_coverage_no_error_logs_test.json", "w", encoding="utf-8") as f: json.dump(coverage, f) From a627706fb80936ca6eefb3c85ea88e5c289b4e77 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:19:35 -0800 Subject: [PATCH 23/32] Use f-string conversion flag instead of str() At Line 77, prefer {e!s} over {str(e)} to satisfy Ruff RUF010 and keep formatting idiomatic. --- tests/validate_clean_load_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index 33620dbc..c774411a 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -74,7 +74,8 @@ def on_console(msg) -> None: ), f"Critical errors detected during load:\n{error_summary}" except Exception as e: - print(f"Load validation failed: {str(e)}") + # print(f"Load validation failed: {str(e)}") + print(f"Load validation failed: {e!s}") os.makedirs("artifacts", exist_ok=True) timestamp = int(time.time()) page.screenshot(path=f"artifacts/test_load_error_screenshot_{timestamp}.png") From 5815b499e0d22e6c5d309096dcb270f65b503a97 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:20:04 +0000 Subject: [PATCH 24/32] style: format code with Black and isort This commit fixes the style issues introduced in 4660c53 according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/no_error_logs_test.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 060e981b..585f2d31 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -80,9 +80,9 @@ def on_console(msg) -> None: # Save logs for inspection with open( - f"artifacts/test_error_logs_console_{timestamp}.txt", - "w", - encoding="utf-8", + f"artifacts/test_error_logs_console_{timestamp}.txt", + "w", + encoding="utf-8", ) as f: for log in logs: f.write(f"[{log['type']}] {log['text']}\n") @@ -92,5 +92,7 @@ def on_console(msg) -> None: coverage = cdp_session.send("Profiler.takePreciseCoverage")["result"] cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") - with open("v8_coverage_no_error_logs_test.json", "w", encoding="utf-8") as f: + with open( + "v8_coverage_no_error_logs_test.json", "w", encoding="utf-8" + ) as f: json.dump(coverage, f) From 2fe5314ffeed21ef28d9d3f0aa7d7491a77bb4cb Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:29:27 -0800 Subject: [PATCH 25/32] question (bug_risk): Guarding modifiers behind physical_keycode != 0 might unintentionally drop some deserialized key events. Previously, any event with a valid parts[1] int was returned, even when physical_keycode was 0. With the new physical_keycode != 0 guard, a serialized value like key:0:shift will now silently produce null. If 0 is invalid in this context, consider rejecting it explicitly (e.g., to_int() > 0) or treating it as malformed input so the behavior is clear. If 0 can be valid, the guard should be removed so those events still deserialize. --- scripts/settings.gd | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/settings.gd b/scripts/settings.gd index 04ac87e4..5e8f916e 100644 --- a/scripts/settings.gd +++ b/scripts/settings.gd @@ -389,15 +389,23 @@ func deserialize_event(serialized: String) -> InputEvent: match parts[0]: "key": if parts.size() >= 2 and parts[1].is_valid_int(): + var code := parts[1].to_int() + + # OPINION: Explicitly reject 0 to prevent "silent drops" + # as suggested by Sourcery. + if code == 0: + Globals.log_message( + "Ignoring key event with keycode 0", Globals.LogLevel.WARNING + ) + return null + var ev := InputEventKey.new() - ev.physical_keycode = parts[1].to_int() - - if ev.physical_keycode != 0: - ev.shift_pressed = "shift" in parts - ev.ctrl_pressed = "ctrl" in parts - ev.alt_pressed = "alt" in parts - ev.meta_pressed = "meta" in parts - event_to_return = ev + ev.physical_keycode = code + ev.shift_pressed = "shift" in parts + ev.ctrl_pressed = "ctrl" in parts + ev.alt_pressed = "alt" in parts + ev.meta_pressed = "meta" in parts + event_to_return = ev "joybtn": if parts.size() == 3 and parts[1].is_valid_int() and parts[2].is_valid_int(): var ev := InputEventJoypadButton.new() From 8f0136ae07a80f90834be7d87a12f53f7b2e7595 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:47:36 -0800 Subject: [PATCH 26/32] Remove redundant commented debug prints in tests Delete leftover commented-out print statements in tests/no_error_logs_test.py and tests/validate_clean_load_test.py. This is a simple cleanup to reduce clutter; no runtime behavior is changed. --- tests/no_error_logs_test.py | 1 - tests/validate_clean_load_test.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 585f2d31..c4ed4c55 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -72,7 +72,6 @@ def on_console(msg) -> None: ), f"Found {len(error_logs)} error(s) in console:\n{error_details}" except Exception as e: - # print(f"Test: 'test_no_error_logs_after_load' failed: {str(e)}") print(f"Test: 'test_no_error_logs_after_load' failed: {e!s}") os.makedirs("artifacts", exist_ok=True) timestamp = int(time.time()) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index c774411a..9d53b937 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -74,7 +74,6 @@ def on_console(msg) -> None: ), f"Critical errors detected during load:\n{error_summary}" except Exception as e: - # print(f"Load validation failed: {str(e)}") print(f"Load validation failed: {e!s}") os.makedirs("artifacts", exist_ok=True) timestamp = int(time.time()) From 0d02fc28920d9ac59095e02fc6d9a874913b276c Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 21:55:02 -0800 Subject: [PATCH 27/32] suggestion (testing): Also capture pageerror events to catch uncaught exceptions that might not surface as console logs. Currently this only inspects page.on("console", ...) messages. Some uncaught exceptions or promise rejections are only emitted via page.on("pageerror"), so the test could pass while the page is actually broken. Please also listen to page.on("pageerror", ...) (either merging into logs or a separate list) and include those in the failure assertion. --- tests/no_error_logs_test.py | 52 +++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index c4ed4c55..4f797b2a 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -2,49 +2,50 @@ # SPDX-License-Identifier: GPL-3.0-or-later # tests/no_error_logs_test.py """ -Console Error Integrity Test (Playwright + UI Automation) +Console & Page Error Integrity Test (Playwright + UI Automation) ========================================================= Overview -------- Verifies that the SkyLockAssault HTML5 build loads without triggering -any 'error' level logs in the browser console. +any 'error' level logs or uncaught exceptions in the browser. Test Flow --------- -- Listen to all console messages. +- Listen to all console messages and uncaught page errors. - Navigate to the index page and wait for network idle. - Wait for window.godotInitialized (Godot _ready() signal). -- Assert that no logs with type="error" exist. +- Assert that no logs with type="error" or uncaught exceptions exist. """ import json import os import time - from playwright.sync_api import Page def test_no_error_logs_after_load(page: Page) -> None: """ - E2E test to ensure zero console errors on initial load. - - :param page: The Playwright page object. - :type page: Page - :rtype: None + E2E test to ensure zero console errors and uncaught exceptions on initial load. """ logs: list[dict[str, str]] = [] + page_errors: list[str] = [] cdp_session = None def on_console(msg) -> None: """Capture all console messages for inspection.""" logs.append({"type": msg.type, "text": msg.text}) - # Attach the listener before navigation + def on_page_error(exc) -> None: + """Capture uncaught exceptions (pageerror).""" + page_errors.append(f"Uncaught Exception: {exc.message}\n{exc.stack}") + + # Attach listeners before navigation page.on("console", on_console) + page.on("pageerror", on_page_error) try: - # Start CDP session for coverage (consistent with your other tests) + # Start CDP session for coverage cdp_session = page.context.new_cdp_session(page) cdp_session.send("Profiler.enable") cdp_session.send( @@ -63,13 +64,13 @@ def on_console(msg) -> None: # Filter for error logs error_logs = [log for log in logs if log["type"] == "error"] - # Detailed assertion message for easier debugging - error_details = "\n".join( - [f"[{err['type']}] {err['text']}" for err in error_logs] - ) + # Combine errors for a comprehensive assertion + all_errors = [f"[{err['type']}] {err['text']}" for err in error_logs] + page_errors + error_details = "\n".join(all_errors) + assert ( - len(error_logs) == 0 - ), f"Found {len(error_logs)} error(s) in console:\n{error_details}" + len(all_errors) == 0 + ), f"Found {len(all_errors)} error(s) during load:\n{error_details}" except Exception as e: print(f"Test: 'test_no_error_logs_after_load' failed: {e!s}") @@ -77,14 +78,19 @@ def on_console(msg) -> None: timestamp = int(time.time()) page.screenshot(path=f"artifacts/test_error_logs_failure_{timestamp}.png") - # Save logs for inspection + # Save all captured logs and exceptions for inspection with open( - f"artifacts/test_error_logs_console_{timestamp}.txt", - "w", - encoding="utf-8", + f"artifacts/test_error_logs_console_{timestamp}.txt", + "w", + encoding="utf-8", ) as f: + f.write("--- CONSOLE LOGS ---\n") for log in logs: f.write(f"[{log['type']}] {log['text']}\n") + + f.write("\n--- UNCAUGHT EXCEPTIONS ---\n") + for p_err in page_errors: + f.write(f"{p_err}\n") raise finally: if cdp_session: @@ -92,6 +98,6 @@ def on_console(msg) -> None: cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open( - "v8_coverage_no_error_logs_test.json", "w", encoding="utf-8" + "v8_coverage_no_error_logs_test.json", "w", encoding="utf-8" ) as f: json.dump(coverage, f) From e0f590f6587a1ea1ef6a04cb3055e337c6413bc9 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 05:55:44 +0000 Subject: [PATCH 28/32] style: format code with Black and isort This commit fixes the style issues introduced in 0d02fc2 according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/no_error_logs_test.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 4f797b2a..2610f229 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -21,6 +21,7 @@ import json import os import time + from playwright.sync_api import Page @@ -65,11 +66,13 @@ def on_page_error(exc) -> None: error_logs = [log for log in logs if log["type"] == "error"] # Combine errors for a comprehensive assertion - all_errors = [f"[{err['type']}] {err['text']}" for err in error_logs] + page_errors + all_errors = [ + f"[{err['type']}] {err['text']}" for err in error_logs + ] + page_errors error_details = "\n".join(all_errors) assert ( - len(all_errors) == 0 + len(all_errors) == 0 ), f"Found {len(all_errors)} error(s) during load:\n{error_details}" except Exception as e: @@ -80,9 +83,9 @@ def on_page_error(exc) -> None: # Save all captured logs and exceptions for inspection with open( - f"artifacts/test_error_logs_console_{timestamp}.txt", - "w", - encoding="utf-8", + f"artifacts/test_error_logs_console_{timestamp}.txt", + "w", + encoding="utf-8", ) as f: f.write("--- CONSOLE LOGS ---\n") for log in logs: @@ -98,6 +101,6 @@ def on_page_error(exc) -> None: cdp_session.send("Profiler.stopPreciseCoverage") cdp_session.send("Profiler.disable") with open( - "v8_coverage_no_error_logs_test.json", "w", encoding="utf-8" + "v8_coverage_no_error_logs_test.json", "w", encoding="utf-8" ) as f: json.dump(coverage, f) From 8aa0bbcc3e3f7db02350c64d5218dc7d6b5b7cd0 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 22:00:04 -0800 Subject: [PATCH 29/32] suggestion (testing): Consider making the timeouts less brittle or configurable to avoid flaky failures on slower environments. The page.goto(..., timeout=5000) and page.wait_for_function(..., timeout=5000) calls, plus the fixed wait_for_timeout(1000), are likely to be fragile on slower CI or as the app grows, leading to intermittent test failures. Consider either increasing these limits, making them configurable (e.g., via env vars), or replacing the fixed timeout with polling for godotInitialized under a higher overall limit to keep tests reliable while still failing fast when initialization truly breaks. --- tests/no_error_logs_test.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 4f797b2a..bfded0ad 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -23,6 +23,11 @@ import time from playwright.sync_api import Page +# Configuration for stability in different environments +# Default to 5000ms, but allow CI to override via environment variable +DEFAULT_TIMEOUT = int(os.getenv("TEST_TIMEOUT", "5000")) +BUFFER_TIMEOUT = 1000 + def test_no_error_logs_after_load(page: Page) -> None: """ @@ -53,13 +58,21 @@ def on_page_error(exc) -> None: ) # Navigate and wait for the game to initialize + # Using the configurable DEFAULT_TIMEOUT for improved stability page.goto( - "http://localhost:8080/index.html", wait_until="networkidle", timeout=5000 + "http://localhost:8080/index.html", + wait_until="networkidle", + timeout=DEFAULT_TIMEOUT + ) + + # Wait for the custom Godot initialization flag + page.wait_for_function( + "() => window.godotInitialized", + timeout=DEFAULT_TIMEOUT ) - page.wait_for_function("() => window.godotInitialized", timeout=5000) # Allow a short buffer for any delayed post-load errors - page.wait_for_timeout(1000) + page.wait_for_timeout(BUFFER_TIMEOUT) # Filter for error logs error_logs = [log for log in logs if log["type"] == "error"] From b483a2d6da7428b4b0ebb9937b8c5744e907bb73 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:01:11 +0000 Subject: [PATCH 30/32] style: format code with Black and isort This commit fixes the style issues introduced in 57ced50 according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/no_error_logs_test.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/no_error_logs_test.py b/tests/no_error_logs_test.py index 5e38cb4e..1af4104d 100644 --- a/tests/no_error_logs_test.py +++ b/tests/no_error_logs_test.py @@ -63,14 +63,11 @@ def on_page_error(exc) -> None: page.goto( "http://localhost:8080/index.html", wait_until="networkidle", - timeout=DEFAULT_TIMEOUT + timeout=DEFAULT_TIMEOUT, ) # Wait for the custom Godot initialization flag - page.wait_for_function( - "() => window.godotInitialized", - timeout=DEFAULT_TIMEOUT - ) + page.wait_for_function("() => window.godotInitialized", timeout=DEFAULT_TIMEOUT) # Allow a short buffer for any delayed post-load errors page.wait_for_timeout(BUFFER_TIMEOUT) From eff433ff5e7bb8855da2705fc13e387e59a75763 Mon Sep 17 00:00:00 2001 From: Egor Kostan Date: Tue, 3 Mar 2026 22:07:21 -0800 Subject: [PATCH 31/32] question (testing): Clarify whether matching critical error patterns on non-error console types is intentional to avoid false positives. Because the patterns are checked regardless of log['type'], any non-error log (e.g., debug output) that happens to contain one of these phrases will be treated as a critical error and fail the test. If you only want to flag genuine error-level messages, consider applying the pattern check only when log['type'] == 'error', or add a brief comment stating that matching these patterns on non-error logs is intentional. --- tests/validate_clean_load_test.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index 9d53b937..11e02d20 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -49,21 +49,26 @@ def on_console(msg) -> None: # 2. Wait for the engine's ready signal page.wait_for_function("() => window.godotInitialized", timeout=5000) - # 3. Analyze captured logs for the specific patterns seen in the screenshot + # 3. Analyze captured logs for the specific patterns + # We only check for patterns within 'error' or 'warning' logs to avoid false positives + # from informational logs that might mention these terms. critical_errors = [ log["text"] for log in logs - if log["type"] == "error" - or any( - pattern in log["text"] - for pattern in [ - "SCRIPT ERROR", - "Compile Error", - "Parse Error", - "Failed to load script", - "Uncaught (in promise)", - ] - ) + if log["type"] in ["error", "warning"] # Filter by type first + and ( + log["type"] == "error" # All error types are critical + or any( + pattern in log["text"] + for pattern in [ + "SCRIPT ERROR", + "Compile Error", + "Parse Error", + "Failed to load script", + "Uncaught (in promise)", + ] + ) + ) ] # 4. Detailed assertion From 98e6d12dd3370f8dcc89ba8869824c5d8996c2bc Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Wed, 4 Mar 2026 06:08:00 +0000 Subject: [PATCH 32/32] style: format code with Black and isort This commit fixes the style issues introduced in eff433f according to the output from Black and isort. Details: https://github.com/ikostan/SkyLockAssault/pull/451 --- tests/validate_clean_load_test.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/validate_clean_load_test.py b/tests/validate_clean_load_test.py index 11e02d20..939e5bc9 100644 --- a/tests/validate_clean_load_test.py +++ b/tests/validate_clean_load_test.py @@ -56,19 +56,19 @@ def on_console(msg) -> None: log["text"] for log in logs if log["type"] in ["error", "warning"] # Filter by type first - and ( - log["type"] == "error" # All error types are critical - or any( - pattern in log["text"] - for pattern in [ - "SCRIPT ERROR", - "Compile Error", - "Parse Error", - "Failed to load script", - "Uncaught (in promise)", - ] - ) - ) + and ( + log["type"] == "error" # All error types are critical + or any( + pattern in log["text"] + for pattern in [ + "SCRIPT ERROR", + "Compile Error", + "Parse Error", + "Failed to load script", + "Uncaught (in promise)", + ] + ) + ) ] # 4. Detailed assertion