// SPDX-License-Identifier: GPL-3.0-or-later package rootcheck import ( "context" "sync/atomic" "testing" "time" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/config" "codeberg.org/anassaeneroi/enodia-sentinal/go-agent/internal/model" ) func TestManagerRunsAtMostOneBoundedCheckAndDrainsResults(t *testing.T) { cfg := config.Default() cfg.RootcheckInterval = time.Hour manager := New(cfg) started := make(chan struct{}) release := make(chan struct{}) var calls atomic.Int32 var sawDeadline atomic.Bool manager.Run = func(ctx context.Context, state model.State) []model.Alert { _, ok := ctx.Deadline() sawDeadline.Store(ok) calls.Add(1) close(started) <-release return []model.Alert{{SID: SIDHiddenModule, Key: "rk:test", PIDs: []int{}}} } now := time.Date(2026, 7, 22, 12, 0, 0, 0, time.UTC) manager.MaybeStart(now, model.State{}) <-started manager.MaybeStart(now.Add(2*time.Hour), model.State{}) if got := calls.Load(); got != 1 { t.Fatalf("concurrent checks = %d, want 1", got) } close(release) for deadline := time.Now().Add(time.Second); time.Now().Before(deadline); { if alerts := manager.Drain(); len(alerts) == 1 { if !sawDeadline.Load() { t.Fatal("rootcheck did not receive a bounded context") } if alerts[0].SID != SIDHiddenModule { t.Fatalf("alerts = %+v", alerts) } return } time.Sleep(time.Millisecond) } t.Fatal("completed rootcheck result was not drained") } func TestDisabledManagerNeverSchedules(t *testing.T) { cfg := config.Default() cfg.RootcheckEnabled = false manager := New(cfg) var calls atomic.Int32 manager.Run = func(context.Context, model.State) []model.Alert { calls.Add(1); return nil } manager.MaybeStart(time.Now(), model.State{}) if calls.Load() != 0 { t.Fatal("disabled rootcheck scheduled work") } }