Skip to content

Feature/graph undo redo#243

Merged
sudip-mondal-2002 merged 17 commits into
sudip-mondal-2002:mainfrom
MohitBareja16:feature/graph-undo-redo
May 28, 2026
Merged

Feature/graph undo redo#243
sudip-mondal-2002 merged 17 commits into
sudip-mondal-2002:mainfrom
MohitBareja16:feature/graph-undo-redo

Conversation

@MohitBareja16

@MohitBareja16 MohitBareja16 commented May 26, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Implements a comprehensive, highly-robust Undo/Redo system for the 2D pedalboard audio graph. Users can now seamlessly use Ctrl+Z / Ctrl+Shift+Z to reverse and reapply complex canvas interactions including adding/removing nodes, drawing/deleting cables, and dragging nodes across the screen.

Key implementations:

  • Added five specific command objects to the history queue: AddGraphNodeCommand, RemoveGraphNodeCommand, AddGraphLinkCommand, RemoveGraphLinkCommand, and MoveGraphNodeCommand.
  • Updated the core AudioGraph API with restore_node() and restore_link() to ensure ID mappings and severed cables are perfectly reconstructed upon undo operations.
  • Mapped GUI interactions and WASM endpoint triggers directly to the CommandHistory router to capture all UI events.
  • Updated GuiGraphState to track drag_start_pos for precision floating-point coordinate restoration rather than interpolation approximations.

Related Issue

Fixes #150

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • ♻️ Refactor / code cleanup
  • 📝 Documentation
  • ✅ Tests
  • 🔧 Build / CI / Configuration

How Was This Tested?

  • Platform(s) tested on: Linux / WebAssembly
  • Test command run: make build && ./build/amplitron-tests and npx playwright test
  • Manual test steps:
    1. Ran the comprehensive C++ test suite evaluating execute, undo, and redo semantics natively.
    2. Executed Playwright E2E tests simulating automated node generation and cabling via WASM, triggering 5 keystrokes of Control+Z, and mathematically asserting the exact return to the default initial canvas state.

Checklist

  • Code compiles and builds successfully on my platform
  • All existing tests pass (./amplitron-tests)
  • New tests added for new functionality (if applicable)
  • Documentation updated (README, code comments) if applicable
  • No blocking calls on the audio thread (no std::mutex::lock() on the hot path)
  • Tested on: Linux

Screenshots / Demo

Screencast.from.2026-05-26.11-12-42.mp4

Summary by CodeRabbit

Release Notes

  • New Features

    • Added undo/redo support for audio graph operations, including adding and removing nodes, creating and removing connections, and repositioning nodes.
    • Increased default undo history depth from 50 to 100 actions, allowing more editing history to be retained.
  • Tests

    • Added integration tests validating undo/redo behavior for graph operations.
    • Added end-to-end test for graph canvas interactions and undo functionality.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements undo/redo support for audio graph edits (adding/removing nodes and cables, moving nodes) and introduces PortAudio test mocking infrastructure. The command base interface is updated to return bool from execute(), enabling no-op detection. Five new graph command types orchestrate reversible node and cable operations through the GUI command history. PortAudio device backend logic is simplified with inline host-API comparisons, and comprehensive test mocks enable deterministic device and lifecycle scenario testing.

Changes

Graph Undo/Redo and PortAudio Test Infrastructure

Layer / File(s) Summary
Command Base Interface and History Updates
src/gui/commands/command_base.h, src/gui/commands/command_history.cpp, src/gui/commands/command_history.h, src/gui/commands/command.h
Command::execute() now returns bool with a default implementation returning true, allowing no-op commands to signal failure. CommandHistory::execute() checks the return value and skips history recording when execute returns false. DEFAULT_MAX_DEPTH increases from 50 to 100 to support deeper undo stacks.
Existing Commands Updated to Return bool
src/gui/commands/command_chain.h, src/gui/commands/command_clear.h, src/gui/commands/command_param.h, src/gui/commands/command_preset.h, src/gui/commands/command_reorder.h, src/gui/commands/command_reset.h
All existing command types (AddEffectCommand, RemoveEffectCommand, ClearAllCommand, ParameterChangeCommand, LoadPresetCommand, RecallSnapshotCommand, ReorderEffectCommand, ResetAllCommand) now implement execute() returning bool, explicitly returning true after mutation. Necessary audio engine and effect header includes are added across all command files.
Audio Graph Restoration APIs
src/audio/engine/audio_graph.h, src/audio/engine/audio_graph.cpp
Adds restore_node() and restore_link() public methods to AudioGraph that reinstate cached DSP nodes and links, advance next_id_ counters to prevent collisions, and rebuild topology. restore_link() rolls back on failure and retries topology rebuild.
Graph Edit Command Implementations
src/gui/commands/command_graph.h
Implements five reversible command types: AddGraphNodeCommand assigns node ID and caches the DSPNode for redo; RemoveGraphNodeCommand snapshots full node and severed links for undo restoration; AddGraphLinkCommand tracks success and commits only on success; RemoveGraphLinkCommand removes and restores by ID; MoveGraphNodeCommand updates GUI node positions without graph mutations.
GUI State, Manager, and Pedal Board Integration
src/gui/state/gui_graph_state.h, src/gui/gui_manager.h, src/gui/pedalboard/pedal_board_chain.cpp
Adds drag_start_pos to NodeLayoutState for tracking drag interactions. Exposes command_history() accessor on GuiManager. Routes node dragging, cable drops, cable right-click removal, and node deletion through command history instead of direct backend mutations. Executes MoveGraphNodeCommand, AddGraphLinkCommand, RemoveGraphLinkCommand, and RemoveGraphNodeCommand as appropriate.
Menu and Emscripten Trigger Integration
src/gui/pedalboard/pedal_board_menu.cpp, src/main.cpp
Updates menu node creation handlers and Emscripten trigger functions to route graph edits through command history. trigger_add_splitter_node and trigger_add_link now execute graph commands and return result values. trigger_delete_last_node executes RemoveGraphNodeCommand without direct position erasure or graph commits.
PortAudio Mock Test Infrastructure
tests/fixtures/portaudio_mock.h, tests/fixtures/portaudio_mock.cpp
Introduces function-pointer hooks and wrapper entry points for mocking PortAudio device enumeration, default device selection, and stream lifecycle. Wrappers check if Amplitron::g_mock_pa_* pointers are set and dispatch to mock functions or real PortAudio Pa_* calls accordingly.
PortAudio Backend Device Management Updates
src/audio/backend/audio_backend_portaudio_devices.cpp, src/audio/backend/audio_backend_portaudio_internal.h, src/audio/backend/audio_backend_portaudio_lifecycle.cpp
Simplifies host-API mismatch detection to use direct PaDeviceInfo::hostApi comparisons via Pa_GetDeviceInfo and Pa_GetHostApiInfo instead of helper function calls. Includes minor formatting adjustments.
Test Coverage and E2E Validation
CMakeLists.txt, tests/web/amplitron.spec.ts, tests/integration/test_command_graph.cpp, tests/integration/test_audio_backend_portaudio.cpp
Registers new integration test in build configuration. Adds Playwright E2E test exercising add-nodes, draw-links, and five-times undo workflow with baseline assertion. Refactors PortAudio test to use shared mock fixture with per-test MockGuard RAII setup/teardown. Extends test scenarios with PortAudioTestSaboteur covering device enumeration, auto-detection fallbacks, and mocked failure paths.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #150: Undo/redo support for graph edits (add/remove nodes and cables) — This PR fully implements the acceptance criteria: Ctrl+Z/Ctrl+Shift+Z undo/redo for all graph operations, configurable stack depth (now 100), no persistence across restarts, node deletion undoes all connected cables, integration tests for command types, and E2E test scenario with multiple nodes, links, and undo operations.

