felt/internal/clock/warnings_test.go
Mikkel Georgsen ae90d9bfae feat(01-04): add clock warnings, API routes, tests, and server wiring
- Clock API routes: start, pause, resume, advance, rewind, jump, get, warnings
- Role-based access control (floor+ for mutations, any auth for reads)
- Clock state persistence callback to DB on meaningful changes
- Blind structure levels loaded from DB on clock start
- Clock registry wired into HTTP server and cmd/leaf main
- 25 tests covering: state machine, countdown, pause/resume, auto-advance,
  jump, rewind, hand-for-hand, warnings, overtime, crash recovery, snapshot
- Fix missing crypto/rand import in auth/pin.go (Rule 3 auto-fix)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 03:56:23 +01:00

160 lines
4 KiB
Go

package clock
import (
"testing"
"time"
)
func TestWarningThresholdDetection(t *testing.T) {
engine := NewClockEngine("test-warnings-1", nil)
engine.SetWarnings([]Warning{
{Seconds: 5, Type: "both", SoundID: "warning_5s", Message: "5 seconds"},
})
engine.LoadLevels([]Level{
{Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 8},
})
engine.Start("op1")
// Tick until we cross the 5s threshold
// The level is 8 seconds, so after ~3 seconds we should hit the 5s warning
warningFired := false
for i := 0; i < 100; i++ {
time.Sleep(50 * time.Millisecond)
engine.Tick()
snap := engine.Snapshot()
if snap.RemainingMs < 5000 {
// Check if warning was emitted by checking internal state
engine.mu.RLock()
warningFired = engine.emittedWarnings[5]
engine.mu.RUnlock()
if warningFired {
break
}
}
}
if !warningFired {
t.Error("expected 5-second warning to fire")
}
}
func TestWarningNotReEmitted(t *testing.T) {
engine := NewClockEngine("test-warnings-2", nil)
engine.SetWarnings([]Warning{
{Seconds: 5, Type: "both", SoundID: "warning_5s", Message: "5 seconds"},
})
engine.LoadLevels([]Level{
{Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 8},
})
engine.Start("op1")
// Tick until warning fires
for i := 0; i < 100; i++ {
time.Sleep(50 * time.Millisecond)
engine.Tick()
engine.mu.RLock()
fired := engine.emittedWarnings[5]
engine.mu.RUnlock()
if fired {
break
}
}
// Mark that we've seen the warning
engine.mu.RLock()
firstFired := engine.emittedWarnings[5]
engine.mu.RUnlock()
if !firstFired {
t.Fatal("warning never fired")
}
// Continue ticking -- emittedWarnings[5] should remain true (not re-emitted)
for i := 0; i < 10; i++ {
time.Sleep(10 * time.Millisecond)
engine.Tick()
}
engine.mu.RLock()
stillFired := engine.emittedWarnings[5]
engine.mu.RUnlock()
if !stillFired {
t.Error("warning flag was cleared unexpectedly")
}
}
func TestWarningResetOnLevelChange(t *testing.T) {
engine := NewClockEngine("test-warnings-3", nil)
engine.SetWarnings([]Warning{
{Seconds: 3, Type: "both", SoundID: "warning_3s", Message: "3 seconds"},
})
engine.LoadLevels([]Level{
{Position: 0, LevelType: "round", GameType: "nlhe", SmallBlind: 100, BigBlind: 200, DurationSeconds: 5},
{Position: 1, LevelType: "round", GameType: "nlhe", SmallBlind: 200, BigBlind: 400, DurationSeconds: 5},
})
engine.Start("op1")
// Tick until warning fires for level 0
for i := 0; i < 100; i++ {
time.Sleep(30 * time.Millisecond)
engine.Tick()
engine.mu.RLock()
fired := engine.emittedWarnings[3]
engine.mu.RUnlock()
if fired {
break
}
}
// Manual advance to level 1
engine.AdvanceLevel("op1")
// After advance, warnings should be reset
engine.mu.RLock()
resetCheck := engine.emittedWarnings[3]
engine.mu.RUnlock()
if resetCheck {
t.Error("expected warnings to be reset after level change")
}
}
func TestDefaultWarnings(t *testing.T) {
warnings := DefaultWarnings()
if len(warnings) != 3 {
t.Fatalf("expected 3 default warnings, got %d", len(warnings))
}
expectedSeconds := []int{60, 30, 10}
for i, w := range warnings {
if w.Seconds != expectedSeconds[i] {
t.Errorf("warning %d: expected %ds, got %ds", i, expectedSeconds[i], w.Seconds)
}
if w.Type != "both" {
t.Errorf("warning %d: expected type 'both', got '%s'", i, w.Type)
}
}
}
func TestCustomWarnings(t *testing.T) {
engine := NewClockEngine("test-custom-warnings", nil)
custom := []Warning{
{Seconds: 120, Type: "visual", SoundID: "custom_120s", Message: "2 minutes"},
{Seconds: 15, Type: "audio", SoundID: "custom_15s", Message: "15 seconds"},
}
engine.SetWarnings(custom)
got := engine.GetWarnings()
if len(got) != 2 {
t.Fatalf("expected 2 custom warnings, got %d", len(got))
}
if got[0].Seconds != 120 {
t.Errorf("expected first warning at 120s, got %ds", got[0].Seconds)
}
if got[1].Type != "audio" {
t.Errorf("expected second warning type 'audio', got '%s'", got[1].Type)
}
}