Skip to content

UI audio logic unit tests#746

Merged
ikostan merged 33 commits into
mainfrom
ui-audio-logic-unit-tests
Jun 12, 2026
Merged

UI audio logic unit tests#746
ikostan merged 33 commits into
mainfrom
ui-audio-logic-unit-tests

Conversation

@ikostan

@ikostan ikostan commented Jun 10, 2026

Copy link
Copy Markdown
Owner

name: Default Pull Request Template
about: Suggesting changes to SkyLockAssault
title: ''
labels: ''
assignees: ''

Pull Request Summary: Architectural Testing & Reliability Fixes for Audio UI

This PR introduces an automated testing suite for AudioSettings and implements significant resilience improvements for the AudioManager and VolumeSlider components. The primary objective is to transition from brittle, manually checked UI tests to a robust, API-driven test suite suitable for CI environments.


🚀 Key Changes

1. Architectural Testing Suite (test_audio_settings_interaction.gd)

  • GUT Integration: Established a comprehensive test suite using the GUT framework.
  • Focus-Gate Validation: Added stress tests to verify that audio triggers only fire when the UI component has focus, validating the logic in audio_settings.gd.
  • Resilience Testing: Introduced boundary and stress tests to ensure:
    • Rapid UI interaction (spamming) does not overwhelm the audio pool.
    • Out-of-range volume values and invalid SFX keys are handled gracefully without crashing.
  • State Management: Mocked Globals.previous_scene during test execution to prevent unintended scene changes (restarts) during UI interaction tests.

2. AudioManager Refactoring (audio_manager.gd)

  • Diagnostic APIs: Added is_sfx_playing() and get_active_sfx_stream_path() to expose internal state to tests.
    • Note: These are explicitly commented as ## [DIAGNOSTIC] to signal their appropriate use.
  • Determinism: Updated get_active_sfx_stream_path() to return the most recently played SFX, ensuring tests have a deterministic target rather than an arbitrary pool index.

3. Component Resilience (volume_slider.gd)

  • Signal Integrity: Updated the volume logic to allow programmatic updates via _on_value_changed() (explicitly called by tests), bypassing issues where slider.value property setters do not emit signals.
  • Input Clamping: Implemented clamp() logic in _on_value_changed to ensure slider values always stay within valid bounds, improving resilience against invalid user or test inputs.

4. Repository Health

  • Metadata Cleanup: Updated .gitignore to ignore engine-generated *.uid files, eliminating "noisy" diffs and potential merge conflicts.

🛠 Technical Fixes

  • Race Conditions: Resolved crashes occurring when calling methods on nodes currently being freed by using is_instance_valid(audio_instance) checks.
  • Brittle Assertions: Replaced hardcoded string substrings (e.g., "check") with named constants (SFX_CHECK_KEYWORD) to ensure tests remain valid even if asset file names change.
  • Async Stability: Added await get_tree().create_timer(0.5).timeout buffers in stress tests to ensure the audio system has time to reconcile playback state before assertions run.

🧪 Testing Status

Category Status Notes
Interaction Tests ✅ Passed Mute toggles, reset functionality, and back navigation.
Regression/Spam ✅ Passed Verified pool usage under heavy load.
Boundary/Resilience ✅ Passed Invalid inputs, extreme slider values, corrupted states.
Focus Gate ✅ Passed Confirmed silent-when-unfocused logic.

Final Verification: 12/12 Tests Passed.


Related Issue

Closes #ISSUE_NUMBER (if applicable)

