Add web auth, infinite-scroll, agent admission and review fixes across agent/server/web

This commit is contained in:
Stanislav Rossovskii
2026-05-28 23:45:12 +04:00
parent 37b1a1d6d6
commit 1026e9ebbe
75 changed files with 3021 additions and 916 deletions

View File

@@ -2,6 +2,7 @@ package scheduler
import (
"context"
"strings"
"sync/atomic"
"testing"
"time"
@@ -9,6 +10,14 @@ import (
"github.com/monlet/agent/internal/config"
)
func runScheduler(ctx context.Context, agentID string, checks []config.CheckConfig, out chan<- Event, h Hooks) {
m := NewManager(ctx, agentID, out, h)
m.Update(checks)
<-ctx.Done()
m.Stop()
m.Wait()
}
func mkCheck(id, cmd string, interval, timeout time.Duration) config.CheckConfig {
return config.CheckConfig{
ID: id,
@@ -22,7 +31,7 @@ func TestEmitsEvents(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
out := make(chan Event, 10)
go Run(ctx, "a1", []config.CheckConfig{mkCheck("c1", "echo hi", 200*time.Millisecond, 100*time.Millisecond)}, out, Hooks{})
go runScheduler(ctx, "a1", []config.CheckConfig{mkCheck("c1", "echo hi", 200*time.Millisecond, 100*time.Millisecond)}, out, Hooks{})
select {
case ev := <-out:
if ev.CheckID != "c1" || ev.Status != "ok" {
@@ -43,9 +52,168 @@ func TestNoOverlapSkips(t *testing.T) {
var skipped int32
hooks := Hooks{OnSkipped: func(string) { atomic.AddInt32(&skipped, 1) }}
c := mkCheck("slow", "sleep 1", 100*time.Millisecond, 900*time.Millisecond)
go Run(ctx, "a1", []config.CheckConfig{c}, out, hooks)
go runScheduler(ctx, "a1", []config.CheckConfig{c}, out, hooks)
<-ctx.Done()
if atomic.LoadInt32(&skipped) == 0 {
t.Fatal("expected at least one skipped tick")
}
}
func TestReportsFullEventChannel(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out := make(chan Event, 1)
out <- Event{EventID: "occupied"}
var full int32
m := NewManager(ctx, "a1", out, Hooks{
OnEventChannelFull: func() { atomic.AddInt32(&full, 1) },
})
m.Update([]config.CheckConfig{mkCheck("c1", "echo hi", time.Hour, time.Second)})
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if atomic.LoadInt32(&full) > 0 {
break
}
time.Sleep(10 * time.Millisecond)
}
if atomic.LoadInt32(&full) == 0 {
t.Fatal("expected full channel hook")
}
<-out
select {
case ev := <-out:
if ev.CheckID != "c1" {
t.Fatalf("unexpected event: %+v", ev)
}
case <-ctx.Done():
t.Fatal("blocked event was not delivered")
}
m.Stop()
m.Wait()
}
func TestManagerReloadChangedCheckWaitsForRunning(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out := make(chan Event, 10)
m := NewManager(ctx, "a1", out, Hooks{})
m.Update([]config.CheckConfig{mkCheck("c1", "sleep 0.3; echo old", time.Second, time.Second)})
time.Sleep(50 * time.Millisecond)
m.Update([]config.CheckConfig{mkCheck("c1", "echo new", time.Second, time.Second)})
var first, second Event
select {
case first = <-out:
case <-ctx.Done():
t.Fatal("no first event")
}
select {
case second = <-out:
case <-ctx.Done():
t.Fatal("no second event")
}
m.Stop()
m.Wait()
if !strings.Contains(first.Output, "old") {
t.Fatalf("first output: %q", first.Output)
}
if !strings.Contains(second.Output, "new") {
t.Fatalf("second output: %q", second.Output)
}
}
func TestManagerReloadRemovedCheckLetsRunningFinish(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out := make(chan Event, 10)
stopped := make(chan string, 1)
m := NewManager(ctx, "a1", out, Hooks{OnStopped: func(id string) { stopped <- id }})
m.Update([]config.CheckConfig{mkCheck("c1", "sleep 0.2; echo old", time.Second, time.Second)})
time.Sleep(50 * time.Millisecond)
m.Update(nil)
select {
case ev := <-out:
if !strings.Contains(ev.Output, "old") {
t.Fatalf("output: %q", ev.Output)
}
case <-ctx.Done():
t.Fatal("removed running check did not finish")
}
select {
case id := <-stopped:
if id != "c1" {
t.Fatalf("stopped id: %q", id)
}
case <-ctx.Done():
t.Fatal("removed check did not stop")
}
m.Stop()
m.Wait()
}
func TestManagerRemoveThenReaddSameCheckDoesNotOverlap(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
out := make(chan Event, 10)
m := NewManager(ctx, "a1", out, Hooks{})
m.Update([]config.CheckConfig{mkCheck("c1", "sleep 0.25; echo old", time.Second, time.Second)})
time.Sleep(50 * time.Millisecond)
m.Update(nil)
m.Update([]config.CheckConfig{mkCheck("c1", "echo new", time.Second, time.Second)})
var first, second Event
select {
case first = <-out:
case <-ctx.Done():
t.Fatal("no first event")
}
select {
case second = <-out:
case <-ctx.Done():
t.Fatal("no second event")
}
m.Stop()
m.Wait()
if !strings.Contains(first.Output, "old") {
t.Fatalf("first output: %q", first.Output)
}
if !strings.Contains(second.Output, "new") {
t.Fatalf("second output: %q", second.Output)
}
}
func TestManagerUsesLatestPendingReloadAfterRunningFinishes(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
out := make(chan Event, 10)
m := NewManager(ctx, "a1", out, Hooks{})
m.Update([]config.CheckConfig{mkCheck("c1", "sleep 0.25; echo old", time.Second, time.Second)})
time.Sleep(50 * time.Millisecond)
m.Update([]config.CheckConfig{mkCheck("c1", "echo intermediate", time.Second, time.Second)})
m.Update([]config.CheckConfig{mkCheck("c1", "echo latest", time.Second, time.Second)})
var first, second Event
select {
case first = <-out:
case <-ctx.Done():
t.Fatal("no first event")
}
select {
case second = <-out:
case <-ctx.Done():
t.Fatal("no second event")
}
m.Stop()
m.Wait()
if !strings.Contains(first.Output, "old") {
t.Fatalf("first output: %q", first.Output)
}
if strings.Contains(second.Output, "intermediate") || !strings.Contains(second.Output, "latest") {
t.Fatalf("second output: %q", second.Output)
}
}