301 lines
14 KiB
Markdown
301 lines
14 KiB
Markdown
---
|
|
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>
|