79ef715277
Finding the container costs no network call, so it now runs before the interval check. Red-proof RP-D1b. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
94 lines
3.7 KiB
Go
94 lines
3.7 KiB
Go
package stacks
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
)
|
|
|
|
// R-772 — a health probe that finds NO container to probe recorded `healthy: true` and then waited 5 minutes (measured
|
|
// 2026-10-01 on 9202, Karakeep with its container stopped). A record of a check that did not run must never say healthy
|
|
// (presence is not success), and the box must look again soon.
|
|
|
|
func r772Manager(now time.Time) *Manager {
|
|
m := &Manager{cfg: &config.Config{}, logger: log.New(io.Discard, "", 0), stacks: map[string]*Stack{}}
|
|
m.updateNowFn = func() time.Time { return now }
|
|
return m
|
|
}
|
|
|
|
func r772Stack() *Stack {
|
|
return &Stack{
|
|
Name: "karakeep",
|
|
State: StateDegraded,
|
|
Meta: Metadata{HealthCheck: &HealthCheckConfig{Interval: "5m",
|
|
Checks: []HealthCheckItem{{Type: "http", Port: 3000, Path: "/api/health"}}}},
|
|
// the app container stopped; the others still run — nothing the probe may use
|
|
Containers: []ContainerInfo{
|
|
{Name: "karakeep", State: StateExited},
|
|
{Name: "karakeep-meilisearch", State: StateRunning},
|
|
{Name: "karakeep-chrome", State: StateRunning},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestRunHealthProbes_NoContainerIsNotHealthy(t *testing.T) {
|
|
m := r772Manager(time.Now())
|
|
m.stacks["karakeep"] = r772Stack()
|
|
if err := m.RunHealthProbes(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hp := m.stacks["karakeep"].HealthProbe
|
|
if hp == nil {
|
|
t.Fatal("no record written")
|
|
}
|
|
if hp.Healthy || !hp.NotChecked || len(hp.Details) != 1 || hp.Details[0].Healthy || hp.Details[0].MessageKey != KeyHealthNoProbeContainer {
|
|
t.Fatalf("a check that did not run must read NOT healthy + not_checked, got %+v", hp)
|
|
}
|
|
if m.stacks["karakeep"].State != StateDegraded {
|
|
t.Fatalf("the record must not change the state the containers gave, got %s", m.stacks["karakeep"].State)
|
|
}
|
|
}
|
|
|
|
// The box looks again on the FAST cycle (10 s), not after the 5-minute interval: 20 s later a second run re-records.
|
|
// Pre-fix (healthy: true) the second run skipped it for 5 minutes.
|
|
func TestRunHealthProbes_NotCheckedIsLookedAtAgainSoon(t *testing.T) {
|
|
first := time.Now().Add(-20 * time.Second)
|
|
m := r772Manager(first)
|
|
m.stacks["karakeep"] = r772Stack()
|
|
_ = m.RunHealthProbes()
|
|
m.updateNowFn = func() time.Time { return time.Now() }
|
|
_ = m.RunHealthProbes()
|
|
if got := m.stacks["karakeep"].HealthProbe.LastCheck; !got.After(first) {
|
|
t.Fatalf("a not-checked app must be looked at again within the 10-second cycle; last check still %v", got)
|
|
}
|
|
}
|
|
|
|
// R-630 stays true: a not-checked record never paints a RUNNING app unhealthy; a probe that ran and failed does.
|
|
func TestProbeSaysUnhealthy_NotCheckedLeavesTheStateAlone(t *testing.T) {
|
|
if probeSaysUnhealthy(&HealthProbeResult{Healthy: false, NotChecked: true}) {
|
|
t.Fatal("not checked must not override the state")
|
|
}
|
|
if !probeSaysUnhealthy(&HealthProbeResult{Healthy: false}) {
|
|
t.Fatal("a failed check must override running → unhealthy")
|
|
}
|
|
if probeSaysUnhealthy(nil) || probeSaysUnhealthy(&HealthProbeResult{Healthy: true}) {
|
|
t.Fatal("nil / healthy must not override")
|
|
}
|
|
}
|
|
|
|
// Measured live on 9202 (v0.286.0): with a HEALTHY record 2 minutes old, stopping the probe's container left the record
|
|
// "healthy" until its 5-minute interval ran out. The missing container is now seen on the very next tick.
|
|
func TestRunHealthProbes_AStoppedContainerIsSeenAtOnce(t *testing.T) {
|
|
m := r772Manager(time.Now())
|
|
st := r772Stack()
|
|
st.HealthProbe = &HealthProbeResult{Healthy: true, LastCheck: time.Now().Add(-2 * time.Minute)}
|
|
m.stacks["karakeep"] = st
|
|
_ = m.RunHealthProbes()
|
|
if hp := m.stacks["karakeep"].HealthProbe; hp.Healthy || !hp.NotChecked {
|
|
t.Fatalf("a stopped probe container must be recorded not checked on the next tick, got %+v", hp)
|
|
}
|
|
}
|