Possibly related PRs

  • sudip-mondal-2002/Amplitron#132: This PR builds on the modular Splitter/Mixer DAG canvas feature set introduced in PR #132, adding undo/redo command wrappers for the same graph editing operations.
  • sudip-mondal-2002/Amplitron#208: Both PRs modify the same Emscripten/src/main.cpp graph-canvas entry points (trigger_add_splitter_node, trigger_add_link, node deletion) to route web demo graph edits, with this PR further wiring them through the command-history flow.
  • sudip-mondal-2002/Amplitron#22: This PR extends the existing undo/redo command-history framework with new graph-focused command types, updating the shared Command::execute() interface and history success semantics at the code level.

Suggested labels

mentor:MohitBareja16, type:refactor, quality:clean

Suggested reviewers

  • sudip-mondal-2002

Poem

🐰 Hops with glee through undo-redo lanes,
Commands now whisper their bool returns,
Graph edits dance in history's chains,
Each node and cable learns, unlearns, and learns,
While PortAudio mocks test the reins! 🎼

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Feature/graph undo redo' is vague and does not clearly describe the main change; it uses a generic feature branch naming pattern rather than a descriptive single-sentence summary. Consider rephrasing to 'Implement undo/redo for 2D graph operations (add/remove nodes and cables)' to clearly convey what the feature accomplishes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR fully implements all acceptance criteria from issue #150: five command types, undo/redo for all graph operations, configurable stack depth (updated to 100), unit and E2E tests, and proper link restoration on undo.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #150 graph undo/redo feature; no unrelated modifications detected across command objects, audio graph, GUI interactions, or test files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with 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.

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Report 📊

Line Coverage: 81.1%

✅ Coverage meets threshold: 81.1% >= 60%

Full Coverage Summary
Summary coverage rate:
  lines......: 81.1% (6683 of 8239 lines)
  functions..: 92.1% (742 of 806 functions)
  branches...: no data found

@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

PR Preview Removed

The GitHub Pages preview for this PR has been removed because the PR was closed.

@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: 7

Caution

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

⚠️ Outside diff range comments (1)
src/audio/audio_backend_portaudio_lifecycle.cpp (1)

156-164: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard device-info dereferences in auto-detect logging path.

MOCK_OR_REAL_GET_DEVICE_INFO(...) can return nullptr; in_info->name, out_info->name, in_info->defaultSampleRate, and the direct ...->name dereference on Line 188 can crash.

Suggested fix
-            std::cout << "\n>> Auto-selected (same API: " << c.api_name << "):" << std::endl;
-            std::cout << "   INPUT:  " << in_info->name << " [USB Guitar Cable]" << std::endl;
-            std::cout << "   OUTPUT: " << out_info->name << " [Speakers]" << std::endl;
+            std::cout << "\n>> Auto-selected (same API: " << c.api_name << "):" << std::endl;
+            std::cout << "   INPUT:  " << (in_info ? in_info->name : "Unknown") << " [USB Guitar Cable]" << std::endl;
+            std::cout << "   OUTPUT: " << (out_info ? out_info->name : "Unknown") << " [Speakers]" << std::endl;
 
