|
|
|
@@ -11,6 +11,63 @@ import (
|
|
|
|
|
"github.com/kami/maven/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// downCand builds a service_down candidate the way a tick actually does it:
|
|
|
|
|
// one fact per kuma monitor under the prefix, carrying the source and value
|
|
|
|
|
// loop.DownServices checks. The old cand() shape wrote a single fact keyed
|
|
|
|
|
// plain "service_down", which mavpoll stopped producing, and that is why the
|
|
|
|
|
// tests passed through the whole of #534.
|
|
|
|
|
func downCand(names ...string) loop.Candidate {
|
|
|
|
|
now := time.Date(2026, 7, 31, 21, 40, 0, 0, time.UTC)
|
|
|
|
|
st := loop.State{Now: now, Facts: map[string]store.Fact{}}
|
|
|
|
|
for _, n := range names {
|
|
|
|
|
key := loop.ServiceDownPrefix + n
|
|
|
|
|
st.Facts[key] = store.Fact{
|
|
|
|
|
Key: key, Ts: now.Add(-3 * time.Minute),
|
|
|
|
|
Source: loop.ServiceDownSource, Value: `"down"`,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return loop.Candidate{
|
|
|
|
|
Rule: loop.Rule{Name: "service_down", Severity: loop.Sev4},
|
|
|
|
|
Severity: loop.Sev4, State: st,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The nudge he reads on telegram must name what broke. It is a sev4 that
|
|
|
|
|
// reaches him away from the box, so "a service is down" costs him a trip to
|
|
|
|
|
// kuma to learn anything at all.
|
|
|
|
|
func TestNudgeNamesTheDownService(t *testing.T) {
|
|
|
|
|
// Lowercased before matching: a name that opens the sentence is
|
|
|
|
|
// capitalized by capitalizeFirst, which is wanted.
|
|
|
|
|
nt := newTestTemplates(t, 5)
|
|
|
|
|
for i := 0; i < 40; i++ {
|
|
|
|
|
body, _ := nt.Nudge(downCand("paperless"))
|
|
|
|
|
if !strings.Contains(strings.ToLower(body), "paperless") {
|
|
|
|
|
t.Fatalf("body does not name the service: %q", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Two down: both named, in the key order the rule itself uses.
|
|
|
|
|
for i := 0; i < 40; i++ {
|
|
|
|
|
body, _ := nt.Nudge(downCand("nginx", "paperless"))
|
|
|
|
|
low := strings.ToLower(body)
|
|
|
|
|
if !strings.Contains(low, "nginx") || !strings.Contains(low, "paperless") {
|
|
|
|
|
t.Fatalf("body drops a service: %q", body)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Nothing down means no template fits, and the fallback answers rather than
|
|
|
|
|
// the picker inventing a name.
|
|
|
|
|
func TestNudgeServiceDownWithoutFacts(t *testing.T) {
|
|
|
|
|
nt := newTestTemplates(t, 5)
|
|
|
|
|
body, mood := nt.Nudge(downCand())
|
|
|
|
|
if body != "Сервис не отвечает." {
|
|
|
|
|
t.Fatalf("fallback body %q", body)
|
|
|
|
|
}
|
|
|
|
|
if mood != "neutral" {
|
|
|
|
|
t.Fatalf("mood %q", mood)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// cand builds a candidate the way a tick would.
|
|
|
|
|
func cand(rule string, sinceMin int, factKey string) loop.Candidate {
|
|
|
|
|
now := time.Date(2026, 7, 31, 21, 40, 0, 0, time.UTC)
|
|
|
|
@@ -46,8 +103,12 @@ func TestNudgeTemplatesLoad(t *testing.T) {
|
|
|
|
|
t.Errorf("%s: only %d variants", rule, len(set.Variants))
|
|
|
|
|
}
|
|
|
|
|
// Every rule needs one variant that needs no value, or a candidate
|
|
|
|
|
// without context has nothing to say. routine and morning are exempt:
|
|
|
|
|
// they always carry a name and must always say it.
|
|
|
|
|
// without context has nothing to say. routine, morning and
|
|
|
|
|
// service_down are exempt: they always carry a name and must always
|
|
|
|
|
// say it. service_down's predicate cannot fire without a down fact,
|
|
|
|
|
// so loop.DownServices always has something to fill {service} with,
|
|
|
|
|
// and the nameless variant it used to carry was the bug (#534) —
|
|
|
|
|
// {service} never filled, so that variant was the only fillable one.
|
|
|
|
|
plain := 0
|
|
|
|
|
seen := map[string]bool{}
|
|
|
|
|
for _, v := range set.Variants {
|
|
|
|
@@ -59,7 +120,7 @@ func TestNudgeTemplatesLoad(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
seen[v] = true
|
|
|
|
|
}
|
|
|
|
|
if plain == 0 && rule != "routine" && rule != "morning" {
|
|
|
|
|
if plain == 0 && rule != "routine" && rule != "morning" && rule != "service_down" {
|
|
|
|
|
t.Errorf("%s: every variant needs a placeholder value", rule)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -102,8 +163,8 @@ func TestNudgeNoLeftoverPlaceholders(t *testing.T) {
|
|
|
|
|
cand("water", 0, ""), // no duration
|
|
|
|
|
cand("water", 30, ""), // under an hour
|
|
|
|
|
cand("water", 200, ""), // hours
|
|
|
|
|
cand("service_down", 3, "vaultwarden"),
|
|
|
|
|
cand("service_down", 3, ""), // no service name
|
|
|
|
|
downCand("vaultwarden"),
|
|
|
|
|
downCand(), // nothing down: the fallback answers
|
|
|
|
|
cand("routine:таблетки", 0, ""),
|
|
|
|
|
cand("morning:утро", 0, ""),
|
|
|
|
|
cand("unknown_rule", 0, ""),
|
|
|
|
|