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

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-queue-config-player-tests 01 execute 1
backend/queue/queue_test.go
backend/queue/navigation_test.go
backend/queue/persistence_test.go
true
TEST-02
truths artifacts key_links
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
path provides min_lines
backend/queue/queue_test.go Tests for SetQueue, Add/Insert/Move/Remove, ToggleShuffle, CycleRepeat, Clear, mock TrackLoader 200
path provides min_lines
backend/queue/navigation_test.go Tests for Next/Previous in all modes, edge cases (empty, single, boundary) 150
path provides min_lines
backend/queue/persistence_test.go Tests for SaveState/RestoreState roundtrip fidelity 100
from to via pattern
backend/queue/queue_test.go backend/database/testhelper.go database.NewTestDB(t) database.NewTestDB
from to via pattern
backend/queue/persistence_test.go backend/queue/persistence.go SaveState/RestoreState roundtrip 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.

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

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-test-infrastructure/03-01-SUMMARY.md

From backend/queue/queue.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:

func NewTestDB(t *testing.T) *DB

FK dependency chain for test data setup:

-- 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(&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. 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.

<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>
After completion, create `.planning/phases/04-queue-config-player-tests/04-01-SUMMARY.md`