-            if (in_info->defaultSampleRate > 0) {
+            if (in_info && in_info->defaultSampleRate > 0) {
                 sample_rate = static_cast<int>(in_info->defaultSampleRate);
                 std::cout << "   Rate:   " << sample_rate << " Hz (device native)" << std::endl;
             }
...
-                        std::cout << "   OUTPUT: " << MOCK_OR_REAL_GET_DEVICE_INFO(output_device)->name << std::endl;
+                        const PaDeviceInfo* selected_out = MOCK_OR_REAL_GET_DEVICE_INFO(output_device);
+                        std::cout << "   OUTPUT: " << (selected_out ? selected_out->name : "Unknown") << std::endl;

Also applies to: 188-188

🤖 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 `@src/audio/audio_backend_portaudio_lifecycle.cpp` around lines 156 - 164, The
logging and sample-rate assignment dereference pointers returned by
MOCK_OR_REAL_GET_DEVICE_INFO without nullptr checks; guard the in_info and
out_info pointers before accessing their members (in_info->name, out_info->name,
in_info->defaultSampleRate) in the auto-detect path: if either pointer is null,
use a safe fallback string like "<unknown device>" for names and skip or keep
existing sample_rate if defaultSampleRate is unavailable; update the prints and
the sample_rate assignment around the code that references in_info, out_info,
and sample_rate to perform these null checks (symbols:
MOCK_OR_REAL_GET_DEVICE_INFO, in_info, out_info, sample_rate).
🧹 Nitpick comments (4)
tests/test_audio_backend_portaudio.cpp (2)

48-71: ⚡ Quick win

Avoid pass-without-verification lifecycle tests.

These tests currently pass even if start()/restart() fails, which can hide regressions. Make the expected lifecycle outcome explicit (or convert to deterministic mock-based assertions) so failures are observable.

🤖 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 `@tests/test_audio_backend_portaudio.cpp` around lines 48 - 71, The tests
silently allow start()/restart() to fail because they only conditionally check
results; change them to assert the lifecycle calls succeed and produce the
expected state: replace the conditional in
TEST(PortAudioLifecycle_StartStopStartCycle) with ASSERT_TRUE(engine.start())
before checking engine.is_running(), and after stop use
ASSERT_TRUE(engine.start()) and ASSERT_TRUE(engine.is_running()); in
TEST(PortAudioLifecycle_Restart) assert the restart outcome (e.g.,
ASSERT_TRUE(engine.restart()) or ASSERT_TRUE(engine.is_running()) immediately
after engine.restart()), ensuring you still call initialize() and shutdown() and
use AudioEngine::initialize, start, stop, restart, is_running, and shutdown to
locate the code to update.

101-143: ⚡ Quick win

Avoid raw PortAudio global init/terminate in this unit test.

Mixing Pa_Initialize()/Pa_Terminate() with live AudioEngine objects can create test-order coupling on global backend state. Prefer engine-managed lifecycle (or scoped mocks) and ensure engine shutdown completes before any manual termination.

🤖 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 `@tests/test_audio_backend_portaudio.cpp` around lines 101 - 143, This test
currently calls Pa_Initialize() and Pa_Terminate() directly which conflicts with
AudioEngine-managed lifecycle and can cause test-order coupling; remove the raw
PortAudio calls and let AudioEngine manage init/terminate (use
engine_valid.initialize() / engine_valid.shutdown() or create/destroy scoped
AudioEngine instances), ensure any engine_uninit or engine_valid objects are
fully shutdown before ending the test, and replace direct global init/terminate
with engine-managed setup/teardown around the uses of AudioEngine methods
(initialize(), set_input_device(), set_output_device()) so global PortAudio
state is not mutated outside the engine.
tests/test_command_graph.cpp (1)

97-117: ⚡ Quick win

Clean up singleton graph state after this test.

GuiGraphState is global; leaving node_positions[999] populated can leak into other tests. Remove the injected node state at test end to keep tests isolated.

🤖 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 `@tests/test_command_graph.cpp` around lines 97 - 117, The test leaves a global
entry in the GuiGraphState singleton which can affect other tests; after
exercising MoveGraphNodeCommand via history.execute/undo/redo, remove the
injected node state by erasing state.node_positions[node_id] (or reset the
singleton state) so the test cleans up its inserted node (reference symbols:
GuiGraphState::get_instance(), state.node_positions, node_id,
MoveGraphNodeCommand, CommandHistory).
tests/web/amplitron.spec.ts (1)

623-668: ⚡ Quick win

Replace fixed sleeps with condition-based waits.

The repeated waitForTimeout(100) calls can make this scenario flaky on slower CI. Prefer waiting until node/link counts reach expected intermediate values after each action/undo.

🤖 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 `@tests/web/amplitron.spec.ts` around lines 623 - 668, Replace fixed waits
after calling Module functions and after undo keystrokes with condition-based
waits that poll the DOM/Module state until counts match expected values: after
each trigger_add_splitter_node call assert get_node_count increases (use
page.waitForFunction(() => Module.ccall('get_node_count', 'number', [], []) ===
expected))), after each trigger_add_link call assert get_link_count increases,
and during the undo loop wait for node/link counts to decrement to their
expected intermediate values instead of waitForTimeout; reference the Module
functions get_node_count, get_link_count, trigger_add_splitter_node,
trigger_add_link and the undo keystroke (Control+Z) to determine expected
counts.
🤖 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 `@src/audio/audio_backend_portaudio_devices.cpp`:
- Around line 74-76: The mismatch check mixes real and mocked device info;
update the logic so devices_share_host_api uses the same mock-aware access as
the call sites (or replace its usage by comparing the already-fetched
in_api/out_api). Specifically, either modify devices_share_host_api to call
g_mock_pa_get_device_info when g_mock_pa_get_device_info is set (instead of
calling Pa_GetDeviceInfo directly), or stop calling devices_share_host_api and
instead compare hostApi using the in_api and out_api pointers already obtained
in audio_backend_portaudio_devices.cpp (using in_api->hostApi and
out_api->hostApi when out_api is non-null); reference
g_mock_pa_get_host_api_info, g_mock_pa_get_device_info, devices_share_host_api,
in_api, out_api, and Pa_GetDeviceInfo to find and update the code.

In `@src/audio/audio_graph.cpp`:
- Around line 333-337: restore_link currently pushes link into links_ and
updates next_id_ before calling rebuild_topology(), leaving the graph corrupted
if rebuild_topology() fails; change it to perform validation and rollback on
failure by either (a) running the same validation/addition logic used by
add_link (prefer calling add_link or its validator) before mutating internal
state, or (b) make a local copy of links_ and next_id_, apply the push/update to
the copies, call rebuild_topology() on the tentative state, and only commit
copies to links_ and next_id_ if rebuild_topology() succeeds—if
rebuild_topology() fails, restore the originals (or avoid mutating them) and
propagate/handle the error so invalid payloads do not remain in links_.

In `@src/gui/command_graph.h`:
- Around line 103-112: The execute() implementation must signal when it actually
mutated the graph so history can skip no-ops: change the command API so
execute() returns a bool (true when mutation occurred) and update this execute()
(use engine_.graph().add_link(...), set was_successful = (link.id != -1), call
engine_.commit_graph_changes() only when was_successful, then return
was_successful); then update CommandHistory::execute() to call the command's
bool execute() and only queue/clear redo when that call returns true (i.e., skip
recording commands that return false).

In `@src/gui/command_history.h`:
- Line 19: Update the class comment that documents the default command history
depth so it matches the new constant value DEFAULT_MAX_DEPTH (now 100); locate
the class comment above the declaration in command_history.h (referenced near
DEFAULT_MAX_DEPTH) and change the documented default from 50 to 100 or state it
references DEFAULT_MAX_DEPTH to avoid future drift.

In `@src/gui/pedal_board_menu.cpp`:
- Around line 107-111: The menu currently constructs AddGraphNodeCommand with a
fixed ImVec2(40, 60) which writes into GuiGraphState::node_positions and
bypasses the auto-placement logic in render_signal_chain, causing new
Splitter/Mixer nodes to overlap; modify the two MenuItem handlers to call
AddGraphNodeCommand without a fixed position (pass nullptr or use the overload
that omits the position) so the command defers to
GuiGraphState/render_signal_chain auto-placement, e.g. replace the ImVec2(40,
60) argument in the AddGraphNodeCommand calls for "Splitter" and "Mixer" so node
positions are computed instead of hard-coded.

In `@src/main.cpp`:
- Around line 146-152: The functions trigger_add_splitter_node (and the similar
function that adds links) currently read IDs before or by peeking at
graph().get_nodes().back()/get_links().back(), which can be invalid or stale;
instead, after creating the command (e.g. the Amplitron::AddGraphNodeCommand /
AddGraphLinkCommand), call g_gui->command_history().execute(std::move(cmd))
first, then read the ID produced by the command object (use cmd->node_id or
cmd->link_id after execution) and return that, and if execution indicates
failure or the produced ID is invalid return -1; ensure you reference the same
symbols (g_gui, command_history().execute,
Amplitron::AddGraphNodeCommand/AddGraphLinkCommand, node_id/link_id) when making
the change.
- Around line 190-195: The remove path passes ImVec2(0,0) into
RemoveGraphNodeCommand so undo restores the node at top-left and then
immediately erases the stored position; fix by retrieving the node's actual
current position (e.g. use node.position if available or lookup
Amplitron::GuiGraphState::get_instance().node_positions.at(node.id)) and pass
that ImVec2 into the std::make_unique<Amplitron::RemoveGraphNodeCommand>(...)
call, and stop erasing the entry from
GuiGraphState::get_instance().node_positions here so the undo can restore the
original position.

---

Outside diff comments:
In `@src/audio/audio_backend_portaudio_lifecycle.cpp`:
- Around line 156-164: The logging and sample-rate assignment dereference
pointers returned by MOCK_OR_REAL_GET_DEVICE_INFO without nullptr checks; guard
the in_info and out_info pointers before accessing their members (in_info->name,
out_info->name, in_info->defaultSampleRate) in the auto-detect path: if either
pointer is null, use a safe fallback string like "<unknown device>" for names
and skip or keep existing sample_rate if defaultSampleRate is unavailable;
update the prints and the sample_rate assignment around the code that references
in_info, out_info, and sample_rate to perform these null checks (symbols:
MOCK_OR_REAL_GET_DEVICE_INFO, in_info, out_info, sample_rate).

---

Nitpick comments:
In `@tests/test_audio_backend_portaudio.cpp`:
- Around line 48-71: The tests silently allow start()/restart() to fail because
they only conditionally check results; change them to assert the lifecycle calls
succeed and produce the expected state: replace the conditional in
TEST(PortAudioLifecycle_StartStopStartCycle) with ASSERT_TRUE(engine.start())
before checking engine.is_running(), and after stop use
ASSERT_TRUE(engine.start()) and ASSERT_TRUE(engine.is_running()); in
TEST(PortAudioLifecycle_Restart) assert the restart outcome (e.g.,
ASSERT_TRUE(engine.restart()) or ASSERT_TRUE(engine.is_running()) immediately
after engine.restart()), ensuring you still call initialize() and shutdown() and
use AudioEngine::initialize, start, stop, restart, is_running, and shutdown to
locate the code to update.
- Around line 101-143: This test currently calls Pa_Initialize() and
Pa_Terminate() directly which conflicts with AudioEngine-managed lifecycle and
can cause test-order coupling; remove the raw PortAudio calls and let
AudioEngine manage init/terminate (use engine_valid.initialize() /
engine_valid.shutdown() or create/destroy scoped AudioEngine instances), ensure
any engine_uninit or engine_valid objects are fully shutdown before ending the
test, and replace direct global init/terminate with engine-managed
setup/teardown around the uses of AudioEngine methods (initialize(),
set_input_device(), set_output_device()) so global PortAudio state is not
mutated outside the engine.

In `@tests/test_command_graph.cpp`:
- Around line 97-117: The test leaves a global entry in the GuiGraphState
singleton which can affect other tests; after exercising MoveGraphNodeCommand
via history.execute/undo/redo, remove the injected node state by erasing
state.node_positions[node_id] (or reset the singleton state) so the test cleans
up its inserted node (reference symbols: GuiGraphState::get_instance(),
state.node_positions, node_id, MoveGraphNodeCommand, CommandHistory).

In `@tests/web/amplitron.spec.ts`:
- Around line 623-668: Replace fixed waits after calling Module functions and
after undo keystrokes with condition-based waits that poll the DOM/Module state
until counts match expected values: after each trigger_add_splitter_node call
assert get_node_count increases (use page.waitForFunction(() =>
Module.ccall('get_node_count', 'number', [], []) === expected))), after each
trigger_add_link call assert get_link_count increases, and during the undo loop
wait for node/link counts to decrement to their expected intermediate values
instead of waitForTimeout; reference the Module functions get_node_count,
get_link_count, trigger_add_splitter_node, trigger_add_link and the undo
keystroke (Control+Z) to determine expected counts.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfa4adfc-345a-4dba-9392-187899583c4f

