Files
yellowjacket/.planning/phases/04-queue-config-player-tests/04-01-PLAN.md
T

277 lines
13 KiB
Markdown

---
phase: 04-queue-config-player-tests
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/queue/queue_test.go
- backend/queue/navigation_test.go
- backend/queue/persistence_test.go
autonomous: true
requirements: [TEST-02]
must_haves:
truths:
- "Queue navigation (Next/Previous) works correctly in all repeat modes (off, one, all) for both normal and shuffle playback"
- "Queue mutations (Add, Insert, Move, Remove) correctly update tracks and adjust currentIndex"
- "Queue state persists across SaveState/RestoreState cycles without data loss"
- "Shuffle order contains all indices, has current track at position 0, and has no duplicates"
- "All queue tests pass with -race flag"
artifacts:
- path: "backend/queue/queue_test.go"
provides: "Tests for SetQueue, Add/Insert/Move/Remove, ToggleShuffle, CycleRepeat, Clear, mock TrackLoader"
min_lines: 200
- path: "backend/queue/navigation_test.go"
provides: "Tests for Next/Previous in all modes, edge cases (empty, single, boundary)"
min_lines: 150
- path: "backend/queue/persistence_test.go"
provides: "Tests for SaveState/RestoreState roundtrip fidelity"
min_lines: 100
key_links:
- from: "backend/queue/queue_test.go"
to: "backend/database/testhelper.go"
via: "database.NewTestDB(t)"
pattern: "database\\.NewTestDB"
- from: "backend/queue/persistence_test.go"
to: "backend/queue/persistence.go"
via: "SaveState/RestoreState roundtrip"
pattern: "SaveState|RestoreState"
---
<objective>
Write comprehensive unit tests for the queue package covering core operations, navigation logic, and state persistence.
Purpose: Queue tests are the highest-priority safety net — Phase 7 (PERF-01) will change queue persistence from full table rewrite to incremental INSERT/DELETE. These tests must catch any data loss or index corruption during that refactoring.
Output: 3 test files with ~15-20 tests covering SetQueue, Next, Previous, shuffle, repeat modes, Add/Insert/Move/Remove, and full persistence round-trip.
</objective>
<execution_context>
@/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md
@/home/caleb/.config/opencode/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-test-infrastructure/03-01-SUMMARY.md
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From backend/queue/queue.go:
```go
type RepeatMode string
const (
RepeatOff RepeatMode = "off"
RepeatAll RepeatMode = "all"
RepeatOne RepeatMode = "one"
)
type Track struct {
ID int64 `json:"id"`
AudioFileID int64 `json:"audioFileId"`
FilePath string `json:"filePath"`
Position int64 `json:"position"`
Title string `json:"title"`
Artist string `json:"artist"`
}
type State struct {
Tracks []Track `json:"tracks"`
CurrentIndex int `json:"currentIndex"`
ShuffleMode bool `json:"shuffleMode"`
RepeatMode RepeatMode `json:"repeatMode"`
SourcePlaylistID int64 `json:"sourcePlaylistId"`
}
type TrackLoader interface {
LoadFile(filePath string) error
Play() error
IsPlaying() bool
CurrentPositionSeconds() (int, error)
UnloadTrack()
}
// Queue struct (unexported fields — accessible from same package tests):
type Queue struct {
ctx context.Context
logger *slog.Logger
db *database.DB
player TrackLoader
mu sync.Mutex
tracks []Track
currentIndex int
shuffleMode bool
repeatMode RepeatMode
shuffleOrder []int
sourcePlaylistID int64
setQueueGen atomic.Int64
}
func NewQueue(logger *slog.Logger, db *database.DB) *Queue
func (q *Queue) SetPlayer(player TrackLoader)
func (q *Queue) SetQueue(filePaths []string, startIndex int, shuffleStart bool)
func (q *Queue) AddTrack(filePath string)
func (q *Queue) AddTracks(filePaths []string)
func (q *Queue) InsertNext(filePath string)
func (q *Queue) InsertTracksAt(filePaths []string, index int)
func (q *Queue) MoveQueueTracks(fromIndices []int, toIndex int)
func (q *Queue) RemoveTrack(position int)
func (q *Queue) RemoveTracks(positions []int)
func (q *Queue) Next()
func (q *Queue) Previous()
func (q *Queue) PlayIndex(index int)
func (q *Queue) ToggleShuffle()
func (q *Queue) CycleRepeat()
func (q *Queue) GetState() State
func (q *Queue) Clear()
func (q *Queue) SaveState()
func (q *Queue) RestoreState()
```
From backend/database/testhelper.go:
```go
func NewTestDB(t *testing.T) *DB
```
FK dependency chain for test data setup:
```sql
-- file_types is pre-seeded (0=.mp3, 1=.flac, 2=.ogg, 3=.wav)
-- queue row pre-seeded (id=1)
-- Insert chain:
INSERT INTO artist_credit (id, text) VALUES (1, 'Test Artist');
INSERT INTO recordings (id, name, artist_credit_id) VALUES (1, 'Test Track', 1);
INSERT INTO audio_files (id, file_path, length_milliseconds, file_type_id, recording_id)
VALUES (1, '/test/track1.mp3', 180000, 0, 1);
-- Then queue_tracks can reference audio_file_id
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Queue core operations and navigation tests</name>
<files>backend/queue/queue_test.go, backend/queue/navigation_test.go</files>
<action>
Create two test files for the queue package using internal tests (package queue, not queue_test).
**queue_test.go** — Core operation tests (~10-12 tests):
1. Define a `mockTrackLoader` struct satisfying `TrackLoader` interface at top of file. All methods are no-ops: `LoadFile` returns nil, `Play` returns nil, `IsPlaying` returns false, `CurrentPositionSeconds` returns (0, nil), `UnloadTrack` is empty. Add a `loadedFile string` field to track which file was loaded.
2. Define a `setupTestQueue(t *testing.T) (*Queue, *database.DB)` helper that:
- Calls `database.NewTestDB(t)` to get isolated DB
- Creates `NewQueue(slog.Default(), db)`
- Sets a `&mockTrackLoader{}` via `SetPlayer`
- Returns queue and db
3. Define a `seedAudioFiles(t *testing.T, db *database.DB, count int) []string` helper that:
- Inserts `count` audio_file rows with FK chain (1 shared artist_credit, 1 shared recording per file, audio_files with file_path `/test/trackN.mp3`)
- Uses `db.ExecContext()` for raw SQL inserts
- Returns the file paths as a string slice
- Uses `t.Helper()`
4. Write these test functions (all with t.Parallel()):
- `TestSetQueue_PopulatesTracks` — SetQueue with 5 file paths at startIndex 0, verify GetState returns correct track count and currentIndex
- `TestSetQueue_WithStartIndex` — SetQueue at startIndex 2, verify currentIndex is 2
- `TestSetQueue_WithShuffleStart` — SetQueue with shuffleStart=true, verify shuffleMode is true and shuffleOrder is populated
- `TestAddTrack_AppendsToQueue` — SetQueue with 3 tracks, AddTrack a 4th, verify 4 tracks total and the new track is last
- `TestInsertTracksAt_BeforeCurrentIndex` — SetQueue 5 tracks at index 2, InsertTracksAt index 1, verify currentIndex shifted by inserted count
- `TestInsertTracksAt_AfterCurrentIndex` — same but insert at index 3, verify currentIndex unchanged
- `TestMoveQueueTracks_ForwardMove` — SetQueue 5 tracks, move track from index 1 to index 3, verify order and currentIndex adjustment
- `TestMoveQueueTracks_BackwardMove` — move from index 3 to index 1, verify order
- `TestMoveQueueTracks_MoveCurrentTrack` — move the current track, verify currentIndex follows it
- `TestRemoveTrack_RemovesCorrectTrack` — SetQueue 5 tracks, remove at index 2, verify 4 tracks remain and correct track removed
- `TestRemoveTrack_RemoveCurrentTrack` — remove at currentIndex, verify index adjusts
- `TestClear_EmptiesQueue` — SetQueue, Clear, verify empty state
- `TestToggleShuffle_TogglesMode` — verify shuffle toggles on/off and shuffleOrder populates/clears
- `TestCycleRepeat_CyclesThroughModes` — verify off→all→one→off cycle
**navigation_test.go** — Navigation edge case tests (~6-8 tests):
Use direct field manipulation (same package) to set up queue state without DB:
- Create queue with `&Queue{logger: slog.Default()}`, set `tracks`, `currentIndex`, `shuffleMode`, `repeatMode`, `shuffleOrder` directly
Tests (all t.Parallel()):
- `TestNextIndex_NormalMode_AdvancesToNextTrack` — 5 tracks, index 2, repeatOff → returns 3
- `TestNextIndex_NormalMode_EndOfQueue_RepeatOff` — index at last track, repeatOff → returns -1
- `TestNextIndex_NormalMode_EndOfQueue_RepeatAll` — index at last track, repeatAll → returns 0 (wraps)
- `TestNextIndex_RepeatOne` — any index, repeatOne → returns same index
- `TestPreviousIndex_NormalMode_GoesBack` — index 3, repeatOff → returns 2
- `TestPreviousIndex_AtStart_RepeatOff` — index 0, repeatOff → returns -1
- `TestPreviousIndex_AtStart_RepeatAll` — index 0, repeatAll → returns last index
- `TestGenerateShuffleOrder_Properties` — table-driven test verifying: all indices present, no duplicates, current track at shuffleOrder[0], length matches tracks length. Test with 1, 5, and 20 tracks.
- `TestNextIndex_ShuffleMode` — set shuffleOrder, verify navigation follows shuffle order not track order
Use the established codebase test conventions: t.Parallel(), t.Helper() on helpers, t.Errorf with "got X, want Y" format, no assertion libraries.
</action>
<verify>
<automated>cd backend && go test -race -count=1 -run "TestSetQueue|TestAdd|TestInsert|TestMove|TestRemove|TestClear|TestToggle|TestCycle|TestNext|TestPrevious|TestGenerate" ./queue/ -v 2>&amp;1 | tail -30</automated>
</verify>
<done>queue_test.go has ~12 tests for core operations (SetQueue, Add, Insert, Move, Remove, Clear, ToggleShuffle, CycleRepeat); navigation_test.go has ~8 tests for Next/Previous in all modes + shuffle order properties. All pass with -race.</done>
</task>
<task type="auto">
<name>Task 2: Queue persistence round-trip tests</name>
<files>backend/queue/persistence_test.go</files>
<action>
Create persistence_test.go in the queue package (internal, package queue).
Reuse the `setupTestQueue` and `seedAudioFiles` helpers from queue_test.go (same package, accessible).
Write these test functions (all t.Parallel()):
- `TestSaveState_RestoreState_Roundtrip` — The critical safety net test:
1. Setup queue with DB, seed 5 audio files
2. SetQueue with 5 file paths at startIndex 2
3. CycleRepeat to "all"
4. ToggleShuffle
5. SaveState
6. Create a NEW Queue instance with same DB: `q2 := NewQueue(slog.Default(), db); q2.SetPlayer(&amp;mockTrackLoader{})`
7. RestoreState on q2
8. Verify ALL fields match: tracks length, each track's FilePath/Title/Artist, currentIndex, shuffleMode, repeatMode, shuffleOrder
- `TestSaveState_RestoreState_EmptyQueue` — SaveState with no tracks, RestoreState, verify empty state
- `TestSaveState_RestoreState_SingleTrack` — Verify edge case with 1 track
- `TestSaveState_RestoreState_PreservesTrackOrder` — SetQueue with 10 tracks, verify exact order after restore (not just count)
- `TestRestoreState_NoSavedState` — RestoreState on fresh DB with no prior SaveState, verify queue stays empty (no panic, no error)
- `TestSaveState_OverwritesPreviousState` — SaveState with 5 tracks, then SaveState with 3 different tracks, RestoreState should get the 3 tracks
These tests are the highest-priority safety net for Phase 7 (PERF-01). The roundtrip test verifies ALL queue state fields survive serialization, which is essential before changing persistence from full-table-rewrite to incremental.
</action>
<verify>
<automated>cd backend && go test -race -count=1 -run "TestSaveState|TestRestoreState" ./queue/ -v 2>&amp;1 | tail -20</automated>
</verify>
<done>persistence_test.go has ~6 tests covering full round-trip fidelity, empty/single edge cases, and overwrite behavior. All pass with -race.</done>
</task>
</tasks>
<verification>
```bash
cd backend && go test -race -count=1 ./queue/ -v
```
All queue tests pass with -race flag. Expected ~18-20 tests total.
</verification>
<success_criteria>
- backend/queue/queue_test.go exists with ~12 tests for core operations
- backend/queue/navigation_test.go exists with ~8 tests for navigation + shuffle
- backend/queue/persistence_test.go exists with ~6 tests for state persistence
- All tests pass with `go test -race ./queue/`
- SaveState/RestoreState roundtrip preserves all state fields
- Edge cases covered: empty queue, single track, boundary indices, repeat mode wrapping
</success_criteria>
<output>
After completion, create `.planning/phases/04-queue-config-player-tests/04-01-SUMMARY.md`
</output>