--- 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" --- 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. @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-test-infrastructure/03-01-SUMMARY.md 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 ``` Task 1: Queue core operations and navigation tests backend/queue/queue_test.go, backend/queue/navigation_test.go 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. cd backend && go test -race -count=1 -run "TestSetQueue|TestAdd|TestInsert|TestMove|TestRemove|TestClear|TestToggle|TestCycle|TestNext|TestPrevious|TestGenerate" ./queue/ -v 2>&1 | tail -30 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. Task 2: Queue persistence round-trip tests backend/queue/persistence_test.go 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(&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. cd backend && go test -race -count=1 -run "TestSaveState|TestRestoreState" ./queue/ -v 2>&1 | tail -20 persistence_test.go has ~6 tests covering full round-trip fidelity, empty/single edge cases, and overwrite behavior. All pass with -race. ```bash cd backend && go test -race -count=1 ./queue/ -v ``` All queue tests pass with -race flag. Expected ~18-20 tests total. - 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 After completion, create `.planning/phases/04-queue-config-player-tests/04-01-SUMMARY.md`