Add unit tests and refactor music service classes

This commit is contained in:
anthorob committed 2026-09-06 16:21:45 -04:00
1 parent b8786c8dc9
commit 1b981abdf7
13 files changed
+1085 -133

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
---
sessionId: session-260905-173932-36te
---
# Requirements
### Overview & Goals
Improve the `MusicManager` class for better maintainability, testability, and separation of concerns.
### Scope
**In Scope:**
- Refactor `MusicManager` to reduce responsibilities
- Extract vote-skip logic into a dedicated service
- Fix synchronization strategy
- Improve error handling
- Eliminate magic numbers
**Out of Scope:**
- Changing `QueueManager` or `TrackScheduler` internals (beyond reducing circular refs)
- Changing external behavior or command interfaces
# Technical Design
### Current Issues
1. **Vote-skip business logic in MusicManager** (lines 113–145): The `voteSkip()` method contains complex voting logic (threshold calculation, member counting, skip decisions) that doesn't belong in a player manager. Magic number `2.1` is used as a vote divisor with no explanation.
2. **Circular dependencies**: `MusicManager` → `TrackScheduler` → `QueueManager` → `TrackScheduler` → `MusicManager`. This makes testing and reasoning about the code difficult.
3. **Overly broad `synchronized` methods**: Nearly every public method is `synchronized` on `this`, even simple getters like `getCurrentlyPlaying()` and `isCurrentlyPlaying()`. This creates unnecessary contention. Methods like `getQueueSize()` aren't synchronized at all, creating inconsistency.
4. **Poor error handling in `connect()`** (line 80): Throws a bare `RuntimeException` when `audioChannel` is null instead of using `IllegalArgumentException` or returning a `CommandExecutionResult`.
5. **`baseVolume` parsed from String without error handling** (line 44): `Integer.parseInt()` can throw `NumberFormatException` with no fallback.
### Proposed Changes
#### 1. Extract `VoteSkipService`
- New class: `me.bobert.netneutrality.music.VoteSkipService`
- Moves all vote-skip logic out of `MusicManager`
- Replace magic `2.1` with a named constant `VOTE_THRESHOLD_DIVISOR` or configurable guild setting
- `MusicManager.voteSkip()` delegates to the new service
#### 2. Reduce circular dependencies
- `TrackScheduler` and `QueueManager` should not need to call back into `MusicManager` for `disconnect()`. Instead, use a callback/listener interface (e.g., `MusicManagerCallback`) that `TrackScheduler` holds, breaking the direct circular reference.
#### 3. Fix synchronization
- Remove `synchronized` from simple delegating methods (`getCurrentlyPlaying()`, `isCurrentlyPlaying()`, `resume()`, `stopTrack()`) — `AudioPlayer` is already thread-safe.
- Keep `synchronized` only on methods that coordinate multiple state changes (`connect()`, `disconnect()`, `play()`).
- Make `getQueueSize()` consistent with the chosen strategy.
#### 4. Improve error handling
- `connect()`: Replace `RuntimeException` with `IllegalArgumentException("audioChannel must not be null")`
- `baseVolume`: Add try-catch with a sensible default (e.g., 50) and a warning log.
#### 5. Extract constants
- `VOTE_THRESHOLD_DIVISOR = 2.1` (or make configurable)
- `MIN_MEMBERS_FOR_VOTE = 3`
- `DEFAULT_VOLUME = 50`
### File Structure
| File | Action |
|------|--------|
| `music/MusicManager.java` | Modify — remove vote logic, fix sync, fix error handling |
| `music/VoteSkipService.java` | **New** — extracted vote-skip logic |
| `music/MusicManagerCallback.java` | **New** — interface to break circular dependency |
| `music/TrackScheduler.java` | Modify — use callback interface instead of direct `MusicManager` reference where possible |
| `music/QueueManager.java` | Modify — remove `getMusicManager()` call chain for disconnect |
# Delivery Steps
### ✓ Step 1: Extract VoteSkipService and constants
Vote-skip logic is isolated in a new service class and magic numbers are replaced with named constants.
- Create `VoteSkipService.java` in `me.bobert.netneutrality.music`
- Move the vote-skip logic from `MusicManager.voteSkip()` into `VoteSkipService.voteSkip(CommandContext, QueueManager, AudioChannel)`
- Define constants: `VOTE_THRESHOLD_DIVISOR`, `MIN_MEMBERS_FOR_VOTE`
- Update `MusicManager.voteSkip()` to delegate to the new service
- Add `DEFAULT_VOLUME` constant and wrap `baseVolume` parsing in try-catch with fallback
### ✓ Step 2: Fix synchronization and error handling
Synchronization is applied only where needed and error handling is improved.
- Remove `synchronized` from simple delegating methods: `getCurrentlyPlaying()`, `isCurrentlyPlaying()`, `resume()`, `stopTrack()`, `setVolume()`
- Keep `synchronized` on `connect()`, `disconnect()`, `play()`, `isNotConnectedTo()`
- Replace `RuntimeException` in `connect()` with `IllegalArgumentException`
- Ensure `getQueueSize()` follows the same synchronization strategy as other methods
### ✓ Step 3: Reduce circular dependencies between MusicManager, TrackScheduler, and QueueManager
The circular reference chain is broken using a callback interface.
- Create `MusicManagerCallback.java` interface with method `void onQueueEmpty()` (or similar) to replace direct `MusicManager.disconnect()` calls from `QueueManager`
- Have `MusicManager` implement the callback interface
- Update `TrackScheduler` constructor to accept the callback interface instead of the full `MusicManager` where only disconnect/connection behavior is needed
- Update `QueueManager.clearQueue()` to use the callback instead of `scheduler.getMusicManager().disconnect()`