Test Plan: UX & API Verification
Scope
Test every block-related API endpoint and the UI's interaction with it. Covers both the React SPA at pedal.local and the REST/WebSocket APIs.
1. API Health & Baseline
| # |
Test |
Method |
Path |
Expected |
Result |
| 1.1 |
State endpoint |
GET |
/api/state |
200 — returns current preset, bypass, volume, routing |
|
| 1.2 |
Preset list |
GET |
/api/presets |
200 — Bank 0 with 4 slots (2 presets, 2 null) |
|
| 1.3 |
Models list |
GET |
/api/models |
200 — both .nam files visible, loaded states |
|
| 1.4 |
Profiles |
GET |
/api/audio/profiles |
200 — standard + low listed |
|
| 1.5 |
Profile detail |
GET |
/api/audio/profile |
200 — period=256, jack_running=true |
|
2. Block Toggle (PATCH /api/blocks)
Core bug: previously enabled changed but bypass stayed true, so blocks remained skipped.
| # |
Test |
Payload |
Expected |
Result |
| 2.1 |
Enable a disabled block |
{"id": "nam_amp", "enabled": true} |
bypass: false returned |
|
| 2.2 |
Disable an enabled block |
{"id": "nam_amp", "enabled": false} |
bypass: true returned |
|
| 2.3 |
Toggle enabled→disabled |
GET state before & after |
Chain shows block with enabled=false, bypass=true |
|
| 2.4 |
Toggle disabled→enabled |
GET state before & after |
Chain shows block with enabled=true, bypass=false |
|
| 2.5 |
Toggle nonexistent block ID |
{"id": "phaser", "enabled": true} |
404 — "Block 'phaser' not found" |
|
| 2.6 |
Missing ID field |
{"enabled": true} |
400 — "Missing 'id' or 'enabled'" |
|
| 2.7 |
Missing enabled field |
{"id": "nam_amp"} |
400 — "Missing 'id' or 'enabled'" |
|
| 2.8 |
Toggle and confirm audio |
Toggle off → play guitar → toggle on → play guitar |
Audio is muted when bypassed, flows when not |
|
3. Block Param Update (PATCH /api/block)
| # |
Test |
Payload |
Expected |
Result |
| 3.1 |
Update volume param |
{"id": "nam_amp", "level": 0.5} |
params.level == 0.5 returned |
|
| 3.2 |
Update with max value |
{"id": "nam_amp", "level": 1.0} |
Accepted, no clipping |
|
| 3.3 |
Update with zero value |
{"id": "nam_amp", "level": 0.0} |
Accepted (mute) |
|
| 3.4 |
Update nonexistent block |
{"id": "flanger", "rate": 0.5} |
404 |
|
| 3.5 |
Update multiple params |
{"id": "nam_amp", "level": 0.7, "some_custom": 0.3} |
Both params stored |
|
| 3.6 |
Readback after save |
GET preset → confirm params persisted |
Matches last write |
|
4. Preset CRUD
| # |
Test |
Method |
Path/Payload |
Expected |
Result |
| 4.1 |
Get preset 0 |
GET |
/api/presets/0/0 |
200 — Fender Deluxe Reverb Clean |
|
| 4.2 |
Get preset 1 |
GET |
/api/presets/0/1 |
200 — Marshall JCM 900 Dirty |
|
| 4.3 |
Get empty slot |
GET |
/api/presets/0/2 |
404 — "Preset not found" |
|
| 4.4 |
Activate preset 0 |
POST |
/api/presets/0/0/activate |
200, preset == "Fender Deluxe Reverb Clean" |
|
| 4.5 |
Activate preset 1 |
POST |
/api/presets/0/1/activate |
200, preset == "Marshall JCM 900 Dirty" |
|
| 4.6 |
Create preset in slot 2 |
PUT |
/api/presets/0/2 with nam_amp chain |
200 |
|
| 4.7 |
Delete preset in slot 2 |
DELETE |
/api/presets/0/2 |
200 — slot returns to null |
|
| 4.8 |
Save preset with IR + NAM |
PUT |
block with ir_file_path + nam_model_path |
Both saved |
|
| 4.9 |
Cross-channel isolation |
GET |
/api/presets?channel=bass |
Returns bass bank, not guitar |
|
5. Audio Profile Switching
| # |
Test |
Payload |
Expected |
Result |
| 5.1 |
Switch to low latency |
{"profile": "low"} |
200, period: 64, restarted: true |
|
| 5.2 |
Verify JACK restarted |
GET /api/audio/profile |
period == 64, jack_running == true |
|
| 5.3 |
Switch back to standard |
{"profile": "standard"} |
200, period: 256, restarted: true |
|
| 5.4 |
Verify JACK restarted |
GET /api/audio/profile |
period == 256, jack_running == true |
|
| 5.5 |
Audio still flows after switch |
Play guitar |
No crackles, no silence |
|
| 5.6 |
Switch to same profile |
{"profile": "standard"} |
restarted: false |
|
| 5.7 |
Invalid profile |
{"profile": "extreme"} |
400 — unknown profile |
|
| 5.8 |
Xrun count not null |
GET /api/audio/profile |
xrun_count is int or null (not error) |
|
6. Volume & Bypass (Pipeline level)
| # |
Test |
Method/Payload |
Expected |
Result |
| 6.1 |
Set volume |
PUT /api/volume {"volume": 0.5} |
volume: 0.5 returned |
|
| 6.2 |
Volume clamping |
PUT /api/volume {"volume": 2.0} |
volume: 1.0 (clamped) |
|
| 6.3 |
Volume clamping low |
PUT /api/volume {"volume": -1.0} |
volume: 0.0 (clamped) |
|
| 6.4 |
Bypass toggle |
POST /api/bypass {"bypass": true} |
bypass: true |
|
| 6.5 |
Bypass toggle off |
POST /api/bypass {"bypass": false} |
bypass: false |
|
| 6.6 |
Bypass flip |
POST /api/bypass (no body) |
bypass toggles from current state |
|
| 6.7 |
Bypass + audio |
Toggle bypass ON → play guitar |
Bypassed: clean DI signal, no FX |
|
| 6.8 |
Bypass + audio |
Toggle bypass OFF → play guitar |
Active: NAM model processing audible |
|
| 6.9 |
POST alias at /api/bypass/toggle |
POST /api/bypass/toggle {"bypass": true} |
Same as 6.4 |
|
7. WebSocket Real-Time Updates
| # |
Test |
Action |
Expected |
Result |
| 7.1 |
Connect WS |
wscat -c ws://pedal.local/ws |
Receives {"type": "connected", "state": {...}} |
|
| 7.2 |
Ping/pong |
Send {"type": "ping"} |
Response {"type": "pong"} |
|
| 7.3 |
Block toggle broadcast |
Toggle block via PATCH /api/blocks |
WS message {"type": "block_toggled", ...} |
|
| 7.4 |
Preset change broadcast |
Activate preset via API |
WS message {"type": "preset_changed", ...} |
|
| 7.5 |
Bypass broadcast |
Toggle bypass |
WS message {"type": "bypass_changed", ...} |
|
| 7.6 |
Volume change broadcast |
Set volume |
WS message {"type": "volume_changed", ...} |
|
| 7.7 |
Audio profile broadcast |
Switch profile |
WS message {"type": "audio_profile_changed", ...} |
|
8. UI Visual Block Tests (Browser)
| # |
Test |
Action |
Expected |
Result |
| 8.1 |
Page loads |
Navigate to http://pedal.local/ |
React SPA renders, block chain visible |
|
| 8.2 |
NAM block visible |
Check the block chain |
"nam_amp" tile shown with correct icon |
|
| 8.3 |
Block label |
Read tile text |
Shows "nam_amp" or friendly name |
|
| 8.4 |
Toggle block ON/OFF |
Click the block's power icon |
Block appears enabled/disabled visually |
|
| 8.5 |
State persists on refresh |
Toggle OFF → F5 → check |
Block still shows OFF |
|
| 8.6 |
Reverb/delay blocks render |
Check all FX types |
All known FX types have unique icons |
|
| 8.7 |
Empty slot display |
Preset with empty chain |
Shows "no blocks" or empty chain area |
|
| 8.8 |
Block reorder (if supported) |
Drag block |
Chain updates after drop |
|
| 8.9 |
URL root works |
GET / |
Returns React SPA (not 404 or redirect) |
|
9. Edge Cases & Robustness
| # |
Test |
Action |
Expected |
Result |
| 9.1 |
Missing preset directory |
Delete ~/.pedal/presets, restart |
Pedal boots, creates one empty stub |
|
| 9.2 |
Missing models directory |
Remove .nam files |
/api/models returns [], preset fails gracefully |
|
| 9.3 |
Corrupt preset JSON |
Inject bad JSON into preset_0.json |
API returns 500, pedal continues running |
|
| 9.4 |
Web server restart |
systemctl restart pi-multifx-pedal |
30s max downtime, then all endpoints respond |
|
| 9.5 |
Concurrent API calls |
Fire 10 rapid toggle requests |
All return 200, final state is deterministic |
|
| 9.6 |
JACK not running |
Kill jackd mid-session |
Audio stops, API still responds, JACK restarts on next profile switch |
|
| 9.7 |
Rate-limited toggle |
Toggle block on/off 50 times |
No memory leak, state remains consistent |
|
| 9.8 |
SD card at 95%+ |
Fill disk to near-capacity |
API still responds (read-only ops work) |
|
10. Channel Isolation (if applicable)
| # |
Test |
Action |
Expected |
Result |
| 10.1 |
Guitar state doesn't affect bass |
Set guitar volume 0.0, check bass |
Bass volume unchanged |
|
| 10.2 |
Dual-mono routing |
POST /api/channel-mode {"channel_mode": "dual-mono"} |
200, mode switched |
|
| 10.3 |
Stereo mode |
POST /api/channel-mode {"channel_mode": "stereo"} |
200, unified state |
|
Test Execution
Run in order. Mark each cell:
- ✅ — passes
- ❌ — fails (log issue + fix before moving on)
- ⚠️ — non-critical/optional (label reason)
- ⏭️ — skipped (label why)
For curl tests, use this template:
For WS tests: