🎉 live server seems to be working now

This commit is contained in:
2026-05-14 14:29:57 +02:00
commit d72e439fd9
181 changed files with 47406 additions and 0 deletions

View File

@@ -0,0 +1,259 @@
// ABOUTME: Tests for refactored Player composing Receiver
// ABOUTME: Verifies backward-compatible API and new ProcessCallback
package sendspin
import (
"context"
"testing"
"time"
)
func TestNewPlayer_Defaults(t *testing.T) {
player, err := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test Player",
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if player == nil {
t.Fatal("expected non-nil player")
}
if player.receiver != nil {
t.Error("receiver should be nil before Connect")
}
}
func TestNewPlayer_ProcessCallbackStored(t *testing.T) {
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test Player",
ProcessCallback: func(samples []int32) {},
})
if player.config.ProcessCallback == nil {
t.Error("expected ProcessCallback to be stored in config")
}
}
// TestNewPlayer_DeviceInfoPassesThrough guards #48: --manufacturer and
// --product-name CLI flags write into PlayerConfig.DeviceInfo, and must
// survive the trip into the underlying Receiver without being replaced
// by the library defaults.
func TestNewPlayer_DeviceInfoPassesThrough(t *testing.T) {
custom := DeviceInfo{
ProductName: "Custom Product",
Manufacturer: "Custom Mfg",
SoftwareVersion: "9.9.9",
}
player, err := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
DeviceInfo: custom,
})
if err != nil {
t.Fatalf("NewPlayer: %v", err)
}
if player.config.DeviceInfo != custom {
t.Errorf("config.DeviceInfo = %+v, want %+v", player.config.DeviceInfo, custom)
}
}
// TestNewPlayer_StaticDelayStored guards #47: PlayerConfig.StaticDelayMs
// must be preserved on the Player so Connect can plumb it into Receiver
// and from there into Scheduler.
func TestNewPlayer_StaticDelayStored(t *testing.T) {
player, err := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
StaticDelayMs: 250,
})
if err != nil {
t.Fatalf("NewPlayer: %v", err)
}
if player.config.StaticDelayMs != 250 {
t.Errorf("config.StaticDelayMs = %d, want 250", player.config.StaticDelayMs)
}
}
// TestReceiver_StaticDelayDefaultZero sanity-checks that a ReceiverConfig
// without StaticDelayMs set produces a scheduler with zero offset. This is
// the test that would have flagged the new field if a future refactor
// stopped plumbing it through. Uses a short connect attempt + teardown
// because we don't want to actually dial anything.
func TestReceiver_StaticDelayDefaultZero(t *testing.T) {
recv, err := NewReceiver(ReceiverConfig{
ServerAddr: "localhost:0",
PlayerName: "Test",
})
if err != nil {
t.Fatalf("NewReceiver: %v", err)
}
defer recv.Close()
if recv.config.StaticDelayMs != 0 {
t.Errorf("default StaticDelayMs = %d, want 0", recv.config.StaticDelayMs)
}
}
// TestNewPlayer_ReconnectDefaultsApplied guards the reconnect backoff
// defaults (#38). When the caller enables reconnect but leaves timing
// fields zero, NewPlayer must fill them with the documented defaults so
// an accidentally-zero delay never spin-loops.
func TestNewPlayer_ReconnectDefaultsApplied(t *testing.T) {
player, err := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
Reconnect: ReconnectConfig{Enabled: true},
})
if err != nil {
t.Fatalf("NewPlayer: %v", err)
}
rc := player.config.Reconnect
if rc.InitialDelay != 500*time.Millisecond {
t.Errorf("InitialDelay = %v, want 500ms", rc.InitialDelay)
}
if rc.MaxDelay != 30*time.Second {
t.Errorf("MaxDelay = %v, want 30s", rc.MaxDelay)
}
if rc.Multiplier != 2.0 {
t.Errorf("Multiplier = %v, want 2.0", rc.Multiplier)
}
if rc.MaxAttempts != 0 {
t.Errorf("MaxAttempts = %d, want 0 (infinite)", rc.MaxAttempts)
}
}
// TestNewPlayer_ReconnectDefaultsSkippedWhenDisabled makes sure we don't
// silently turn reconnect on. If Enabled is false we leave the zero values
// alone — there is no supervisor goroutine to read them anyway.
func TestNewPlayer_ReconnectDefaultsSkippedWhenDisabled(t *testing.T) {
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
})
if player.config.Reconnect.Enabled {
t.Error("Reconnect.Enabled should default to false")
}
if player.config.Reconnect.InitialDelay != 0 {
t.Error("InitialDelay should not be populated when Reconnect.Enabled is false")
}
}
// TestNewPlayer_ReconnectRediscoverCallbackStored confirms the closure
// survives into player.config so the reconnect supervisor can call it.
func TestNewPlayer_ReconnectRediscoverCallbackStored(t *testing.T) {
called := false
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
Reconnect: ReconnectConfig{
Enabled: true,
Rediscover: func(ctx context.Context) (string, error) {
called = true
return "other:1234", nil
},
},
})
if player.config.Reconnect.Rediscover == nil {
t.Fatal("Rediscover callback not stored")
}
addr, err := player.config.Reconnect.Rediscover(context.Background())
if err != nil || addr != "other:1234" || !called {
t.Errorf("callback not invoked correctly: addr=%q err=%v called=%v", addr, err, called)
}
}
// TestJitter stays inside the ±frac band. With frac=0.2 and a 1s delay,
// the result must always fall within [800ms, 1200ms].
func TestJitter(t *testing.T) {
base := 1 * time.Second
for i := 0; i < 100; i++ {
got := jitter(base, 0.2)
if got < 800*time.Millisecond || got > 1200*time.Millisecond {
t.Errorf("jitter(%v, 0.2) = %v, outside ±20%% band", base, got)
}
}
if jitter(0, 0.2) != 0 {
t.Error("jitter(0, _) should return 0")
}
}
func TestPlayer_StatusBeforeConnect(t *testing.T) {
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test Player",
Volume: 80,
})
status := player.Status()
if status.Volume != 80 {
t.Errorf("expected volume 80, got %d", status.Volume)
}
if status.Connected {
t.Error("expected not connected before Connect()")
}
if status.State != "idle" {
t.Errorf("expected state idle, got %s", status.State)
}
}
// TestPlayer_EnsureCapsResolved_ExplicitOverrideSkipsProbe asserts replace
// semantics: if the user has set either MaxSampleRate or MaxBitDepth,
// ensureCapsResolved must NOT overwrite the other field via probe.
// Explicit user choice wins entirely, even when only one of the two
// fields is non-zero.
func TestPlayer_EnsureCapsResolved_ExplicitOverrideSkipsProbe(t *testing.T) {
tests := []struct {
name string
rate int
depth int
wantRate int
wantDepth int
}{
{"both set", 48000, 16, 48000, 16},
{"rate only", 96000, 0, 96000, 0},
{"depth only", 0, 16, 0, 16},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
MaxSampleRate: tt.rate,
MaxBitDepth: tt.depth,
})
player.ensureCapsResolved()
if player.config.MaxSampleRate != tt.wantRate {
t.Errorf("MaxSampleRate = %d, want %d (probe must not run)",
player.config.MaxSampleRate, tt.wantRate)
}
if player.config.MaxBitDepth != tt.wantDepth {
t.Errorf("MaxBitDepth = %d, want %d (probe must not run)",
player.config.MaxBitDepth, tt.wantDepth)
}
})
}
}
// TestPlayer_EnsureCapsResolved_RunsOnce guards reconnect behavior: the
// caps are probed at most once. The cached flag is set even on probe-skip
// paths so subsequent reconnects don't re-probe miniaudio either.
func TestPlayer_EnsureCapsResolved_RunsOnce(t *testing.T) {
player, _ := NewPlayer(PlayerConfig{
ServerAddr: "localhost:8927",
PlayerName: "Test",
MaxSampleRate: 48000, // user-set, so no probe
})
player.ensureCapsResolved()
if !player.capsResolved {
t.Error("capsResolved should be true after first call")
}
// Mutating MaxSampleRate after first call must not be re-resolved by a
// second invocation — proves the early return on capsResolved fires.
player.config.MaxSampleRate = 99999
player.ensureCapsResolved()
if player.config.MaxSampleRate != 99999 {
t.Errorf("second call should be no-op; got MaxSampleRate=%d",
player.config.MaxSampleRate)
}
}