12f038618e
Constructors seed only healthy hosts; an already-degraded/stale host is left unseeded so the first Check() emits once (cooldown dedups on hub restart). Born-degraded red-proof + staleness test updated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
94 lines
2.6 KiB
Go
94 lines
2.6 KiB
Go
package monitor
|
||
|
||
import (
|
||
"database/sql"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
|
||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
// backdate sets a host's last_report_at to N minutes ago, simulating the passage
|
||
// of time without sleeping. Uses a second connection (the checker reads via store).
|
||
func backdate(t *testing.T, db *sql.DB, hostID string, minutesAgo int) {
|
||
t.Helper()
|
||
if _, err := db.Exec(`UPDATE hosts SET last_report_at = datetime('now', ?) WHERE host_id = ?`,
|
||
fmt.Sprintf("-%d minutes", minutesAgo), hostID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
func TestHostStalenessChecker(t *testing.T) {
|
||
path := filepath.Join(t.TempDir(), "test.db")
|
||
st, err := store.New(path, log.New(io.Discard, "", 0))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer st.Close()
|
||
db, _ := sql.Open("sqlite", path)
|
||
defer db.Close()
|
||
|
||
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ck", RetrievalPassword: "p"})
|
||
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "k1"})
|
||
st.SaveHostReport("h1", "c1", []byte(`{}`), store.HostReportDenorm{}) // sets last_report_at
|
||
|
||
var events []string
|
||
onEvent := func(customerID, eventType, severity, message, detailsJSON, source string) {
|
||
events = append(events, eventType)
|
||
}
|
||
|
||
// F2: already-stale (40m) at construction → left UNSEEDED (not seeded "stale"), NO event on init.
|
||
backdate(t, db, "h1", 40)
|
||
sc := NewHostStalenessChecker(st, 30*time.Minute, onEvent, log.New(io.Discard, "", 0))
|
||
if len(events) != 0 {
|
||
t.Fatalf("construction must not emit, got %v", events)
|
||
}
|
||
if sc.GetState("h1") != "unknown" {
|
||
t.Fatalf("born-stale must be left unseeded, GetState = %q want unknown", sc.GetState("h1"))
|
||
}
|
||
|
||
// F2: the FIRST Check on a born-stale host emits host_stale once (persistent-down alerting).
|
||
sc.Check()
|
||
if len(events) != 1 || last(events) != "host_stale" {
|
||
t.Fatalf("born-stale first Check → one host_stale, got %v", events)
|
||
}
|
||
// Steady stale → no duplicate.
|
||
sc.Check()
|
||
if len(events) != 1 {
|
||
t.Fatalf("steady stale must not re-emit, got %v", events)
|
||
}
|
||
|
||
// Fresh report → host_recovered.
|
||
backdate(t, db, "h1", 2)
|
||
sc.Check()
|
||
if last(events) != "host_recovered" {
|
||
t.Fatalf("events = %v, want last host_recovered", events)
|
||
}
|
||
|
||
// Aged to stale → host_stale.
|
||
backdate(t, db, "h1", 40)
|
||
sc.Check()
|
||
if last(events) != "host_stale" {
|
||
t.Fatalf("events = %v, want last host_stale", events)
|
||
}
|
||
|
||
// Aged past 2× → host_down.
|
||
backdate(t, db, "h1", 130)
|
||
sc.Check()
|
||
if last(events) != "host_down" {
|
||
t.Fatalf("events = %v, want last host_down", events)
|
||
}
|
||
}
|
||
|
||
func last(s []string) string {
|
||
if len(s) == 0 {
|
||
return ""
|
||
}
|
||
return s[len(s)-1]
|
||
}
|