diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 8b7688c..b9eb76c 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -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 | — |
diff --git a/.planning/phases/04-queue-config-player-tests/04-01-PLAN.md b/.planning/phases/04-queue-config-player-tests/04-01-PLAN.md
new file mode 100644
index 0000000..929b68c
--- /dev/null
+++ b/.planning/phases/04-queue-config-player-tests/04-01-PLAN.md
@@ -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"
+---
+
+
+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
+
+
+
diff --git a/.planning/phases/04-queue-config-player-tests/04-02-PLAN.md b/.planning/phases/04-queue-config-player-tests/04-02-PLAN.md
new file mode 100644
index 0000000..f656f52
--- /dev/null
+++ b/.planning/phases/04-queue-config-player-tests/04-02-PLAN.md
@@ -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"
+---
+
+
+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).
+
+
+
+@/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
+
+
+
+
+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}
+```
+
+
+
+
+
+
+ Task 1: Config and sub-config validation tests
+ 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
+
+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).
+
+
+ cd backend && go test -race -count=1 ./config/ ./theme/ ./tracklist/ ./favorites/ ./library/ -run "TestTheme|TestTrackList|TestFavorites|TestLibrary|TestConfig" -v 2>&1 | tail -40
+
+ 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.
+
+
+
+ Task 2: Player volume and state mapping tests
+ backend/player/volume_test.go
+
+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.
+
+
+ cd backend && go test -race -count=1 -run "TestUserVolume|TestVolume|TestClamp|TestState" ./player/ -v 2>&1 | tail -20
+
+ 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.
+
+
+
+
+
+```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.
+
+
+
+- 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`
+
+
+