277 lines
13 KiB
Markdown
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>&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(&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>&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>
|