package daemon import ( "sync" "sync/atomic" "testing" "time" ) func TestHeartbeatService_TimesOutInactiveSession(t *testing.T) { var timedOut atomic.Bool hs := NewHeartbeatService(HeartbeatConfig{ Interval: 20 * time.Millisecond, Timeout: 40 * time.Millisecond, }, func(sessionID string) { if sessionID == "test-session" { timedOut.Store(true) } }) hs.Start() defer hs.Stop() hs.Register("test-session") // 不调用 Beat,等待超时 time.Sleep(120 * time.Millisecond) if !timedOut.Load() { t.Error("session should have timed out") } } func TestHeartbeatService_BeatPreventsTimeout(t *testing.T) { var timedOut atomic.Bool hs := NewHeartbeatService(HeartbeatConfig{ Interval: 20 * time.Millisecond, Timeout: 60 * time.Millisecond, }, func(_ string) { timedOut.Store(true) }) hs.Start() defer hs.Stop() hs.Register("active-session") // 每 25ms 发送心跳,持续 150ms(不超时) stop := make(chan struct{}) go func() { ticker := time.NewTicker(25 * time.Millisecond) defer ticker.Stop() for { select { case <-stop: return case <-ticker.C: hs.Beat("active-session") } } }() time.Sleep(150 * time.Millisecond) close(stop) if timedOut.Load() { t.Error("session with regular heartbeats should not timeout") } } func TestHeartbeatService_UnregisterPreventsCallback(t *testing.T) { var callCount atomic.Int32 hs := NewHeartbeatService(HeartbeatConfig{ Interval: 20 * time.Millisecond, Timeout: 30 * time.Millisecond, }, func(_ string) { callCount.Add(1) }) hs.Start() defer hs.Stop() hs.Register("temp-session") time.Sleep(10 * time.Millisecond) hs.Unregister("temp-session") // 正常关闭会话 time.Sleep(100 * time.Millisecond) if callCount.Load() > 0 { t.Error("unregistered session should not trigger timeout callback") } } func TestHeartbeatService_MultipleSessionsIndependent(t *testing.T) { var mu sync.Mutex timedOut := make(map[string]bool) hs := NewHeartbeatService(HeartbeatConfig{ Interval: 15 * time.Millisecond, Timeout: 40 * time.Millisecond, }, func(sessionID string) { mu.Lock() timedOut[sessionID] = true mu.Unlock() }) hs.Start() defer hs.Stop() hs.Register("active") hs.Register("inactive") // 只给 active 发心跳 stop := make(chan struct{}) go func() { ticker := time.NewTicker(20 * time.Millisecond) defer ticker.Stop() for { select { case <-stop: return case <-ticker.C: hs.Beat("active") } } }() time.Sleep(200 * time.Millisecond) close(stop) mu.Lock() defer mu.Unlock() if timedOut["active"] { t.Error("active session should not timeout") } if !timedOut["inactive"] { t.Error("inactive session should timeout") } } func TestHeartbeatService_StopStopsScanning(t *testing.T) { var callCount atomic.Int32 hs := NewHeartbeatService(HeartbeatConfig{ Interval: 20 * time.Millisecond, Timeout: 30 * time.Millisecond, }, func(_ string) { callCount.Add(1) }) hs.Start() hs.Register("s1") time.Sleep(10 * time.Millisecond) hs.Stop() // 停止扫描 time.Sleep(100 * time.Millisecond) // Stop 后不应继续扫描触发回调 if callCount.Load() > 0 { t.Error("stopped service should not trigger more timeouts") } }