Files
crossbar/docs/plans/v0/files/internal/health/health_test.go
T
kyleandClaude Fable 5.1 89e47d83f8 v0 plan: AGENTS.md, gate, five task files with given tests, run-plan driver
Tests were run against a private reference implementation: gate ok after every
task in order, smoke ok (stream spread ~1000 ms). The reference is not in the
repository.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 01:51:53 -07:00

155 lines
4.4 KiB
Go

package health_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"git.wntrmute.dev/kyle/crossbar/internal/health"
)
// fake is a llama-server stand-in whose /health can be flipped and whose model list is fixed.
type fake struct {
srv *httptest.Server
down atomic.Bool
models []string
hits atomic.Int32
}
func newFake(t *testing.T, models ...string) *fake {
f := &fake{models: models}
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
f.hits.Add(1)
if f.down.Load() {
http.Error(w, "loading", http.StatusServiceUnavailable)
return
}
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
mux.HandleFunc("/v1/models", func(w http.ResponseWriter, r *http.Request) {
type m struct {
ID string `json:"id"`
}
var data []m
for _, id := range f.models {
data = append(data, m{ID: id})
}
_ = json.NewEncoder(w).Encode(map[string]any{"object": "list", "data": data})
})
f.srv = httptest.NewServer(mux)
t.Cleanup(f.srv.Close)
return f
}
func TestFirstPollMakesHealthy(t *testing.T) {
a := newFake(t, "zeta", "alpha", "alpha")
tbl := health.New(map[string]string{"a": a.srv.URL}, time.Hour, nil)
if s, ok := tbl.Get("a"); !ok || s.Healthy || len(s.Loaded) != 0 {
t.Fatalf("before any poll: %+v %v", s, ok)
}
tbl.PollOnce(context.Background())
s, _ := tbl.Get("a")
if !s.Healthy || s.Consecutive != 1 || s.LastErr != "" || s.LastOK.IsZero() {
t.Errorf("after one good poll: %+v", s)
}
if len(s.Loaded) != 2 || s.Loaded[0] != "alpha" || s.Loaded[1] != "zeta" {
t.Errorf("Loaded = %v, want sorted, unique [alpha zeta]", s.Loaded)
}
}
func TestFailureThenRecoveryNeedsTwoPolls(t *testing.T) {
a := newFake(t, "m")
tbl := health.New(map[string]string{"a": a.srv.URL}, time.Hour, nil)
ctx := context.Background()
tbl.PollOnce(ctx)
a.down.Store(true)
tbl.PollOnce(ctx)
s, _ := tbl.Get("a")
if s.Healthy || s.Consecutive != 0 || s.LastErr == "" {
t.Fatalf("after failure: %+v", s)
}
if len(s.Loaded) != 1 {
t.Errorf("Loaded is left as last seen; got %v", s.Loaded)
}
a.down.Store(false)
tbl.PollOnce(ctx)
if s, _ := tbl.Get("a"); s.Healthy || s.Consecutive != 1 {
t.Errorf("one good poll after a failure must not be healthy yet: %+v", s)
}
tbl.PollOnce(ctx)
if s, _ := tbl.Get("a"); !s.Healthy || s.Consecutive != 2 || s.LastErr != "" {
t.Errorf("two good polls: %+v", s)
}
}
func TestMarkDown(t *testing.T) {
a := newFake(t, "m")
tbl := health.New(map[string]string{"a": a.srv.URL}, time.Hour, nil)
tbl.PollOnce(context.Background())
tbl.MarkDown("a", "connection refused")
s, _ := tbl.Get("a")
if s.Healthy || s.Consecutive != 0 || s.LastErr != "marked down: connection refused" {
t.Errorf("after MarkDown: %+v", s)
}
tbl.MarkDown("nobody", "x") // unknown hosts are ignored, not a panic
tbl.PollOnce(context.Background())
if s, _ := tbl.Get("a"); s.Healthy {
t.Errorf("one poll after MarkDown must not be healthy: %+v", s)
}
}
func TestUnreachableAndUnknown(t *testing.T) {
tbl := health.New(map[string]string{"a": "http://127.0.0.1:1"}, time.Hour, &http.Client{Timeout: time.Second})
tbl.PollOnce(context.Background())
s, ok := tbl.Get("a")
if !ok || s.Healthy || s.LastErr == "" {
t.Errorf("unreachable host: %+v %v", s, ok)
}
if _, ok := tbl.Get("zzz"); ok {
t.Errorf("unknown host must report ok=false")
}
}
func TestAllIsACopy(t *testing.T) {
a := newFake(t, "m")
tbl := health.New(map[string]string{"a": a.srv.URL}, time.Hour, nil)
tbl.PollOnce(context.Background())
all := tbl.All()
all["a"].Loaded[0] = "changed"
if s, _ := tbl.Get("a"); s.Loaded[0] != "m" {
t.Errorf("All must return copies")
}
if len(all) != 1 {
t.Errorf("All = %v", all)
}
}
func TestRunPollsOnStart(t *testing.T) {
a := newFake(t, "m")
tbl := health.New(map[string]string{"a": a.srv.URL}, 20*time.Millisecond, nil)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
go func() { tbl.Run(ctx); close(done) }()
deadline := time.Now().Add(2 * time.Second)
for a.hits.Load() < 3 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("Run did not return after cancel")
}
if a.hits.Load() < 3 {
t.Errorf("Run polled %d times in 2s at 20ms interval", a.hits.Load())
}
if s, _ := tbl.Get("a"); !s.Healthy {
t.Errorf("not healthy after Run: %+v", s)
}
}