Changes

  • List key changes here (e.g., "Updated Jump.gd to use Godot 4.4's new Tween
    system")
  • Any breaking changes? (e.g., "Deprecated old signal; migrate to new one")

Testing

  • Ran the game in Godot v4.5 editor—describe what you tested (e.g., "Jump
    works on Win10 with 60 FPS")
  • Any new unit tests added? (Link to test scene if yes)
  • Screenshots/GIFs if UI-related: (Attach below)

Checklist

  • Code follows Godot style guide (e.g., snake_case for variables)
  • No console errors in editor/output
  • Ready for review!

Additional Notes

Anything else? (e.g., "Tested on Win10 64-bit; needs Linux validation")

Summary by Sourcery

Add public AudioManager helpers for inspecting active SFX and introduce a comprehensive UI interaction test suite for audio settings behavior.

New Features:

  • Expose AudioManager APIs to query whether any SFX is playing and to retrieve the active SFX stream path.

Enhancements:

  • Add architectural GUT tests covering audio settings UI interactions, including mute toggles, reset behavior, slider updates, and stress/resilience scenarios.

CI:

  • Update Codecov GitHub Action to a newer pinned commit in the browser test workflow.

Tests:

  • Add regression-focused tests to validate audio pool behavior, focus-gated SFX playback, slider boundary handling, invalid input resilience, and reset recovery for audio settings UI.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed volume slider to properly clamp input values within valid range, preventing out-of-bounds audio settings.
  • Tests

    • Added comprehensive test suite for audio settings interactions, validating mute toggling, volume slider behavior, and reset functionality.
  • Chores

    • Updated workflow action dependency.

Bots/AI Contributions Summary for PR #746

This PR adds public diagnostic helpers (is_sfx_playing(), get_active_sfx_stream_path()) to AudioManager, implements comprehensive GUT tests for UI audio interactions (mute toggles, sliders, reset behavior, focus-gating, boundary/stress cases), includes a minor bug fix for volume slider clamping, and updates the Codecov GitHub Action. It received strong support from automated bots and AI tools for dependency management, PR summarization, review, and code quality analysis.

Automated Bots & AI Tools

  • @dependabot[bot]: Handled the dependency update by bumping codecov/codecov-action from 6.0.1 to 7.0.0 (major version) in the browser test workflow, including the automated commit and related merge. This improves coverage reporting reliability and security.

  • @sourcery-ai: Provided the primary structured PR summary and reviewer's guide. Highlighted new AudioManager APIs, the extensive GUT test suite (UI interactions, resilience, stress scenarios), CI updates, and linked the work to issue [FEATURE] UI Audio Logic Unit Tests (GUT): Navigation Triggering #494. Also contributed to title and description refinement.

  • @coderabbitai: Delivered a focused summary covering bug fixes (volume slider clamping), new comprehensive test suite for audio settings interactions, and maintenance chores (workflow dependency update).

  • @deepsource-io: Performed automated static code analysis and code review on the changes (AudioManager extensions, test suite, CI workflow). Provided an overall grade across Security, Reliability, Complexity, and Hygiene categories, along with inline comments and a full review report.

These tools enhanced test coverage documentation, reviewer guidance, dependency security, and overall code health validation.

Human Maintainers

  • @ikostan: Primary contributor and PR author. Led the full implementation, including new diagnostic APIs on AudioManager, volume slider input clamping fix, comprehensive GUT test suite (test_audio_settings_interaction.gd) covering interaction scenarios, focus-gating, resilience, and stress tests, test harness helpers, sequence diagrams, milestone integration, and CI workflow updates.

dependabot Bot and others added 5 commits June 8, 2026 09:09
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.1 to 7.0.0.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](codecov/codecov-action@e79a696...fb8b358)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
…v/codecov-action-7.0.0

Bump codecov/codecov-action from 6.0.1 to 7.0.0
Create a comprehensive GUT test suite to validate that UI interaction and navigation events are correctly intercepted, interpreted, and translated into sound effect playback requests.
@sourcery-ai

sourcery-ai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds diagnostic AudioManager APIs to inspect active SFX, tightens VolumeSlider value handling, and introduces a comprehensive GUT-based UI interaction test suite for audio settings, plus a minor CI workflow bump and docs for the milestone.

Sequence diagram for UI audio tests using diagnostic AudioManager APIs

sequenceDiagram
    actor TestAudioSettings
    participant VolumeSlider
    participant AudioManager

    TestAudioSettings->>VolumeSlider: _on_value_changed(out_of_range_value)
    VolumeSlider->>VolumeSlider: clamp(new_value, min_value, max_value)
    alt [value changed]
        VolumeSlider->>AudioManager: play_sfx(sound_key, position, volume_db, pitch_scale)
    end
    TestAudioSettings->>AudioManager: is_any_sfx_playing()
    TestAudioSettings->>AudioManager: get_active_sfx_stream_path()
Loading

File-Level Changes

Change Details Files
Expose diagnostic AudioManager APIs so tests can query SFX playback deterministically.
  • Annotate existing SFX inspection methods with DIAGNOSTIC comments to mark them as non-gameplay helpers.
  • Add get_active_sfx_stream_path() to iterate the SFX pool backwards and return the most recently started active stream’s resource_path, or an empty string if none are playing.
scripts/managers/audio_manager.gd
Harden VolumeSlider against invalid or programmatic inputs.
  • Clamp _on_value_changed() input to [min_value, max_value] before processing to keep volume within valid bounds.
  • Preserve early-return behavior that skips redundant backend updates when the value hasn’t changed (within float tolerance).
scripts/ui/components/volume_slider.gd
Add an architectural GUT test suite for audio settings UI behavior using public AudioManager APIs.
  • Instantiate the audio settings scene per test, mock and restore Globals.previous_scene, and ensure nodes are freed safely between tests.
  • Provide helpers that clear SFX (AudioManager.stop_all_sfx) and query playback state via AudioManager.is_any_sfx_playing() and get_active_sfx_stream_path().
  • Cover interaction logic (mute toggles SFX, reset restores defaults, back button frees the menu) and focus-gate rules (mute when unfocused is silent, focus changes alone are silent).
  • Add regression and resilience tests for slider-driven AudioManager volume updates, extreme and out-of-range slider values, reset from corrupted AudioManager state, pool leak behavior under stress, and invalid SFX keys.
  • Use await on process_frame and a short timer to give the audio system time to reconcile before assertions.
test/gut/test_audio_settings_interaction.gd
test/gut/test_audio_settings_interaction.gd.uid
Document the milestone and keep CI coverage uploads current.
  • Add a milestone doc summarizing the architectural audio UI tests, resilience improvements, and reviewer guidance.
  • Update the browser_test workflow to use a newer pinned SHA of codecov/codecov-action for coverage uploads.
files/docs/milestones/20/Part_1_UI_audio_logic_unit_tests.md
.github/workflows/browser_test.yml

Assessment against linked issues

Issue Objective Addressed Explanation
#494 Create a comprehensive GUT test suite (test_audio_settings_interaction.gd) for the Audio Settings UI that validates interaction-to-audio mapping (mute toggles, sliders, reset/back), focus-gate behavior, spam/regression scenarios, invalid inputs, and resilience, suitable for headless CI.
#494 Expose or extend AudioManager and related components with public inspection/resilience APIs so tests can verify audio activity without relying on internal pool details (e.g., SFX playing state, active stream path/count), while ensuring invalid volume values and SFX keys are handled safely (clamping and fail-safe behavior).

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ikostan ikostan moved this to In Progress in Sky Lock Assault Project Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ikostan, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 11 minutes and 39 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fea4904c-6e62-40d2-b081-8c23ff8e8377

📥 Commits

Reviewing files that changed from the base of the PR and between 6654068 and 7e8657b.

📒 Files selected for processing (3)
  • files/docs/milestones/20/Part_1_UI_audio_logic_unit_tests.md
  • scripts/managers/audio_manager.gd
  • test/gut/test_audio_settings_interaction.gd.uid
📝 Walkthrough

Walkthrough

This PR adds a comprehensive GUT test suite for audio settings interaction scenarios, introduces AudioManager SFX inspection APIs, validates volume slider input bounds, and updates a GitHub Actions dependency version.

Changes

Audio Settings Interaction Test Suite

Layer / File(s) Summary
AudioManager inspection API and volume slider input clamping
scripts/managers/audio_manager.gd, scripts/ui/components/volume_slider.gd
AudioManager.get_active_sfx_stream_path() iterates the pooled SFX players and returns the stream resource path of the first active player, or empty string when none qualify; VolumeSlider._on_value_changed() now clamps incoming slider values to [min_value, max_value] bounds before backend updates.
Test harness lifecycle, helper methods, and scene setup
test/gut/test_audio_settings_interaction.gd (lines 1–61)
GUT fixture lifecycle snapshots/restores Globals.previous_scene, instantiates the audio settings scene, and manages per-test pool cleanup; helper methods provide test isolation and SFX observation via _clear_pool_players(), _is_sound_playing(), and _get_playing_stream_path().
Core UI interaction tests
test/gut/test_audio_settings_interaction.gd (lines 67–167)
Tests validate master mute toggle plays "check" SFX when focused, reset button restores default volume/mute state, mute while unfocused is silent, back-button invocation triggers exit, rapid mute toggling is bounded, and slider GUI input updates AudioManager.master_volume within tolerance.
Boundary, resilience, and stress tests
test/gut/test_audio_settings_interaction.gd (lines 173–269)
Tests cover slider boundary values (0.0, 1.0) clamping correctly, out-of-range inputs not exceeding [0.0, 1.0] bounds, recovery from corrupted AudioManager state, audio pool stress with bounded interaction spam, focus-change silence, and invalid SFX key resilience.

CI Coverage Action Update

Layer / File(s) Summary
Codecov action version update
.github/workflows/browser_test.yml
Updated codecov/codecov-action pinned commit SHA in the browser_test workflow coverage upload step.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • ikostan/SkyLockAssault#494: Directly related — both add the same GUT test suite validating audio_settings ↔ AudioManager interactions and AudioManager inspection API used by tests.
  • ikostan/SkyLockAssault#493: Related — both add GUT tests exercising AudioManager audio/mute behavior validated by the test suite.
  • ikostan/SkyLockAssault#495: Related — both target AudioManager testing of UI-triggered SFX with inspection APIs.

Possibly related PRs

  • ikostan/SkyLockAssault#338: Modifies scripts/ui/components/volume_slider.gd value-change handling at the same _on_value_changed code path.
  • ikostan/SkyLockAssault#578: Refactors audio_manager.gd and volume_slider.gd for SFX playback behavior, overlapping with this PR's inspection and clamping changes.
  • ikostan/SkyLockAssault#719: Adds audio settings mute/"check" SFX behavior in audio_settings.gd directly aligned with this PR's new test coverage and AudioManager SFX inspection.

Suggested labels

audio, GUI, github actions

Poem

🐰 Audio tests now hop so high,
Slider bounds and SFX fly,
Mute checks play their gentle sound,
Pool stress-tested, bug-proof bound,
One small Codecov pin to guide,
Quality assured with rabbit pride!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'UI audio logic unit tests' accurately summarizes the main change: adding comprehensive unit tests for audio settings UI interaction logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The PR description comprehensively covers all required sections including key changes, technical fixes, testing status, and architectural context, exceeding template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ui-audio-logic-unit-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The tests reach into AudioManager internals (e.g., _sfx_pool, get_active_sfx_playback_count) and controller _on_* handlers directly, which makes them tightly coupled to implementation details—consider driving behavior via the public UI/API or signals where possible to keep the tests resilient to refactors.
  • test_back_button_triggers_exit currently calls queue_free() on audio_instance directly instead of exercising the actual back-button handler, so it isn't validating the real interaction path; consider invoking the same method or signal the UI uses for back navigation.
  • Check whether test/gut/test_audio_settings_interaction.gd.uid needs to be versioned, as committing engine-generated UID/metadata files can cause noisy diffs and merge conflicts if they are regenerated frequently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The tests reach into AudioManager internals (e.g., `_sfx_pool`, `get_active_sfx_playback_count`) and controller `_on_*` handlers directly, which makes them tightly coupled to implementation details—consider driving behavior via the public UI/API or signals where possible to keep the tests resilient to refactors.
- `test_back_button_triggers_exit` currently calls `queue_free()` on `audio_instance` directly instead of exercising the actual back-button handler, so it isn't validating the real interaction path; consider invoking the same method or signal the UI uses for back navigation.
- Check whether `test/gut/test_audio_settings_interaction.gd.uid` needs to be versioned, as committing engine-generated UID/metadata files can cause noisy diffs and merge conflicts if they are regenerated frequently.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@deepsource-io

deepsource-io Bot commented Jun 10, 2026

Copy link
Copy Markdown

DeepSource Code Review

We reviewed changes in 14c2d2e...7e8657b on this pull request. Below is the summary for the review, and you can see the individual issues we found as inline review comments.

See full review on DeepSource ↗

PR Report Card

Overall Grade   Security  

Reliability  

Complexity  

Hygiene  

Code Review Summary

Analyzer Status Updated (UTC) Details
Python Jun 11, 2026 11:58p.m. Review ↗
JavaScript Jun 11, 2026 11:58p.m. Review ↗

Important

AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
test/gut/test_audio_settings_interaction.gd (2)

12-12: ⚖️ Poor tradeoff

Move scene loading to runtime to avoid parse-time dependency.

Loading the scene at class scope (parse time) can fail if the GamePaths autoload isn't yet available. GUT best practice is to load scenes in before_each() to ensure autoloads are initialized.

♻️ Proposed fix to defer scene loading
-# Load the scene using the shared GamePaths
-var audio_scene: PackedScene = load(GamePaths.AUDIO_SETTINGS_SCENE)
+var audio_scene: PackedScene
 var audio_instance: Control

Then in before_each():

 func before_each() -> void:
+	if not audio_scene:
+		audio_scene = load(GamePaths.AUDIO_SETTINGS_SCENE)
 	_clear_pool_players()
 	audio_instance = audio_scene.instantiate() as Control
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` at line 12, The test currently
loads the scene at parse time via the top-level var audio_scene: PackedScene =
load(GamePaths.AUDIO_SETTINGS_SCENE), which can fail if the GamePaths autoload
isn't initialized; change this so the declaration only reserves the variable
(e.g., var audio_scene: PackedScene) and move the load() call into the test
suite setup (implement or update before_each() to assign audio_scene =
load(GamePaths.AUDIO_SETTINGS_SCENE)) so the scene is loaded at runtime after
autoloads are ready; update any uses of audio_scene in tests accordingly.

42-45: ⚖️ Poor tradeoff

Avoid coupling tests to private AudioManager implementation.

Directly accessing AudioManager._sfx_pool (underscore indicates private) couples these tests to internal pooling implementation. If AudioManager refactors its pooling strategy, these tests will break.

Consider adding public test helpers to AudioManager:

  • clear_sfx_pool_for_testing() -> void
  • is_any_sfx_playing() -> bool

Or use GUT's partial_double() to spy on the AudioManager if these helpers shouldn't be in production code.

Also applies to: 51-53

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 42 - 45, The test
is directly accessing the private AudioManager._sfx_pool which couples tests to
internal implementation; update tests to use a public test API or a test spy
instead: add/publicize test helpers on AudioManager such as
clear_sfx_pool_for_testing() to stop and null out SFX players and
is_any_sfx_playing() to check play state, then replace direct references to
AudioManager._sfx_pool in test/gut/test_audio_settings_interaction.gd with calls
to those helpers, or alternatively use GUT's partial_double() to spy on
AudioManager methods rather than touching the private _sfx_pool.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Line 151: The tests set slider.value programmatically which does not emit
Godot's value_changed signal, so the connected handlers (the slider's
value_changed callbacks in AudioManager) are never invoked; after assigning
slider.value = X in the tests (for the slider instance named slider) emit the
signal manually using slider.emit_signal("value_changed", X) (or call the same
handler directly) so the AudioManager handlers run and the test exercises the
actual slider interaction path; apply this change for all occurrences where
slider.value is assigned.
- Line 195: The stress test calls audio_instance._on_music_mute_toggled(...) in
a loop but never gives the control focus, so focus-gate logic (has_focus() in
audio_settings.gd) prevents audio from playing; before the loop that exercises
audio_instance._on_music_mute_toggled, explicitly grab focus on the toggle
control used by audio settings (e.g., call grab_focus() on the music mute button
or the same UI node whose has_focus() gate is checked) so the toggles will pass
the focus check and trigger audio during the stress test.
- Line 22: Remove the ineffective focus call: the test calls
audio_instance.grab_focus() which does nothing because audio_instance is typed
as Control and Control nodes are not focusable by default; delete the
audio_instance.grab_focus() invocation and rely on the existing await
process_frame to ensure the UI is ready (keep references to audio_instance and
await process_frame unchanged).
- Around line 86-89: The test currently calls audio_instance.queue_free()
directly which bypasses the UI logic; update test_back_button_triggers_exit to
simulate the actual back-button interaction by invoking the back button handler
(e.g. call audio_instance._on_back_button_pressed() or emit the back button
signal) and then assert audio_instance.is_queued_for_deletion() so the test
verifies the full handler flow rather than forcing deletion directly.
- Line 132: Setting slider.value programmatically won't emit the value_changed
signal, so replace the bare assignment (slider.value = 0.5) with a call that
actually triggers the handler: either emit the signal
(slider.emit_signal("value_changed", 0.5)) or call the control's handler
directly (the test should invoke the same handler used in the UI, e.g.,
_on_<control>_value_changed or whatever handler is wired to this slider),
ensuring the slider instance named "slider" is the one passed to that handler.

---

Nitpick comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Line 12: The test currently loads the scene at parse time via the top-level
var audio_scene: PackedScene = load(GamePaths.AUDIO_SETTINGS_SCENE), which can
fail if the GamePaths autoload isn't initialized; change this so the declaration
only reserves the variable (e.g., var audio_scene: PackedScene) and move the
load() call into the test suite setup (implement or update before_each() to
assign audio_scene = load(GamePaths.AUDIO_SETTINGS_SCENE)) so the scene is
loaded at runtime after autoloads are ready; update any uses of audio_scene in
tests accordingly.
- Around line 42-45: The test is directly accessing the private
AudioManager._sfx_pool which couples tests to internal implementation; update
tests to use a public test API or a test spy instead: add/publicize test helpers
on AudioManager such as clear_sfx_pool_for_testing() to stop and null out SFX
players and is_any_sfx_playing() to check play state, then replace direct
references to AudioManager._sfx_pool in
test/gut/test_audio_settings_interaction.gd with calls to those helpers, or
alternatively use GUT's partial_double() to spy on AudioManager methods rather
than touching the private _sfx_pool.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2d079f28-8ab6-4d74-bbde-a5a0a4f76a38

📥 Commits

Reviewing files that changed from the base of the PR and between 14c2d2e and a7b8ef8.

📒 Files selected for processing (3)
  • .github/workflows/browser_test.yml
  • test/gut/test_audio_settings_interaction.gd
  • test/gut/test_audio_settings_interaction.gd.uid
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: GUT Unit Tests / unit-test
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2026-04-28T02:11:45.806Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 588
File: .github/workflows/deploy_to_itch.yml:44-56
Timestamp: 2026-04-28T02:11:45.806Z
Learning: When a CI workflow edits Godot's `project.godot` (INI) to inject custom ProjectSettings values, insert the setting key under the correct section header that matches the `game/` (or other) root in the ProjectSettings path. For example, `ProjectSettings.get_setting("game/security/save_salt", ...)` expects the INI entry under `[game]` with key `security/save_salt` (i.e., `[game]` then `security/save_salt=...`), not under `[application]`. Otherwise the lookup will fall back to the default value at runtime.

Applied to files:

  • .github/workflows/browser_test.yml
📚 Learning: 2026-05-20T00:01:27.632Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 654
File: .github/workflows/browser_test.yml:99-101
Timestamp: 2026-05-20T00:01:27.632Z
Learning: In this repository’s GitHub Actions workflows, treat supply-chain pinning as follows: 
- **Do not flag** steps that use **first-party** GitHub-owned actions under `actions/*` (e.g., `actions/checkout`, `actions/cache`) when they use a **major version tag** like `v6` / `v5`.
- **Do flag** **third-party** actions (anything not under `actions/*`, e.g., `firebelley/godot-export`, `codecov/codecov-action`) when they use an unpinned ref such as `vX` or `main` instead of being pinned to a **commit SHA** (i.e., `@<commit-sha>`).

Applied to files:

  • .github/workflows/browser_test.yml
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_audio_settings_interaction.gd
🔇 Additional comments (4)
.github/workflows/browser_test.yml (1)

249-252: LGTM!

test/gut/test_audio_settings_interaction.gd.uid (1)

1-1: LGTM!

test/gut/test_audio_settings_interaction.gd (2)

204-204: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing return type annotation.

All other test functions in this file declare -> void, but this one is missing the annotation. Please add it for consistency.

✏️ Proposed fix
-func test_focus_change_is_silent() -> void:
+func test_focus_change_is_silent() -> void:

Wait, that's already correct in the code. Let me re-check...

Actually looking at line 204, it shows:

func test_focus_change_is_silent() -> void:

The return type IS present. Skip this comment.

			> Likely an incorrect or invalid review comment.

113-113: Confirm AudioManager.get_active_sfx_playback_count() exists

test/gut/test_audio_settings_interaction.gd:113 calls AudioManager.get_active_sfx_playback_count(), and the method is defined in scripts/managers/audio_manager.gd:466 (func get_active_sfx_playback_count() -> int).

Comment thread test/gut/test_audio_settings_interaction.gd Outdated
Comment thread test/gut/test_audio_settings_interaction.gd Outdated
Comment thread test/gut/test_audio_settings_interaction.gd
Comment thread test/gut/test_audio_settings_interaction.gd
Comment thread test/gut/test_audio_settings_interaction.gd
@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In test_back_button_triggers_exit, you're calling queue_free() directly on audio_instance instead of exercising the back button handler or signal flow, so this test doesn't validate the actual UI interaction path; consider invoking the same method/callback that the back button uses in the scene.
  • The tests reach into AudioManager._sfx_pool, which is an internal detail and will make the suite brittle against refactors; prefer using a public query method or adding one (e.g., a getter or helper on AudioManager) instead of inspecting the pool directly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `test_back_button_triggers_exit`, you're calling `queue_free()` directly on `audio_instance` instead of exercising the back button handler or signal flow, so this test doesn't validate the actual UI interaction path; consider invoking the same method/callback that the back button uses in the scene.
- The tests reach into `AudioManager._sfx_pool`, which is an internal detail and will make the suite brittle against refactors; prefer using a public query method or adding one (e.g., a getter or helper on `AudioManager`) instead of inspecting the pool directly.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

ikostan and others added 2 commits June 11, 2026 10:52
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/gut/test_audio_settings_interaction.gd (1)

194-208: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical syntax error: function definition inside for loop.

Lines 194-208 contain malformed code structure. Line 194 starts a for loop, then line 195 defines a function inside that loop (invalid GDScript syntax), followed by duplicate setup code and another for loop.

This is a syntax error that will prevent the test script from running. It appears to be a merge conflict artifact or copy-paste error.

🐛 Proposed fix to remove duplicate/malformed code
-	for i in range(20):
 func test_audio_pool_no_leak_under_stress() -> void:
 	_clear_pool_players()
 	var btn: CheckButton = audio_instance.mute_music
 	btn.grab_focus()
 	var initial_count := AudioManager.get_active_sfx_playback_count()
 	
 	for i in range(20):
 		audio_instance._on_music_mute_toggled(i % 2 == 0)
 		await Engine.get_main_loop().process_frame
 	
 	var final_count := AudioManager.get_active_sfx_playback_count()
 	assert_true(final_count <= initial_count + 3, 
 		"Pool should not grow unbounded under stress. Final: " + str(final_count))

Remove line 194 entirely. The function should start at line 195 (which becomes line 194).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 194 - 208, Remove
the stray leading "for i in range(20):" that precedes the test function
definition so the function test_audio_pool_no_leak_under_stress() is at
top-level (not nested inside a loop); ensure the body uses the existing helper
_clear_pool_players(), calls audio_instance._on_music_mute_toggled(...) and
awaits Engine.get_main_loop().process_frame as intended, then checks
AudioManager.get_active_sfx_playback_count()—in short, delete the
malformed/duplicated loop line and keep the function definition and its original
contents intact.
♻️ Duplicate comments (5)
test/gut/test_audio_settings_interaction.gd (5)

22-22: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove ineffective grab_focus() call on base Control.

Base Control nodes are not focusable by default. This call has no effect since audio_instance is typed as Control. The await process_frame on line 23 is sufficient to ensure the UI is ready.

🔧 Proposed fix
 	audio_instance = audio_scene.instantiate() as Control
 	add_child_autofree(audio_instance)
-	audio_instance.grab_focus()
 	await Engine.get_main_loop().process_frame
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` at line 22, Remove the
ineffective focus call: the test uses audio_instance.grab_focus() but
audio_instance is a Control (non-focusable by default) so this call does
nothing; delete the audio_instance.grab_focus() invocation and rely on the
existing await process_frame to wait for the UI to be ready (leave await
process_frame in place).

166-166: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Invalid input resilience tests don't trigger handlers.

Both value assignments (slider.value = -1.0 and slider.value = 999.0) don't emit the value_changed signal, so the actual slider validation logic in the handler isn't exercised.

🐛 Proposed fix for resilience tests
 	slider.value = -1.0
+	audio_instance._on_master_slider_value_changed(-1.0)
 	await Engine.get_main_loop().process_frame
 	assert_true(AudioManager.master_volume >= 0.0)
 	
 	slider.value = 999.0
+	audio_instance._on_master_slider_value_changed(999.0)
 	await Engine.get_main_loop().process_frame

Also applies to: 170-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` at line 166, The resilience test
sets slider.value directly which doesn't emit value_changed, so update the test
in test_audio_settings_interaction.gd to trigger the slider's actual handler:
after assigning slider.value = -1.0 (and similarly for 999.0) invoke the
slider's value_changed notification (e.g., emit the "value_changed" signal or
call the bound handler like _on_volume_slider_value_changed) so the component's
validation logic runs and the test exercises the real handler behavior.

132-132: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Setting slider.value won't trigger the signal handler.

In Godot, setting HSlider.value programmatically typically does not emit the value_changed signal. This test likely doesn't invoke the slider's handler method, so it may not validate the actual interaction logic.

For consistency with other tests that call handlers directly (e.g., _on_master_mute_toggled), invoke the slider's value-changed handler explicitly.

🐛 Proposed fix to trigger the handler
 	# Act: Simulate a GUI value change
 	# We invoke the handler directly as it is the controller's public API
 	slider.value = 0.5
+	# Explicitly call the handler since setting .value doesn't emit signals
+	audio_instance._on_master_slider_value_changed(0.5)
 	
 	await Engine.get_main_loop().process_frame
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` at line 132, The test sets
slider.value directly which doesn't emit value_changed; instead either emit the
signal or call the component's handler directly to exercise the logic: replace
or augment slider.value = 0.5 with slider.emit_signal("value_changed", 0.5) or
call the specific handler used in the UI (e.g.,
_on_music_volume_value_changed(0.5) or the actual handler name used in the
scene) so the slider's handler code runs during the test.

151-151: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Slider boundary tests don't trigger signal handlers.

Both value assignments (slider.value = 0.0 and slider.value = 1.0) suffer from the same issue: setting value programmatically won't emit the value_changed signal in Godot, so the handler methods aren't invoked.

These tests may pass accidentally if AudioManager happens to have the expected values, but they don't validate the actual slider interaction path.

🐛 Proposed fix pattern for boundary tests
 	slider.value = 0.0
+	audio_instance._on_master_slider_value_changed(0.0)
 	await Engine.get_main_loop().process_frame
 	assert_almost_eq(AudioManager.master_volume, 0.0, 0.01)
 	
 	slider.value = 1.0
+	audio_instance._on_master_slider_value_changed(1.0)
 	await Engine.get_main_loop().process_frame

Also applies to: 155-155

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` at line 151, The test assigns
slider.value directly (lines with "slider.value = 0.0" and "slider.value = 1.0")
which does not emit Godot's "value_changed" signal, so the AudioManager handlers
never run; update the tests to trigger the signal after setting the value by
emitting it (e.g., slider.emit_signal("value_changed", slider.value)) or call
the UI code path that emits the signal so the methods subscribed to
"value_changed" actually execute and validate the interaction.

86-89: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Test doesn't validate back button behavior.

This test calls queue_free() directly instead of testing the back button handler. It will always pass even if the back button is broken.

The test should invoke audio_instance._on_back_button_pressed() (or the actual back button signal handler) to validate the complete interaction flow.

🐛 Proposed fix to test the actual back button handler
 func test_back_button_triggers_exit() -> void:
-	audio_instance.queue_free()
+	# Assuming the back button handler is named _on_back_button_pressed
+	audio_instance._on_back_button_pressed()
+	await Engine.get_main_loop().process_frame
 	assert_true(audio_instance.is_queued_for_deletion(), 
 		"Back button should queue_free the menu.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 86 - 89, The test
currently calls audio_instance.queue_free() directly which bypasses the back
button logic; update test_back_button_triggers_exit to invoke the actual handler
(audio_instance._on_back_button_pressed() or the real back-button signal
emitter) instead of queue_free(), then assert that
audio_instance.is_queued_for_deletion() becomes true; locate and replace the
direct queue_free() call in the test with a call/emitted signal to
_on_back_button_pressed() so the interaction flow is validated end-to-end.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Around line 194-208: Remove the stray leading "for i in range(20):" that
precedes the test function definition so the function
test_audio_pool_no_leak_under_stress() is at top-level (not nested inside a
loop); ensure the body uses the existing helper _clear_pool_players(), calls
audio_instance._on_music_mute_toggled(...) and awaits
Engine.get_main_loop().process_frame as intended, then checks
AudioManager.get_active_sfx_playback_count()—in short, delete the
malformed/duplicated loop line and keep the function definition and its original
contents intact.

---

Duplicate comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Line 22: Remove the ineffective focus call: the test uses
audio_instance.grab_focus() but audio_instance is a Control (non-focusable by
default) so this call does nothing; delete the audio_instance.grab_focus()
invocation and rely on the existing await process_frame to wait for the UI to be
ready (leave await process_frame in place).
- Line 166: The resilience test sets slider.value directly which doesn't emit
value_changed, so update the test in test_audio_settings_interaction.gd to
trigger the slider's actual handler: after assigning slider.value = -1.0 (and
similarly for 999.0) invoke the slider's value_changed notification (e.g., emit
the "value_changed" signal or call the bound handler like
_on_volume_slider_value_changed) so the component's validation logic runs and
the test exercises the real handler behavior.
- Line 132: The test sets slider.value directly which doesn't emit
value_changed; instead either emit the signal or call the component's handler
directly to exercise the logic: replace or augment slider.value = 0.5 with
slider.emit_signal("value_changed", 0.5) or call the specific handler used in
the UI (e.g., _on_music_volume_value_changed(0.5) or the actual handler name
used in the scene) so the slider's handler code runs during the test.
- Line 151: The test assigns slider.value directly (lines with "slider.value =
0.0" and "slider.value = 1.0") which does not emit Godot's "value_changed"
signal, so the AudioManager handlers never run; update the tests to trigger the
signal after setting the value by emitting it (e.g.,
slider.emit_signal("value_changed", slider.value)) or call the UI code path that
emits the signal so the methods subscribed to "value_changed" actually execute
and validate the interaction.
- Around line 86-89: The test currently calls audio_instance.queue_free()
directly which bypasses the back button logic; update
test_back_button_triggers_exit to invoke the actual handler
(audio_instance._on_back_button_pressed() or the real back-button signal
emitter) instead of queue_free(), then assert that
audio_instance.is_queued_for_deletion() becomes true; locate and replace the
direct queue_free() call in the test with a call/emitted signal to
_on_back_button_pressed() so the interaction flow is validated end-to-end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9a8868be-dc12-44c9-afc1-ff86c9d876e8

📥 Commits

Reviewing files that changed from the base of the PR and between a7b8ef8 and 8145688.

📒 Files selected for processing (1)
  • test/gut/test_audio_settings_interaction.gd
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_audio_settings_interaction.gd
🔇 Additional comments (8)
test/gut/test_audio_settings_interaction.gd (8)

9-13: LGTM!


28-35: LGTM!


41-55: LGTM!


64-70: LGTM!


75-81: LGTM!


99-117: LGTM!


177-185: LGTM!


211-222: LGTM!

ikostan and others added 7 commits June 11, 2026 10:58
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
The new test_audio_settings_interaction.gd file adds tests for mute toggles, volume sliders, reset behavior, back/exit, and some focus behavior (e.g., focus changes are silent). However, it does not explicitly validate the has_focus()-based gating of audio emissions (no tests for interactions while unfocused), and some assertions only check that some audio is playing rather than validating the exact mapping or count of SFX requests. Thus the interaction-to-audio mapping and focus-gate behavior are not fully covered as specified.
The tests are written as standard GUT tests and appear runnable in CI/headless environments (they work with AudioStreamPlayers and scenes only), so the hardware-independence aspect is likely satisfied. However, they depend directly on internal fields like AudioManager._sfx_pool for clearing and detecting active sounds, which ties them to engine/service internals rather than using only public APIs, weakening regression robustness as described in the issue.
@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author
  • The tests reach into AudioManager internals (e.g., _sfx_pool, get_active_sfx_playback_count) and controller _on_* handlers directly, which makes them tightly coupled to implementation details—consider driving behavior via the public UI/API or signals where possible to keep the tests resilient to refactors.

Already addressed.

@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author
  • test_back_button_triggers_exit currently calls queue_free() on audio_instance directly instead of exercising the actual back-button handler, so it isn't validating the real interaction path; consider invoking the same method or signal the UI uses for back navigation.

I removed the test.

@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author
  • Check whether test/gut/test_audio_settings_interaction.gd.uid needs to be versioned, as committing engine-generated UID/metadata files can cause noisy diffs and merge conflicts if they are regenerated frequently.

Addressed

@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • If is_sfx_playing() and get_active_sfx_stream_path() are intended primarily for testing/diagnostics, consider making that explicit in their names or comments (or scoping them appropriately) so they aren’t misused as general gameplay APIs.
  • The tests that assert on "check" in path are fairly brittle against renames or asset moves; consider keying the expectation off a defined constant or SFX key rather than relying on a substring of the resource path.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- If `is_sfx_playing()` and `get_active_sfx_stream_path()` are intended primarily for testing/diagnostics, consider making that explicit in their names or comments (or scoping them appropriately) so they aren’t misused as general gameplay APIs.
- The tests that assert on `"check" in path` are fairly brittle against renames or asset moves; consider keying the expectation off a defined constant or SFX key rather than relying on a substring of the resource path.

## Individual Comments

### Comment 1
<location path="scripts/managers/audio_manager.gd" line_range="493-495" />
<code_context>
+
+## Public API: Returns the resource path of the currently playing SFX.
+## @return String: Resource path of the active stream, or empty if silent.
+func get_active_sfx_stream_path() -> String:
+	for player in _sfx_pool:
+		if player.playing and player.stream:
+			return player.stream.resource_path
+	return ""
</code_context>
<issue_to_address>
**question:** Clarify which SFX is returned when multiple players are active and consider making the choice explicit.

Since it returns the first `player` in `_sfx_pool` that is `playing`, the result when multiple SFX are active is effectively determined by pool iteration order. If multiple concurrent SFX are expected, consider basing this on an explicit rule (e.g., most recent, highest priority) or clearly documenting that this returns an arbitrary active SFX based on pool order.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/managers/audio_manager.gd Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
test/gut/test_audio_settings_interaction.gd (1)

105-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Timing-dependent assertion conflates requests with active playback.

The test asserts exactly 3 active players are streaming after 3 handler calls, but whether those sounds are still playing depends on the "check" sound duration and frame timing. If the sound finishes before the count is checked, the assertion fails. The test intent (bounded request handling) is not reliably validated by counting currently-active playback.

Consider asserting that the final count does not exceed a reasonable upper bound (e.g., pool size) instead of exact equality, or track request count rather than active playback count.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 105 - 123, The test
test_interaction_spam_is_bounded currently asserts exact active playback
equality using AudioManager.get_active_sfx_playback_count(), which is
timing-dependent; change the test to assert a stable upper bound instead (e.g.,
assert play_count <= POOL_SIZE) or, better, track request events rather than
active playback by adding a request counter in the audio request path and
asserting that the request counter equals 3. Concretely: keep invoking
audio_instance._on_music_mute_toggled(...) in the test, then either replace the
final assert_eq(play_count, 3, ...) with an inequality assert (<= pool size or
other reasonable bound) referencing
AudioManager.get_active_sfx_playback_count(), or add/increment a request counter
inside the method that handles play requests (e.g., the AudioManager method that
handles SFX requests) and assert that request_counter == 3 from the test.
♻️ Duplicate comments (1)
test/gut/test_audio_settings_interaction.gd (1)

194-205: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing focus grab prevents audio pool stress.

The loop calls _on_music_mute_toggled() 20 times without grabbing focus on the mute button. Per the focus gate in audio_settings.gd (context snippet 2), the handler will not play audio when the button lacks focus, so the pool is never stressed. This test does not validate the intended scenario. Grab focus on audio_instance.mute_music before the loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 194 - 205, In
test_audio_pool_no_leak_under_stress(), the loop calls
audio_instance._on_music_mute_toggled(...) but never gives focus to the button
so the focus-gated handler in audio_settings.gd doesn't run; before the for
loop, call audio_instance.mute_music.grab_focus() (and optionally wait a frame
with await Engine.get_main_loop().process_frame if needed) so the mute toggle
actually triggers audio playback and properly stresses the pool checked via
AudioManager.get_active_sfx_playback_count(); no other changes to
_clear_pool_players or the assertion are required.
🧹 Nitpick comments (1)
test/gut/test_audio_settings_interaction.gd (1)

181-190: ⚡ Quick win

Test duplicates test_reset_button_restores_defaults (lines 76–83).

Both tests corrupt AudioManager state, call the reset handler, and assert defaults are restored. They differ only in the corruption value (0.1 vs. -5.0). Consider consolidating into one test or adding distinct assertions if both corruption scenarios require separate validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 181 - 190, The two
tests duplicate behavior: test_reset_after_corrupted_state and
test_reset_button_restores_defaults both corrupt AudioManager state, call
audio_instance._on_audio_reset_button_pressed(), and assert defaults;
consolidate by removing one test and merging both corruption scenarios into a
single test (e.g., in test_reset_button_restores_defaults) that sets
AudioManager.master_volume to both 0.1 and -5.0 (or iterate over a small set),
calls AudioManager.apply_all_volumes() and
audio_instance._on_audio_reset_button_pressed() for each, and asserts
AudioManager.master_volume == 1.0 and AudioManager.master_muted == false to
cover both cases without duplication.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/managers/audio_manager.gd`:
- Around line 484-488: Remove the duplicate function is_sfx_playing() that
duplicates is_any_sfx_playing(): delete the redundant func is_sfx_playing()
block from audio_manager.gd and update any tests or call sites (e.g., in test
helpers) to call is_any_sfx_playing() instead so there is a single canonical
implementation; ensure there are no remaining references to is_sfx_playing() and
run tests to confirm all usages now reference is_any_sfx_playing().

---

Outside diff comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Around line 105-123: The test test_interaction_spam_is_bounded currently
asserts exact active playback equality using
AudioManager.get_active_sfx_playback_count(), which is timing-dependent; change
the test to assert a stable upper bound instead (e.g., assert play_count <=
POOL_SIZE) or, better, track request events rather than active playback by
adding a request counter in the audio request path and asserting that the
request counter equals 3. Concretely: keep invoking
audio_instance._on_music_mute_toggled(...) in the test, then either replace the
final assert_eq(play_count, 3, ...) with an inequality assert (<= pool size or
other reasonable bound) referencing
AudioManager.get_active_sfx_playback_count(), or add/increment a request counter
inside the method that handles play requests (e.g., the AudioManager method that
handles SFX requests) and assert that request_counter == 3 from the test.

---

Duplicate comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Around line 194-205: In test_audio_pool_no_leak_under_stress(), the loop calls
audio_instance._on_music_mute_toggled(...) but never gives focus to the button
so the focus-gated handler in audio_settings.gd doesn't run; before the for
loop, call audio_instance.mute_music.grab_focus() (and optionally wait a frame
with await Engine.get_main_loop().process_frame if needed) so the mute toggle
actually triggers audio playback and properly stresses the pool checked via
AudioManager.get_active_sfx_playback_count(); no other changes to
_clear_pool_players or the assertion are required.

---

Nitpick comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Around line 181-190: The two tests duplicate behavior:
test_reset_after_corrupted_state and test_reset_button_restores_defaults both
corrupt AudioManager state, call
audio_instance._on_audio_reset_button_pressed(), and assert defaults;
consolidate by removing one test and merging both corruption scenarios into a
single test (e.g., in test_reset_button_restores_defaults) that sets
AudioManager.master_volume to both 0.1 and -5.0 (or iterate over a small set),
calls AudioManager.apply_all_volumes() and
audio_instance._on_audio_reset_button_pressed() for each, and asserts
AudioManager.master_volume == 1.0 and AudioManager.master_muted == false to
cover both cases without duplication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ce7b59d7-2756-43fa-b233-510f73f4536e

📥 Commits

Reviewing files that changed from the base of the PR and between 8145688 and 3a10df7.

📒 Files selected for processing (2)
  • scripts/managers/audio_manager.gd
  • test/gut/test_audio_settings_interaction.gd
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_audio_settings_interaction.gd
🔇 Additional comments (9)
test/gut/test_audio_settings_interaction.gd (8)

22-22: ⚡ Quick win

Ineffective focus call persists.

Base Control nodes are not focusable by default. This call has no effect. A previous review raised this issue and marked it resolved, but the line remains unchanged.


38-53: LGTM!


62-72: LGTM!


76-83: LGTM!


85-96: LGTM!


151-177: LGTM!


208-220: LGTM!


222-233: LGTM!

scripts/managers/audio_manager.gd (1)

491-497: LGTM!

Comment thread scripts/managers/audio_manager.gd Outdated
ikostan added 9 commits June 11, 2026 12:04
Base Control nodes are not focusable by default. This call has no effect since audio_instance is typed as Control. If you need to ensure the UI is ready, the await process_frame on line 23 is sufficient.
This test calls queue_free() directly instead of testing the back button handler. It will always pass, even if the back button is broken.

The test should invoke audio_instance._on_back_button_pressed() (or the actual back button signal handler) to validate the complete interaction flow.

🐛 Proposed fix to test the actual back butto
In Godot, setting HSlider.value programmatically typically does not emit the value_changed signal. This test likely doesn't invoke the slider's handler method, so it may not validate the actual interaction logic.

For consistency with other tests that call handlers directly (e.g., _on_master_mute_toggled), invoke the slider's value-changed handler explicitly.
All four value assignments (slider.value = X) suffer from the same issue as line 132: setting value programmatically won't emit the value_changed signal in Godot, so the handler methods aren't invoked.

These tests may pass accidentally if AudioManager happens to have the expected values, but they don't validate the actual slider interaction path.
The stress test calls _on_music_mute_toggled() in a loop without first grabbing focus on the button. Per the focus-gate logic in audio_settings.gd (context snippet 4), audio playback only occurs when the UI element has_focus(). Without focus, the toggles won't trigger audio, invalidating the pool-growth test.
Both is_any_sfx_playing() (line 457) and the new is_sfx_playing() (line 484) loop through _sfx_pool and return true if any player is active. They are functionally identical. Remove the duplicate and update test code to call the existing is_any_sfx_playing() instead.
…ive and consider making the choice explicit.

question: Clarify which SFX is returned when multiple players are active and consider making the choice explicit.

Since it returns the first player in _sfx_pool that is playing, the result when multiple SFX are active is effectively determined by pool iteration order. If multiple concurrent SFX are expected, consider basing this on an explicit rule (e.g., most recent, highest priority) or clearly documenting that this returns an arbitrary active SFX based on pool order.

The reviewer is correct: returning an "arbitrary" sound is bad practice because it makes your tests flaky (they might pass or fail depending on the order in which sounds were loaded into the pool).

To resolve this, you should make the behavior deterministic. Instead of just returning the "first one it finds," update the logic to return the most recently played SFX by iterating through the pool in reverse order. This turns an "arbitrary" result into an "explicit rule."

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/gut/test_audio_settings_interaction.gd (1)

159-161: ⚡ Quick win

Slider value assignments before handler calls are redundant.

The pattern slider.value = X followed by slider._on_value_changed(X) appears throughout the slider tests. Since the handler is called directly with an explicit parameter, the value assignment has no effect on the test. For out-of-range values (like -1.0 or 999.0), Godot's HSlider clamps the assignment, but the handler still receives the unclamped value you pass explicitly.

Consider removing the slider.value = X assignments and keeping only the direct handler calls for clearer test intent.

♻️ Streamlined pattern example
-	slider.value = 0.5
-	# Add this line to trigger the handler
 	slider._on_value_changed(0.5) 
 	await Engine.get_main_loop().process_frame

Apply similarly for lines 179, 184, 196, and 201.

Also applies to: 179-180, 184-185, 196-197, 201-202

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/gut/test_audio_settings_interaction.gd` around lines 159 - 161, Remove
the redundant slider.value = X assignments and only call the handler directly
(slider._on_value_changed(X)) in the test; the explicit assignment is a no-op
when you invoke the handler with a value and also hides that HSlider clamps
values when set directly. Update all occurrences that follow this pattern (the
local variable slider and its _on_value_changed calls in the test functions) and
for out-of-range cases keep the test calling _on_value_changed with the intended
unclamped input or explicitly assert the clamped behavior by reading
slider.value after assignment if you want to test clamping.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/gut/test_audio_settings_interaction.gd`:
- Around line 159-161: Remove the redundant slider.value = X assignments and
only call the handler directly (slider._on_value_changed(X)) in the test; the
explicit assignment is a no-op when you invoke the handler with a value and also
hides that HSlider clamps values when set directly. Update all occurrences that
follow this pattern (the local variable slider and its _on_value_changed calls
in the test functions) and for out-of-range cases keep the test calling
_on_value_changed with the intended unclamped input or explicitly assert the
clamped behavior by reading slider.value after assignment if you want to test
clamping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: ff2c2bbf-d6d0-49f6-8d2c-a5a773167cc3

📥 Commits

Reviewing files that changed from the base of the PR and between 3a10df7 and 6654068.

📒 Files selected for processing (4)
  • .gitignore
  • scripts/managers/audio_manager.gd
  • scripts/ui/components/volume_slider.gd
  • test/gut/test_audio_settings_interaction.gd
💤 Files with no reviewable changes (1)
  • scripts/managers/audio_manager.gd
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: GUT Unit Tests / unit-test
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-03-30T04:02:23.747Z
Learnt from: ikostan
Repo: ikostan/SkyLockAssault PR: 500
File: test/gut/test_audio_web_bridge.gd:131-145
Timestamp: 2026-03-30T04:02:23.747Z
Learning: In GUT (Godot Unit Test) for Godot 4, when using `assert_called` / `assert_called_count` with parameter matching, include *every* argument the mocked method accepts, including parameters with default values. GUT does not auto-fill default arguments during call matching. For example, if `JavaScriptBridgeWrapper.eval(script: String, global_exec: bool = false)` is invoked as `eval(js_string)`, the actual call recorded by GUT includes the default (`eval(js_string, false)`), so your assertion must match both arguments (e.g., `.bind(js_string, false)`, not `.bind(js_string)`). Apply this rule to GUT assertions in `test/gut` tests.

Applied to files:

  • test/gut/test_audio_settings_interaction.gd
🔇 Additional comments (13)
scripts/ui/components/volume_slider.gd (1)

135-136: LGTM!

test/gut/test_audio_settings_interaction.gd (12)

14-23: LGTM!


33-35: LGTM!


47-60: LGTM!


69-78: LGTM!


83-89: LGTM!


94-102: LGTM!


105-117: LGTM!


127-144: LGTM!


209-217: LGTM!


222-241: LGTM!


245-256: LGTM!


261-269: LGTM!

ikostan added 5 commits June 11, 2026 16:26
Add a detailed PR summary documenting the new audio UI GUT test suite and related resilience/diagnostic changes (files/docs/milestones/20/Part_1_UI_audio_logic_unit_tests.md). Add the engine UID marker for the test file (test/gut/test_audio_settings_interaction.gd.uid) and update .gitignore to ignore engine-generated *.uid files to reduce noisy diffs and merge conflicts.
@ikostan

ikostan commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

@sourcery-ai guide

@ikostan ikostan merged commit 8e0e67c into main Jun 12, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Sky Lock Assault Project Jun 12, 2026
@ikostan ikostan deleted the ui-audio-logic-unit-tests branch June 12, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[FEATURE] UI Audio Logic Unit Tests (GUT): Navigation Triggering

1 participant