package agentcore import ( "context" "fmt" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" "cloudipvalidator/internal/config" ) // While the agent is busy with a slow assignment (an address whose outbound // targets time out keeps it occupied for tens of seconds) it must keep sending // heartbeats; control-api marks a validator that stays silent for // heartbeat_timeout_seconds as unreachable. func TestHeartbeatContinuesDuringSlowChecks(t *testing.T) { slowTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3500 * time.Millisecond) })) defer slowTarget.Close() var heartbeats, assignments int32 mux := http.NewServeMux() mux.HandleFunc("POST /api/v1/agents/register", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"ok":true}`) }) mux.HandleFunc("POST /api/v1/agents/val-1/heartbeat", func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&heartbeats, 1) fmt.Fprint(w, `{"ok":true}`) }) mux.HandleFunc("GET /api/v1/agents/val-1/assignment", func(w http.ResponseWriter, r *http.Request) { if atomic.AddInt32(&assignments, 1) > 1 { w.WriteHeader(http.StatusNoContent) return } fmt.Fprintf(w, `{"ip_id":1,"ip_address":"1.1.1.1","phase":"checking","check_config":[{"type":"https","targets":[%q]}]}`, slowTarget.URL) }) mux.HandleFunc("POST /api/v1/agents/val-1/results", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"ok":true}`) }) mux.HandleFunc("POST /api/v1/agents/val-1/complete", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"ok":true}`) }) capi := httptest.NewServer(mux) defer capi.Close() a := New(&config.ValidatorAgent{ ValidatorID: "val-1", ControlAPIURL: capi.URL, PollIntervalSeconds: 1, Checks: config.AgentChecks{HTTPSTimeoutSeconds: 10, ICMPTimeoutSeconds: 1, ICMPCount: 1}, }, testLogger()) ctx, cancel := context.WithCancel(context.Background()) done := make(chan struct{}) go func() { _ = a.Run(ctx); close(done) }() time.Sleep(3 * time.Second) // the slow check (3.5 s) is still running during := atomic.LoadInt32(&heartbeats) cancel() select { case <-done: case <-time.After(10 * time.Second): t.Fatal("Run did not stop after the context was cancelled") } // One per second plus the first: 3-4 in 3 s. With heartbeats in the poll // loop there is exactly one, sent before the slow assignment started. if during < 3 { t.Fatalf("%d heartbeats in 3 s while a check was running, want at least 3", during) } if atomic.LoadInt32(&assignments) < 1 { t.Fatal("the assignment was never fetched, the test did not exercise a busy agent") } }