docs(04): create phase plan for queue, config & player tests
This commit is contained in:
@@ -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>&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>
|
||||
@@ -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>&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>&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>
|
||||
Reference in New Issue
Block a user