Files
bee/audit/internal/webui/health_poller_test.go
T
Mikhail ChusavitinandClaude Sonnet 5 cc3997f7b1 app/webui: fix component-status.json multi-process clobber and unbounded growth
The long-lived bee-web process (writing PSU/kmsg watchdog records ~every
60s) and each short-lived "bee bee-worker" SAT-task subprocess each held
an independent in-memory copy of component-status.json. Whichever saved
last won outright, silently erasing whatever the other had just written —
e.g. a GPU SAT task's pcie:gpu:nvidia result vanishing the next time the
PSU watchdog ticked. ComponentStatusDB.Record now reloads on-disk state
(keyed by newer LastCheckedAt) before merging its own update.

Also stops re-logging identical repeat observations to History: the
ingest contract defines status_history as a transition log ("История
переходов статусов"), not a per-poll journal, but Record appended one
entry per call regardless — a continuously-polled PSU grew an unbounded
run of identical "still OK" entries. Now only appends when a source's
last recorded status for a key actually changes.

The PSU watchdog itself now backs off (60s -> doubling, capped at 30min)
while steady and resets to 60s the moment any PSU's status changes, cutting
ipmitool shellouts and file writes for a fleet that's been stable for a
while.

Also adds the missing "raid" case to the component-detail API (was
returning 404 for any RAID card's detail click on /topo).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 11:24:18 +03:00

55 lines
1.8 KiB
Go

package webui
import (
"path/filepath"
"testing"
"time"
"bee/audit/internal/app"
)
func TestNextHealthPollIntervalBacksOffAndResetsOnChange(t *testing.T) {
interval := healthPollIntervalMin
for i := 0; i < 10; i++ {
interval = nextHealthPollInterval(interval, false)
if interval > healthPollIntervalMax {
t.Fatalf("interval=%s exceeded cap %s after %d steady ticks", interval, healthPollIntervalMax, i+1)
}
}
if interval != healthPollIntervalMax {
t.Fatalf("interval=%s want cap %s after repeated steady ticks", interval, healthPollIntervalMax)
}
// A change resets straight back to the fast floor, regardless of how
// far the backoff had climbed.
if got := nextHealthPollInterval(interval, true); got != healthPollIntervalMin {
t.Fatalf("interval after change=%s want floor %s", got, healthPollIntervalMin)
}
}
func TestHealthPollerPollPSUUnavailableReturnsFalseWithoutPanic(t *testing.T) {
// ipmitool is not present in this test environment, so pollPSU must
// degrade to a no-op (false, no crash) rather than erroring out the
// poller loop.
db, err := app.OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
if err != nil {
t.Fatal(err)
}
p := newHealthPoller(db)
if p.interval != healthPollIntervalMin {
t.Fatalf("initial interval=%s want floor %s", p.interval, healthPollIntervalMin)
}
if got := p.pollPSU(); got {
t.Fatalf("pollPSU()=%v want false when ipmitool is unavailable", got)
}
}
func TestHealthPollIntervalBoundsAreSane(t *testing.T) {
if healthPollIntervalMin >= healthPollIntervalMax {
t.Fatalf("min %s must be less than max %s", healthPollIntervalMin, healthPollIntervalMax)
}
if healthPollIntervalMin != 60*time.Second {
t.Fatalf("min=%s want 60s (unchanged default poll rate)", healthPollIntervalMin)
}
}