Files
yellowjacket/.planning/milestones/v1.0-phases/04-queue-config-player-tests/04-02-PLAN.md
T
yonlu 6ce0661fca chore: complete v1.0 Consolidation milestone
Archive milestone artifacts:
- milestones/v1.0-ROADMAP.md (full roadmap archive)
- milestones/v1.0-REQUIREMENTS.md (26/26 requirements complete)
- milestones/v1.0-phases/ (8 phase directories with plans, summaries, verifications)

Updated:
- PROJECT.md: full evolution review, all consolidation requirements validated
- ROADMAP.md: collapsed to milestone summary with archive link
- STATE.md: reset for next milestone
- MILESTONES.md: created with stats and accomplishments
- RETROSPECTIVE.md: created with lessons learned

Deleted:
- REQUIREMENTS.md (archived, fresh for next milestone)

8 phases, 17 plans, 34 tasks, 84 tests added, 6 days
2026-03-05 09:34:43 -05:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
04-queue-config-player-tests 02 execute 1
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
true
TEST-04
TEST-05
truths artifacts key_links
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
path provides min_lines
backend/config/config_test.go Tests for Load/Save roundtrip, Validate composition, missing file handling, defaults 80
path provides min_lines
backend/theme/config_test.go Tests for theme validation (hex color, background shade) 40
path provides min_lines
backend/tracklist/config_test.go Tests for tracklist validation (valid/invalid/duplicate columns) 40
path provides min_lines
backend/favorites/config_test.go Tests for favorites validation (icon style) 30
path provides min_lines
backend/library/config_test.go Tests for library validation (directory existence, scan concurrency) 40
path provides min_lines
backend/player/volume_test.go Tests for volume conversion, clamp, state mapping 60
from to via pattern
backend/config/config_test.go backend/config/config.go Load/Save roundtrip with t.TempDir() Save|Load
from to via pattern
backend/player/volume_test.go backend/player/volume.go ToVolume/ToUserVolume conversion 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).

<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>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md

From backend/config/config.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:

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:

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:

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:

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:

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:

type State string
const Playing State = "playing", Paused = "paused", Stopped = "stopped"
func stateToMediaControls(s State) mediacontrols.PlaybackState  // unexported

From backend/mediacontrols/mediacontrols.go:

type PlaybackState int
const StateStopped PlaybackState = 0, StatePlaying = 1, StatePaused = 2

From backend/config/window.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.

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