📥 Commits

Reviewing files that changed from the base of the PR and between cd648a0 and 40d06ce.

📒 Files selected for processing (19)
  • .gitignore
  • CMakeLists.txt
  • src/audio/audio_backend_portaudio_devices.cpp
  • src/audio/audio_backend_portaudio_internal.h
  • src/audio/audio_backend_portaudio_lifecycle.cpp
  • src/audio/audio_engine.h
  • src/audio/audio_graph.cpp
  • src/audio/audio_graph.h
  • src/gui/command.h
  • src/gui/command_graph.h
  • src/gui/command_history.h
  • src/gui/gui_graph_state.h
  • src/gui/gui_manager.h
  • src/gui/pedal_board_chain.cpp
  • src/gui/pedal_board_menu.cpp
  • src/main.cpp
  • tests/test_audio_backend_portaudio.cpp
  • tests/test_command_graph.cpp
  • tests/web/amplitron.spec.ts

Comment thread src/audio/audio_backend_portaudio_devices.cpp Outdated
Comment thread src/audio/engine/audio_graph.cpp
Comment thread src/gui/command_graph.h Outdated
Comment thread src/gui/commands/command_history.h
Comment thread src/gui/pedal_board_menu.cpp Outdated
Comment thread src/main.cpp Outdated
Comment thread src/main.cpp Outdated
Comment thread src/audio/audio_backend_portaudio_internal.h Outdated
Comment thread src/audio/audio_backend_portaudio_lifecycle.cpp Outdated
@sudip-mondal-2002

Copy link
Copy Markdown
Owner

Please fix the conflicts and follow the latest test folder structures

@github-actions github-actions Bot added Audio Processing Related to DSP, effects, or signal processing DAG Related to DAG-based modular signal routing architecture GUI Related to the ImGui user interface and UX High Impact Significant user-facing improvement level:advanced Advanced task · 45 GSSoC points type:feature New feature or request labels May 26, 2026
@MohitBareja16 MohitBareja16 added type:testing Unit tests, integration tests, or test coverage level:critical quality:exceptional type:design type:devops and removed level:advanced Advanced task · 45 GSSoC points labels May 27, 2026
@github-actions github-actions Bot added the level:advanced Advanced task · 45 GSSoC points label May 28, 2026

@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

Caution

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

⚠️ Outside diff range comments (2)
src/audio/engine/audio_graph.cpp (1)

323-333: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make restore_node transactional (rollback on failed restore).

restore_node permanently mutates nodes_ and next_id_ before validation. If restore input is stale/corrupt, this can leave invalid graph state resident.

Suggested fix
 void AudioGraph::restore_node(const DSPNode& node) {
+  int prev_next_id = next_id_;
   nodes_.push_back(node);
   if (node.id >= next_id_) next_id_ = node.id + 1;
   for (int pin : node.input_pin_ids) {
       if (pin >= next_id_) next_id_ = pin + 1;
   }
   for (int pin : node.output_pin_ids) {
       if (pin >= next_id_) next_id_ = pin + 1;
   }
-  rebuild_topology();
+  if (!rebuild_topology()) {
+      nodes_.pop_back();
+      next_id_ = prev_next_id;
+      rebuild_topology();
+  }
 }
🤖 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 `@src/audio/engine/audio_graph.cpp` around lines 323 - 333, The restore_node
function mutates nodes_ and next_id_ before validating the node and calling
rebuild_topology, risking corrupt state on failure; change it to be
transactional by applying the insert to local temporaries (e.g., a temp vector
copy of nodes_ and a temp_next_id computed from node.id and
node.input_pin_ids/output_pin_ids), run the validation/rebuild_topology logic
against the temp state (or call a non-mutating validate_topology(temp_nodes)
helper), and only on success swap the temporaries into nodes_ and next_id_; on
failure discard temporaries and return/error so nodes_ and next_id_ remain
unchanged.
src/gui/pedalboard/pedal_board_chain.cpp (1)

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

Remove debug printf statement.

This debug logging should be removed before merge.

