Files
mesh-catalog/modules/messenger/cmd/messenger/condition_test.go
T
jochen 0ae7933d54 Tell the operator what the mesh finds wrong, and watch the watcher (hq to-be 45 phase 1)
The mesh noticed 48 core failures in six days and told nobody (ADR 0227).
messenger holds the operator-channel seat: it consumes the controller's
condition events and sends them to Telegram and the desktop notifier,
deduplicated by key, reminded once, edited on clear, capped at 20 an hour
with the rest folded, and refusing anything carrying an address, a path or
a secret. mesh-watcher, on a machine other than the control node, sends to
Telegram directly when the self-check heartbeat or the bus goes silent.
2026-10-06 09:35:55 +02:00

100 lines
3.9 KiB
Go

package main
import (
"strings"
"testing"
"time"
)
// The contract with the controller's condition events (to-be 45 §2), as condition.go states it.
func TestTheEventsAreTheControllersSeat(t *testing.T) {
for key, want := range map[string]string{
"mesh-controller.condition-raised": EventRaised,
"mesh-controller.condition-changed": EventChanged,
"mesh-controller.condition-cleared": EventCleared,
} {
if got, ok := eventOf(key); !ok || got != want {
t.Errorf("%s: %q %v", key, got, ok)
}
}
for _, key := range []string{"mesh-controller.applied", "gitea.condition-raised", "condition-raised"} {
if _, ok := eventOf(key); ok {
t.Errorf("%s read as a condition event", key)
}
}
}
func TestAConditionAsTheStoreHoldsIt(t *testing.T) {
body := `{
"key": "machine.ace.silent", "kind": "silent",
"subject": {"scope": "machine", "id": "ace", "machine": "ace"},
"severity": "urgent", "summary": "the home server has not been heard for 15 min",
"evidence": [{"at": "2026-10-06T12:00:00Z", "what": "last heartbeat"}],
"source": "S1", "raised": "2026-10-06T12:15:00Z", "last-observed": "2026-10-06T12:16:00Z",
"observations": 3, "tried": [], "resolver": "self",
"silenced": {"until": "2026-10-06T14:00:00Z", "by": "operator", "why": "moving it"},
"epoch": 57
}`
c, err := DecodeCondition(EventRaised, []byte(body))
if err != nil {
t.Fatal(err)
}
if c.Key != "machine.ace.silent" || c.Kind != "silent" || c.Scope != "machine" || c.ID != "ace" ||
c.Severity != Urgent || c.Source != "S1" || c.Observations != 3 || c.Resolver != "self" {
t.Fatalf("%+v", c)
}
if !c.Raised.Equal(time.Date(2026, 10, 6, 12, 15, 0, 0, time.UTC)) || c.LastObserved.IsZero() {
t.Fatalf("times: %+v", c)
}
if !c.SilencedAt(time.Date(2026, 10, 6, 13, 0, 0, 0, time.UTC)) || c.SilencedAt(time.Date(2026, 10, 6, 15, 0, 0, 0, time.UTC)) {
t.Fatalf("silenced: %v", c.SilencedTill)
}
if c.SubjectWords() != "machine ace" {
t.Fatalf("subject words: %q", c.SubjectWords())
}
}
func TestOtherSpellingsAndAKeyMadeFromItsParts(t *testing.T) {
c, err := DecodeCondition(EventChanged, []byte(`{"kind":"stalled","subject":{"scope":"plan","id":"41"},
"severity":"warning","summary":"plan 41 waits","last_observed":"2026-10-06T12:00:00Z","silenced":null,
"source":{"row":"S3"}}`))
if err != nil {
t.Fatal(err)
}
if c.Key != "plan.41.stalled" || c.Source != "S3" || c.LastObserved.IsZero() || !c.SilencedTill.IsZero() {
t.Fatalf("%+v", c)
}
c, err = DecodeCondition(EventRaised, []byte(`{"key":"provider.keycloak.ace.gitea.failing","subject":"keycloak",
"severity":"warning","summary":"the identity provider fails gitea"}`))
if err != nil || c.Scope != "provider" || c.Kind != "failing" || c.ID != "keycloak" {
t.Fatalf("%+v %v", c, err)
}
}
func TestAClearedEventNeedsOnlyItsKey(t *testing.T) {
c, err := DecodeCondition(EventCleared, []byte(`{"key":"machine.ace.silent","cleared":"2026-10-06T12:30:00Z"}`))
if err != nil || c.Key != "machine.ace.silent" || c.Cleared.IsZero() {
t.Fatalf("%+v %v", c, err)
}
}
func TestWhatCannotBeReadIsRefusedByName(t *testing.T) {
for body, says := range map[string]string{
`not json`: "not a JSON object",
`null`: "null",
`{"severity":"urgent","summary":"x"}`: "no key",
`{"key":"a.b.c","summary":"x"}`: "no severity",
`{"key":"a.b.c","severity":"critical","summary":"x"}`: "neither urgent nor warning",
`{"key":"a.b.c","severity":"urgent"}`: "no summary",
`{"key":"a.b.c","severity":"urgent","summary":"x","raised":"yesterday"}`: "not an RFC 3339 time",
`{"key":7}`: "key is not a string",
`{"key":"a.b.c","severity":"urgent","summary":"x","silenced":{"until":"soon"}}`: "silenced.until",
} {
_, err := DecodeCondition(EventRaised, []byte(body))
if err == nil || !strings.Contains(err.Error(), says) {
t.Errorf("%s: %v, want %q", body, err, says)
}
}
}