Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/health_test.go
T

110 lines
4.1 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/health"
)
// 1519/AC1: the manifest declares runtime-health {kind: tool, tool: mail_reader_health}.
func TestTheManifestDeclaresRuntimeHealth(t *testing.T) {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m struct {
RuntimeHealth struct {
Kind string `json:"kind"`
Tool string `json:"tool"`
} `json:"runtime-health"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.RuntimeHealth.Kind != "tool" || m.RuntimeHealth.Tool != HealthTool {
t.Fatalf("runtime-health = %+v, want kind tool, tool %s", m.RuntimeHealth, HealthTool)
}
}
// 1519/AC2: after a judgement refused, timed out or answered out of shape, mail_reader_health answers
// unhealthy with why naming which of the three, the judgement's own words, and since when; later failures
// keep the first since.
func TestRecordJudgementUnhealthyKeepsTheFirstSince(t *testing.T) {
first := time.Date(2026, 10, 20, 9, 30, 0, 0, time.UTC)
after := RecordJudgement(ReaderHealth{Healthy: true}, OutcomeRefused, "holds no licence", first)
if after.Healthy || after.Cause != OutcomeRefused || after.Why != "holds no licence" || !after.Since.Equal(first) {
t.Fatalf("after one refusal: %+v", after)
}
second := first.Add(5 * time.Minute)
again := RecordJudgement(after, OutcomeTimedOut, "no answer in 40s", second)
if again.Healthy || again.Cause != OutcomeTimedOut || again.Why != "no answer in 40s" {
t.Fatalf("after a second, different failure: %+v", again)
}
if !again.Since.Equal(first) {
t.Fatalf("since = %v, want the FIRST failure's own time %v, not the second's %v", again.Since, first, second)
}
a := again.Answer(time.UTC)
if a.Healthy || !strings.Contains(a.Why, "timed out") || !strings.Contains(a.Why, "no answer in 40s") ||
!strings.Contains(a.Why, "09:30") {
t.Fatalf("Answer().Why = %q, want it to name the cause, the judgement's own words, and since 09:30", a.Why)
}
}
// 1519/AC3: after a judgement that succeeds, it answers healthy.
func TestRecordJudgementSuccessIsHealthyAgain(t *testing.T) {
unhealthy := RecordJudgement(ReaderHealth{Healthy: true}, OutcomeOutOfShape, "not an object", time.Now())
healthy := RecordJudgement(unhealthy, OutcomeOK, "", time.Now())
if !healthy.Healthy || healthy.Cause != "" || !healthy.Since.IsZero() {
t.Fatalf("after a success: %+v", healthy)
}
a := healthy.Answer(time.UTC)
if a.State != health.StateHealthy {
t.Fatalf("Answer() = %+v, want healthy", a)
}
}
// 1519/AC4: the state is kept in the module's database, so a restart answers the same — proven here with the
// store's in-memory double (kit_test.go); the SQL itself (reader_health) is pgstore_test.go's own concern.
func TestHealthToolReadsTheKeptState(t *testing.T) {
store := newMemStore()
unhealthy := RecordJudgement(ReaderHealth{Healthy: true}, OutcomeRefused, "holds no licence", time.Now())
if err := store.SetReaderHealth(context.Background(), unhealthy); err != nil {
t.Fatal(err)
}
// A fresh Bundle, as a restarted process would build one, reading the same store.
b := &Bundle{
Caller: func(map[string]any) string { return "mesh-mail-web" },
callersOverride: func() (Callers, error) { return Callers{"mesh-mail-web"}, nil },
Store: func(context.Context) (Store, error) { return store, nil },
}
got, err := b.healthTool(map[string]any{})
if err != nil {
t.Fatal(err)
}
a, ok := got.(health.Answer)
if !ok || a.Healthy {
t.Fatalf("mail_reader_health after a restart = %+v, want the kept unhealthy state", got)
}
}
// 1519: reader_health's own table exists in the schema, with a column for each of the kept state's parts.
func TestTheSchemaHasAReaderHealthTable(t *testing.T) {
if !strings.Contains(consolidated, "create table reader_health") {
t.Fatal("schema.sql declares no reader_health table")
}
for _, column := range []string{"healthy", "cause", "why", "since"} {
if !strings.Contains(consolidated, column) {
t.Fatalf("schema.sql's reader_health has no column %q", column)
}
}
}