-                    printf("MANUAL DROP HOVER DETECTED! src: %d, dest: %d\n", ui_state.active_src_pin_id, pin_id);
🤖 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 `@src/gui/pedalboard/pedal_board_chain.cpp` at line 357, Remove the debug
printf call that prints "MANUAL DROP HOVER DETECTED!"—locate the printf("MANUAL
DROP HOVER DETECTED! src: %d, dest: %d\n", ui_state.active_src_pin_id, pin_id)
in pedal_board_chain.cpp (near the manual drop/hover handling code) and delete
that line; if you need to retain a trace, replace it with the project's logging
facility or a conditional debug macro, otherwise simply remove the printf
invocation.
♻️ Duplicate comments (1)
src/audio/engine/audio_graph.cpp (1)

335-343: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

restore_link still bypasses link validity checks.

This still restores links without verifying both pins exist and the edge is not already present. rebuild_topology() rollback only protects cycle cases, not orphan/duplicate payloads.

🤖 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 `@src/audio/engine/audio_graph.cpp` around lines 335 - 343, The restore_link
function currently appends to links_ and updates next_id_ without checking link
validity; before mutating links_ or next_id_ in restore_link(const GraphLink&
link) first verify both endpoint pins exist (use your pin lookup/exists helpers)
and that no existing link in links_ has the same source/target (or identical id)
to avoid duplicates/orphans; only if those checks pass then push_back(link),
adjust next_id_ as you already do, call rebuild_topology(), and keep the
existing rollback logic if rebuild_topology() fails—otherwise return immediately
without modifying links_ or next_id_.
🧹 Nitpick comments (4)
src/gui/commands/command_history.cpp (1)

46-56: 💤 Low value

Consider checking execute() return value during redo for consistency.

In redo(), if cmd->execute() returns false (indicating no mutation occurred), the command is still pushed onto the undo stack. This creates a minor inconsistency: execute() at line 7 skips recording for no-ops, but redo doesn't apply the same logic.

In practice this is unlikely to cause issues since redo replays a previously successful command, but for defensive consistency you could check the return value.

♻️ Optional: Check return value in redo()
 bool CommandHistory::redo() {
     if (redo_stack_.empty()) return false;

     auto cmd = std::move(redo_stack_.back());
     redo_stack_.pop_back();

-    cmd->execute();
-    undo_stack_.push_back(std::move(cmd));
+    if (cmd->execute()) {
+        undo_stack_.push_back(std::move(cmd));
+    }
     return true;
 }
🤖 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 `@src/gui/commands/command_history.cpp` around lines 46 - 56,
CommandHistory::redo currently calls cmd->execute() and unconditionally moves
the command from redo_stack_ to undo_stack_; change it to check the bool return
of cmd->execute() and only push the command onto undo_stack_ when execute()
returns true (mirror the behavior used elsewhere when execute() indicates a
no-op). Locate the redo method, evaluate the result of cmd->execute(), and
conditionally push_back(std::move(cmd)) into undo_stack_ only on success; ensure
the method still returns true/false appropriately when execute() fails.
src/gui/commands/command_graph.h (1)

14-20: ⚡ Quick win

Align struct member naming with repo convention.

Member fields in these command structs should use trailing underscores for consistency with project C++ style.

As per coding guidelines "Class Member Variables should use lowercase snake_case with a trailing underscore (e.g., current_port_, midi_queue_)".

Also applies to: 55-60, 99-101, 135-136, 155-157

🤖 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 `@src/gui/commands/command_graph.h` around lines 14 - 20, Rename the struct
member fields to follow the project's trailing-underscore convention: change
node_id, name, type, pedal, position, and cached_node to node_id_, name_, type_,
pedal_, position_, and cached_node_ in the command struct shown (symbols to
update: node_id, name, type, pedal, position, cached_node and the
already-correct engine_). Do the same renaming for the other command structs
referenced (the members in the blocks around lines 55-60, 99-101, 135-136,
155-157) and update all local and external references/usages in this header and
any implementation files so builds compile with the new identifiers. Ensure
symbol names in constructors, execute/undo methods, and any initializer lists
are updated accordingly.
src/gui/pedalboard/pedal_board_chain.cpp (1)

187-189: ⚡ Quick win

Use epsilon comparison for floating-point position equality.

Comparing floats with != is unreliable due to floating-point precision issues. Small rounding errors during drag could cause false negatives (command not recorded) or false positives (redundant commands).

-                if (node_layout.position.x != node_layout.drag_start_pos.x || node_layout.position.y != node_layout.drag_start_pos.y) {
+                constexpr float EPSILON = 0.01f;
+                if (std::abs(node_layout.position.x - node_layout.drag_start_pos.x) > EPSILON ||
+                    std::abs(node_layout.position.y - node_layout.drag_start_pos.y) > EPSILON) {

Apply the same fix at line 208.

Also applies to: 208-210

🤖 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 `@src/gui/pedalboard/pedal_board_chain.cpp` around lines 187 - 189, The float
equality check on node_layout.position vs node_layout.drag_start_pos before
pushing a MoveGraphNodeCommand is fragile; change the condition used where
history_.push_executed(std::make_unique<MoveGraphNodeCommand>(...)) is called to
use an epsilon comparison (e.g. fabs(position.x - drag_start_pos.x) > EPS ||
fabs(position.y - drag_start_pos.y) > EPS) instead of != so tiny rounding
differences don't produce incorrect command records; make the identical change
for the other occurrence that also constructs/pushes a MoveGraphNodeCommand in
this file.
src/main.cpp (1)

146-161: ⚡ Quick win

Raw pointer after std::move relies on CommandHistory implementation details.

The pattern of keeping raw after std::move(cmd) works only because CommandHistory::execute() stores the unique_ptr without destroying it. This is fragile and could break if the history implementation changes.

Consider using push_executed() pattern (as done in pedal_board_chain.cpp line 188) which explicitly separates execution from history recording:

♻️ Safer alternative
 extern "C" EMSCRIPTEN_KEEPALIVE int trigger_add_splitter_node() {
     if (!g_gui) return -1;
     auto cmd = std::make_unique<Amplitron::AddGraphNodeCommand>(
         g_gui->audio_engine(), "Splitter", Amplitron::NodeRoutingType::Splitter, nullptr, ImVec2(0, 0));
-    auto* raw = cmd.get();
-    g_gui->command_history().execute(std::move(cmd));
-    return (raw->node_id != -1) ? raw->node_id : -1;
+    if (!cmd->execute()) return -1;
+    int id = cmd->node_id;
+    g_gui->command_history().push_executed(std::move(cmd));
+    return id;
 }
 
 extern "C" EMSCRIPTEN_KEEPALIVE int trigger_add_link(int src_pin, int dst_pin) {
     if (!g_gui) return -1;
     auto cmd = std::make_unique<Amplitron::AddGraphLinkCommand>(g_gui->audio_engine(), src_pin, dst_pin);
-    auto* raw = cmd.get();
-    g_gui->command_history().execute(std::move(cmd));
-    return raw->was_successful ? raw->link.id : -1;
+    if (!cmd->execute()) return -1;
+    int id = cmd->was_successful ? cmd->link.id : -1;
+    if (cmd->was_successful) {
+        g_gui->command_history().push_executed(std::move(cmd));
+    }
+    return id;
 }
🤖 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 `@src/main.cpp` around lines 146 - 161, The code captures a raw pointer from
cmd before calling command_history().execute(std::move(cmd)), which is fragile;
instead create the command (AddGraphNodeCommand / AddGraphLinkCommand), hand it
to the history via the safer push_executed pattern used in pedal_board_chain.cpp
(i.e. call g_gui->command_history().push_executed(std::move(cmd)) and use the
pointer/handle returned by push_executed to read node_id / was_successful /
link.id), or alternatively call cmd->execute() before moving it into history so
you don't dereference a moved-from pointer; update trigger_add_splitter_node and
trigger_add_link to follow one of these approaches and stop relying on raw after
std::move.
🤖 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 `@src/gui/commands/command_graph.h`:
- Around line 162-168: The execute() method on the move command currently always
returns true, which records no-op moves when node_id is missing or the position
hasn't changed; change execute() (the override in the command class) to first
look up the node in GuiGraphState::get_instance().node_positions (use find to
avoid double lookup), return false if node_id is not present, compare the
existing position to new_pos and only assign positions[node_id].position =
new_pos and return true when they differ, otherwise return false so no-op moves
are not added to undo history.
- Around line 64-83: The execute() implementation must detect when the node
wasn't found and return false instead of proceeding; modify the function so that
after auto* node_to_remove = engine_.graph().find_node(node_id) you immediately
check if node_to_remove is null and return false (do not copy into cached_node
or populate severed_links), otherwise continue to assign cached_node =
*node_to_remove, compute severed_links, call
engine_.graph().remove_node(node_id), erase
GuiGraphState::get_instance().node_positions[node_id], and commit; this ensures
undo() only operates when a real removal occurred.
- Around line 140-144: The execute() method currently always returns true;
change it to call engine_.graph().remove_link(link.id), capture its boolean
result, and only call engine_.commit_graph_changes() when removal succeeded,
then return the actual remove_link result so no-op deletions are not recorded;
refer to the execute() method, engine_.graph().remove_link(link.id),
engine_.commit_graph_changes(), and link.id when making this change.
- Around line 110-121: The execute() path currently treats any non -1 id from
engine_.graph().add_link(...) as a success even when add_link returned an
existing link id; change execute() (and undo/restore logic) so the command only
records success for links actually created by this command: before calling
add_link or after its return, detect whether the link already existed (e.g.,
call a new or existing engine_.graph().has_link(link.source_pin_id,
link.dest_pin_id) or have add_link return a created flag) and set was_successful
(or a new created_by_command boolean) to true only when a new link is created;
ensure engine_.graph().restore_link(link) and undo/remove logic only act when
created_by_command is true and still call engine_.commit_graph_changes() only
when the command actually created or restored a link.

In `@src/gui/commands/command_preset.h`:
- Around line 6-8: Remove the duplicate include of <vector> in the header; in
src/gui/commands/command_preset.h only one `#include` <vector> should remain
(remove the repeated line so the includes are unique alongside <utility>).
Ensure the file contains a single `#include` <vector> and still keeps `#include`
<utility>.

---

Outside diff comments:
In `@src/audio/engine/audio_graph.cpp`:
- Around line 323-333: The restore_node function mutates nodes_ and next_id_
before validating the node and calling rebuild_topology, risking corrupt state
on failure; change it to be transactional by applying the insert to local
temporaries (e.g., a temp vector copy of nodes_ and a temp_next_id computed from
node.id and node.input_pin_ids/output_pin_ids), run the
validation/rebuild_topology logic against the temp state (or call a non-mutating
validate_topology(temp_nodes) helper), and only on success swap the temporaries
into nodes_ and next_id_; on failure discard temporaries and return/error so
nodes_ and next_id_ remain unchanged.

In `@src/gui/pedalboard/pedal_board_chain.cpp`:
- Line 357: Remove the debug printf call that prints "MANUAL DROP HOVER
DETECTED!"—locate the printf("MANUAL DROP HOVER DETECTED! src: %d, dest: %d\n",
ui_state.active_src_pin_id, pin_id) in pedal_board_chain.cpp (near the manual
drop/hover handling code) and delete that line; if you need to retain a trace,
replace it with the project's logging facility or a conditional debug macro,
otherwise simply remove the printf invocation.

---

Duplicate comments:
In `@src/audio/engine/audio_graph.cpp`:
- Around line 335-343: The restore_link function currently appends to links_ and
updates next_id_ without checking link validity; before mutating links_ or
next_id_ in restore_link(const GraphLink& link) first verify both endpoint pins
exist (use your pin lookup/exists helpers) and that no existing link in links_
has the same source/target (or identical id) to avoid duplicates/orphans; only
if those checks pass then push_back(link), adjust next_id_ as you already do,
call rebuild_topology(), and keep the existing rollback logic if
rebuild_topology() fails—otherwise return immediately without modifying links_
or next_id_.

---

Nitpick comments:
In `@src/gui/commands/command_graph.h`:
- Around line 14-20: Rename the struct member fields to follow the project's
trailing-underscore convention: change node_id, name, type, pedal, position, and
cached_node to node_id_, name_, type_, pedal_, position_, and cached_node_ in
the command struct shown (symbols to update: node_id, name, type, pedal,
position, cached_node and the already-correct engine_). Do the same renaming for
the other command structs referenced (the members in the blocks around lines
55-60, 99-101, 135-136, 155-157) and update all local and external
references/usages in this header and any implementation files so builds compile
with the new identifiers. Ensure symbol names in constructors, execute/undo
methods, and any initializer lists are updated accordingly.

In `@src/gui/commands/command_history.cpp`:
- Around line 46-56: CommandHistory::redo currently calls cmd->execute() and
unconditionally moves the command from redo_stack_ to undo_stack_; change it to
check the bool return of cmd->execute() and only push the command onto
undo_stack_ when execute() returns true (mirror the behavior used elsewhere when
execute() indicates a no-op). Locate the redo method, evaluate the result of
cmd->execute(), and conditionally push_back(std::move(cmd)) into undo_stack_
only on success; ensure the method still returns true/false appropriately when
execute() fails.

In `@src/gui/pedalboard/pedal_board_chain.cpp`:
- Around line 187-189: The float equality check on node_layout.position vs
node_layout.drag_start_pos before pushing a MoveGraphNodeCommand is fragile;
change the condition used where
history_.push_executed(std::make_unique<MoveGraphNodeCommand>(...)) is called to
use an epsilon comparison (e.g. fabs(position.x - drag_start_pos.x) > EPS ||
fabs(position.y - drag_start_pos.y) > EPS) instead of != so tiny rounding
differences don't produce incorrect command records; make the identical change
for the other occurrence that also constructs/pushes a MoveGraphNodeCommand in
this file.

In `@src/main.cpp`:
- Around line 146-161: The code captures a raw pointer from cmd before calling
command_history().execute(std::move(cmd)), which is fragile; instead create the
command (AddGraphNodeCommand / AddGraphLinkCommand), hand it to the history via
the safer push_executed pattern used in pedal_board_chain.cpp (i.e. call
g_gui->command_history().push_executed(std::move(cmd)) and use the
pointer/handle returned by push_executed to read node_id / was_successful /
link.id), or alternatively call cmd->execute() before moving it into history so
you don't dereference a moved-from pointer; update trigger_add_splitter_node and
trigger_add_link to follow one of these approaches and stop relying on raw after
std::move.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7257bf33-8f8b-4573-8056-accbb31a8dea

📥 Commits

Reviewing files that changed from the base of the PR and between 634725f and 8f9b835.

📒 Files selected for processing (25)
  • CMakeLists.txt
  • src/audio/backend/audio_backend_portaudio_devices.cpp
  • src/audio/backend/audio_backend_portaudio_internal.h
  • src/audio/backend/audio_backend_portaudio_lifecycle.cpp
  • src/audio/engine/audio_graph.cpp
  • src/audio/engine/audio_graph.h
  • src/gui/commands/command.h
  • src/gui/commands/command_base.h
  • src/gui/commands/command_chain.h
  • src/gui/commands/command_clear.h
  • src/gui/commands/command_graph.h
  • src/gui/commands/command_history.cpp
  • src/gui/commands/command_history.h
  • src/gui/commands/command_param.h
  • src/gui/commands/command_preset.h
  • src/gui/commands/command_reorder.h
  • src/gui/commands/command_reset.h
  • src/gui/gui_manager.h
  • src/gui/pedalboard/pedal_board_chain.cpp
  • src/gui/pedalboard/pedal_board_menu.cpp
  • src/gui/state/gui_graph_state.h
  • src/main.cpp
  • tests/fixtures/portaudio_mock.cpp
  • tests/fixtures/portaudio_mock.h
  • tests/integration/test_audio_backend_portaudio.cpp
💤 Files with no reviewable changes (3)
  • src/gui/commands/command_base.h
  • src/audio/engine/audio_graph.h
  • src/gui/state/gui_graph_state.h
✅ Files skipped from review due to trivial changes (2)
  • src/gui/commands/command.h
  • src/audio/backend/audio_backend_portaudio_lifecycle.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • CMakeLists.txt
  • src/gui/gui_manager.h

Comment thread src/gui/commands/command_graph.h
Comment thread src/gui/commands/command_graph.h
Comment thread src/gui/commands/command_graph.h
Comment thread src/gui/commands/command_graph.h
Comment thread src/gui/commands/command_preset.h 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.

Caution

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

⚠️ Outside diff range comments (1)
tests/integration/test_command_graph.cpp (1)

97-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add teardown for GuiGraphState singleton mutation.

GuiGraphState::get_instance() is shared process-wide, and Line 102 inserts node_id 999 without cleanup. This leaks state across tests and can cause order-dependent failures.

Suggested fix
 TEST(MoveGraphNodeCommand_executes_and_undoes_correctly) {
     CommandHistory history(100);
     auto& state = GuiGraphState::get_instance();
@@
     history.redo();
     ASSERT_EQ(50.0f, state.node_positions[node_id].position.x);
     ASSERT_EQ(50.0f, state.node_positions[node_id].position.y);
+
+    // teardown: avoid leaking singleton state into other tests
+    state.node_positions.erase(node_id);
 }
🤖 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 `@tests/integration/test_command_graph.cpp` around lines 97 - 117, The test
MoveGraphNodeCommand_executes_and_undoes_correctly mutates the process-wide
singleton GuiGraphState (via GuiGraphState::get_instance() and its
node_positions map) by inserting node_id 999 and never cleaning it up; remove or
reset that mutation in a teardown step (e.g., erase node_id 999 from
state.node_positions or restore the singleton to its prior state) at the end of
the test (or use an RAII/scope-guard) so the shared state does not leak into
other tests.
🤖 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 `@tests/integration/test_command_graph.cpp`:
- Around line 97-117: The test
MoveGraphNodeCommand_executes_and_undoes_correctly mutates the process-wide
singleton GuiGraphState (via GuiGraphState::get_instance() and its
node_positions map) by inserting node_id 999 and never cleaning it up; remove or
reset that mutation in a teardown step (e.g., erase node_id 999 from
state.node_positions or restore the singleton to its prior state) at the end of
the test (or use an RAII/scope-guard) so the shared state does not leak into
other tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 416d9a59-f9bf-4c57-a8c3-6d6a7e218848

📥 Commits

Reviewing files that changed from the base of the PR and between 8f9b835 and 36c2023.

📒 Files selected for processing (2)
  • src/main.cpp
  • tests/integration/test_command_graph.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main.cpp

@sudip-mondal-2002 sudip-mondal-2002 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM but test coverage didn't increase from base

@MohitBareja16

Copy link
Copy Markdown
Collaborator Author

@sudip-mondal-2002 I will try to increase the test coverage

@MohitBareja16

Copy link
Copy Markdown
Collaborator Author

@sudip-mondal-2002 Increased test coverage, Check it again

@sudip-mondal-2002 sudip-mondal-2002 merged commit ff3e639 into sudip-mondal-2002:main May 28, 2026
9 checks passed
github-actions Bot added a commit that referenced this pull request May 28, 2026
@sudip-mondal-2002 sudip-mondal-2002 added the gssoc:approved GSSoC 2026 contribution label May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Audio Processing Related to DSP, effects, or signal processing DAG Related to DAG-based modular signal routing architecture gssoc:approved GSSoC 2026 contribution GUI Related to the ImGui user interface and UX High Impact Significant user-facing improvement level:advanced Advanced task · 45 GSSoC points level:critical mentor:sudip-mondal-2002 quality:exceptional type:design type:devops type:feature New feature or request type:testing Unit tests, integration tests, or test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Undo/redo support for graph edits (add/remove nodes and cables)

2 participants