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