docs(04): create phase plan for queue, config & player tests

This commit is contained in:
2026-03-03 13:32:55 -05:00
parent 6e3c95ea61
commit 5400df257c
3 changed files with 581 additions and 2 deletions
+5 -2
View File
@@ -68,7 +68,10 @@ Plans:
2. Config package has ~8-10 tests covering load/save roundtrip fidelity, validation rule enforcement, default value application, and graceful handling of missing or empty config files
3. Player pure logic (UserVolume↔Volume conversion, state serialization/deserialization, format detection from file extension) is extracted into standalone functions with ~5-8 unit tests
4. All tests in this phase pass with `-race` flag enabled
**Plans:** TBD
**Plans:** 2 plans
Plans:
- [ ] 04-01-PLAN.md — Queue package unit tests (core operations, navigation, persistence roundtrip)
- [ ] 04-02-PLAN.md — Config + Player tests (sub-config validators, load/save roundtrip, volume conversion, state mapping)
### Phase 5: Database & Library Tests
**Goal:** Database queries (especially FTS5 search) and library scan logic have unit tests that lock down current behavior before SQL consolidation and performance optimization
@@ -120,7 +123,7 @@ Plans:
| 1. Concurrency Race Fixes | 1/1 | Complete | 2026-02-28 |
| 2. Backend Correctness | 2/2 | Complete | 2026-03-03 |
| 3. Test Infrastructure | 0/1 | Planned | — |
| 4. Queue, Config & Player Tests | 0/? | Not started | — |
| 4. Queue, Config & Player Tests | 0/2 | Planned | — |
| 5. Database & Library Tests | 0/? | Not started | — |
| 6. SQL Consolidation & Code Quality | 0/? | Not started | — |
| 7. Backend Performance | 0/? | Not started | — |
@@ -0,0 +1,276 @@
---
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>
@@ -0,0 +1,300 @@
---
phase: 04-queue-config-player-tests
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- backend/config/config_test.go
- backend/theme/config_test.go
- backend/tracklist/config_test.go
- backend/favorites/config_test.go
- backend/library/config_test.go
- backend/player/volume_test.go
autonomous: true
requirements: [TEST-04, TEST-05]
must_haves:
truths:
- "Config load/save roundtrip preserves all fields without data loss"
- "Sub-config validators reject invalid values and accept valid ones"
- "Missing config file is handled gracefully (created with defaults)"
- "UserVolume↔Volume conversion is mathematically correct at all boundary values"
- "stateToMediaControls maps all player states correctly"
- "All config and player tests pass with -race flag"
artifacts:
- path: "backend/config/config_test.go"
provides: "Tests for Load/Save roundtrip, Validate composition, missing file handling, defaults"
min_lines: 80
- path: "backend/theme/config_test.go"
provides: "Tests for theme validation (hex color, background shade)"
min_lines: 40
- path: "backend/tracklist/config_test.go"
provides: "Tests for tracklist validation (valid/invalid/duplicate columns)"
min_lines: 40
- path: "backend/favorites/config_test.go"
provides: "Tests for favorites validation (icon style)"
min_lines: 30
- path: "backend/library/config_test.go"
provides: "Tests for library validation (directory existence, scan concurrency)"
min_lines: 40
- path: "backend/player/volume_test.go"
provides: "Tests for volume conversion, clamp, state mapping"
min_lines: 60
key_links:
- from: "backend/config/config_test.go"
to: "backend/config/config.go"
via: "Load/Save roundtrip with t.TempDir()"
pattern: "Save|Load"
- from: "backend/player/volume_test.go"
to: "backend/player/volume.go"
via: "ToVolume/ToUserVolume conversion"
pattern: "ToVolume|ToUserVolume"
---
<objective>
Write unit tests for the config package (including all sub-config validators) and player pure logic (volume conversion, state mapping).
Purpose: Config tests verify roundtrip fidelity and validation rules, which are essential before any config format changes. Player logic tests characterize the volume conversion math and state mapping as a safety net for any future player refactoring.
Output: 6 test files — 5 for config/sub-configs (~8-10 tests) and 1 for player (~5-6 tests).
</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
<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
From backend/config/config.go:
```go
type Config struct {
ctx context.Context // unexported
logger *slog.Logger // unexported
filePath string // unexported — set by NewConfig or manually for tests
Library *library.Config `toml:"Library"`
Theme *theme.Config `toml:"Theme"`
Window *WindowConfig `toml:"Window"`
TrackList *tracklist.Config `toml:"TrackList"`
Favorites *favorites.Config `toml:"Favorites"`
}
func NewConfig(logger *slog.Logger) (*Config, error) // reads from system config dir — NOT usable in tests
func (c *Config) Validate() error // delegates to sub-configs
func (c *Config) Load() error // reads from c.filePath
func (c *Config) Save() error // writes to c.filePath with 0o644
func (c *Config) applyDefaults() // unexported — fills nil sub-configs
```
From backend/theme/config.go:
```go
type BackgroundShade string // "darker", "dark", "light"
type Config struct { AccentColor string; BackgroundShade BackgroundShade }
func (c *Config) ApplyDefaults()
func (c *Config) Validate() error // checks hex color regex + shade enum
const DefaultAccentColor = "#ffd43b"
const DefaultBackgroundShade = BackgroundDark
```
From backend/tracklist/config.go:
```go
type ColumnID string // 16 valid values
type Column struct { ID ColumnID }
type Config struct { Columns []Column }
func (c *Config) ApplyDefaults()
func (c *Config) Validate() error // checks valid IDs + no duplicates
var DefaultColumns = []Column{{ColTrackName}, {ColArtistName}, {ColTrackLength}}
```
From backend/favorites/config.go:
```go
type IconStyle string // "heart", "star"
type Config struct { PlaylistID int64; IconStyle; PinDefault bool }
func (c *Config) ApplyDefaults()
func (c *Config) Validate() error // checks icon style enum
const DefaultIconStyle = IconHeart
```
From backend/library/config.go:
```go
type ScanConcurrency string // "auto", "ssd", "hdd"
type Directory string
type Config struct { DirectoryPath Directory; ScanConcurrency }
func (c *Config) Validate() error // checks dir exists on filesystem + mode enum
const DefaultScanConcurrency = ScanConcurrencyAuto
```
From backend/player/volume.go:
```go
type UserVolume int // 0-100
type Volume float64 // -5 to 0
const MinUserVol UserVolume = 0, MaxUserVol = 100, DefaultUserVol = 50
const MinVol Volume = -5, MaxVol = 0
func (uv UserVolume) ToVolume() Volume
func (v Volume) ToUserVolume() UserVolume
func clampVolume(v UserVolume) UserVolume // unexported
```
From backend/player/player.go:
```go
type State string
const Playing State = "playing", Paused = "paused", Stopped = "stopped"
func stateToMediaControls(s State) mediacontrols.PlaybackState // unexported
```
From backend/mediacontrols/mediacontrols.go:
```go
type PlaybackState int
const StateStopped PlaybackState = 0, StatePlaying = 1, StatePaused = 2
```
From backend/config/window.go:
```go
type WindowConfig struct { Width int; Height int }
func NewDefaultWindowConfig() *WindowConfig // returns &WindowConfig{Width: 1024, Height: 768}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Config and sub-config validation tests</name>
<files>backend/config/config_test.go, backend/theme/config_test.go, backend/tracklist/config_test.go, backend/favorites/config_test.go, backend/library/config_test.go</files>
<action>
Create 5 test files for config and all sub-config packages. All use internal test packages (same package name). Follow established conventions: t.Parallel(), table-driven subtests, stdlib testing only, no assertion libraries, t.Helper() on helpers.
**backend/theme/config_test.go** (package theme) — ~3 tests:
- `TestThemeConfig_Validate_ValidValues` — table-driven: valid hex colors ("#fff", "#ffd43b", "#000000") with valid shades ("darker", "dark", "light") all pass
- `TestThemeConfig_Validate_InvalidHexColor` — table-driven: invalid colors ("fff", "#gg0000", "#12345", "red", "") all return error containing "invalid hex color"
- `TestThemeConfig_Validate_InvalidBackgroundShade` — shade "neon" returns error containing "unknown background shade"
- `TestThemeConfig_ApplyDefaults` — verify zero-value Config gets DefaultAccentColor and DefaultBackgroundShade
**backend/tracklist/config_test.go** (package tracklist) — ~3 tests:
- `TestTrackListConfig_Validate_ValidColumns` — valid column IDs pass
- `TestTrackListConfig_Validate_UnknownColumnID` — unknown ID returns error containing "unknown track-list column ID"
- `TestTrackListConfig_Validate_DuplicateColumn` — duplicate ID returns error containing "duplicate column ID"
- `TestTrackListConfig_ApplyDefaults` — verify zero-value Config gets DefaultColumns
**backend/favorites/config_test.go** (package favorites) — ~2-3 tests:
- `TestFavoritesConfig_Validate_ValidIconStyles` — table-driven: "heart", "star" both pass
- `TestFavoritesConfig_Validate_InvalidIconStyle` — "diamond" returns error containing "unknown favorites icon style"
- `TestFavoritesConfig_ApplyDefaults` — verify zero-value gets DefaultIconStyle
**backend/library/config_test.go** (package library) — ~3-4 tests:
- `TestLibraryConfig_Validate_ValidDirectory` — use t.TempDir() as directory, all scan concurrency modes ("auto", "ssd", "hdd") pass
- `TestLibraryConfig_Validate_NonexistentDirectory` — "/nonexistent/path/xyz" returns error
- `TestLibraryConfig_Validate_InvalidScanConcurrency` — "turbo" returns error containing "unknown scan concurrency"
- `TestLibraryConfig_Validate_EmptyDirectory` — empty DirectoryPath with valid scan concurrency passes (no dir check when empty)
- `TestLibraryConfig_ApplyDefaults` — verify zero-value ScanConcurrency gets DefaultScanConcurrency
**backend/config/config_test.go** (package config) — ~3-4 tests:
- `TestConfig_LoadSave_Roundtrip` — The critical roundtrip test:
1. Create Config struct directly with `filePath` set to `filepath.Join(t.TempDir(), "config.toml")`
2. Set all sub-configs to non-default values: theme accent "#ff0000", shade "light", tracklist columns with 5 columns, favorites icon "star", library directory set to a second t.TempDir(), library scan concurrency "ssd", window 800x600
3. Call applyDefaults() then Save()
4. Create NEW Config struct with same filePath, call Load()
5. Verify ALL fields match the original values
Note: Set `logger` to `slog.Default()` on the Config struct for both instances.
- `TestConfig_Load_MissingFile` — Config with filePath pointing to nonexistent file. Load() should create the file with defaults (current behavior). Verify file exists after Load().
- `TestConfig_Validate_ComposesSubConfigErrors` — Config with invalid theme (bad hex) AND invalid tracklist (unknown column) returns an error. Verify both error messages are present (errors.Join behavior).
- `TestConfig_ApplyDefaults_NilSubConfigs` — Config with all nil sub-configs, call applyDefaults(), verify all sub-configs are non-nil with sensible defaults.
For the roundtrip test, import sub-config packages: theme, tracklist, favorites, library. Access unexported fields (filePath, logger) directly since this is an internal test (package config).
</action>
<verify>
<automated>cd backend && go test -race -count=1 ./config/ ./theme/ ./tracklist/ ./favorites/ ./library/ -run "TestTheme|TestTrackList|TestFavorites|TestLibrary|TestConfig" -v 2>&amp;1 | tail -40</automated>
</verify>
<done>5 test files exist covering: theme hex+shade validation, tracklist column validation, favorites icon validation, library dir+concurrency validation, and config load/save roundtrip. All pass with -race.</done>
</task>
<task type="auto">
<name>Task 2: Player volume and state mapping tests</name>
<files>backend/player/volume_test.go</files>
<action>
Create volume_test.go in the player package (internal, package player). Follow established conventions: t.Parallel(), table-driven subtests, stdlib testing only.
Write these test functions:
- `TestUserVolume_ToVolume` — table-driven with cases:
| UserVolume | Expected Volume |
|------------|-----------------|
| 0 (MinUserVol) | -5.0 (MinVol) |
| 100 (MaxUserVol) | 0.0 (MaxVol) |
| 50 (DefaultUserVol) | -2.5 (midpoint) |
| 25 | -3.75 |
| 75 | -1.25 |
For each: verify `uv.ToVolume()` matches expected within a tolerance of 0.001 (use math.Abs for float comparison).
- `TestVolume_ToUserVolume` — table-driven with inverse cases:
| Volume | Expected UserVolume |
|--------|---------------------|
| -5.0 (MinVol) | 0 (MinUserVol) |
| 0.0 (MaxVol) | 100 (MaxUserVol) |
| -2.5 | 50 |
| -3.75 | 25 |
| -1.25 | 75 |
For each: verify `v.ToUserVolume()` matches expected exactly (int comparison).
- `TestUserVolume_ToVolume_OutOfRange` — table-driven: values outside [0,100] like -1, 101, 200, -50. Verify ToVolume() returns zero-value Volume (0.0) per current implementation (the `if` guard fails, returns uninitialized `newVol`).
- `TestVolume_ToUserVolume_OutOfRange` — values outside [-5,0] like -6.0, 1.0, -10.0. Verify ToUserVolume() returns zero-value UserVolume (0) per current implementation.
- `TestUserVolume_ToVolume_Roundtrip` — for every UserVolume from 0 to 100, convert to Volume and back. Verify roundtrip matches original value. This is the characterization test — if the math changes, this breaks.
- `TestClampVolume` — table-driven:
| Input | Expected |
|-------|----------|
| -10 | 0 (MinUserVol) |
| 0 | 0 |
| 50 | 50 |
| 100 | 100 |
| 150 | 100 (MaxUserVol) |
- `TestStateToMediaControls` — table-driven:
| State | Expected PlaybackState |
|-------|------------------------|
| Playing | mediacontrols.StatePlaying (1) |
| Paused | mediacontrols.StatePaused (2) |
| Stopped | mediacontrols.StateStopped (0) |
| State("unknown") | mediacontrols.StateStopped (0) — default case |
Import "yellowjacket/backend/mediacontrols" for the PlaybackState constants. Use `math` for float comparison tolerance.
</action>
<verify>
<automated>cd backend && go test -race -count=1 -run "TestUserVolume|TestVolume|TestClamp|TestState" ./player/ -v 2>&amp;1 | tail -20</automated>
</verify>
<done>volume_test.go has ~7 tests covering ToVolume/ToUserVolume conversion at all boundaries, out-of-range behavior, full roundtrip 0-100, clamp, and state mapping. All pass with -race.</done>
</task>
</tasks>
<verification>
```bash
cd backend && go test -race -count=1 ./config/ ./theme/ ./tracklist/ ./favorites/ ./library/ ./player/ -v
```
All config and player tests pass with -race flag. Expected ~15-17 tests total.
</verification>
<success_criteria>
- 5 config test files exist covering all sub-config validators + composed Config
- Config load/save roundtrip preserves all non-default values
- Missing config file handled gracefully
- volume_test.go exists with ~7 tests for volume conversion + state mapping
- ToVolume/ToUserVolume roundtrip is verified for all values 0-100
- All tests pass with `go test -race`
</success_criteria>
<output>
After completion, create `.planning/phases/04-queue-config-player-tests/04-02-SUMMARY.md`
</output>