hub(D53): held apps reach the operator — mirror, Held apps list (host + Hosts page), reminder after the grace
gates / gates (push) Successful in 1m58s
gates / gates (push) Successful in 1m58s
app_held / app_hold_cleared allow-listed and operator-only; handleEvent mirrors them in held_apps; HeldAppsChecker sends ONE app_held_reminder (warning, operator-only, per-app cooldown) after the grace per reason (6 h / 1 d / 3 d), then weekly; recorded before sending; stale rows not reminded; the hub never clears a hold. Test-only alerting.held_app_grace_override. TestD53_* in api/monitor/notify/web, red-proved by six mutations. Commit only.
This commit is contained in:
1 parent
ecd82abab8
commit
7e57e824a2
27 files changed
+935
-2
No files matched your search
@@ -0,0 +1,45 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
|
||||
)
|
||||
|
||||
// D53 (`08` §10): a box's app_held is accepted and mirrored (stack, reason, since); its daily repeat keeps ONE row; its
|
||||
// app_hold_cleared removes the row. Both types are allow-listed AND operator-only (the v0.78.0 pair, checked together).
|
||||
func TestD53_TheBoxsHeldAppsAreMirrored(t *testing.T) {
|
||||
h, st, _ := newTestHandler(t)
|
||||
for _, et := range []string{"app_held", "app_hold_cleared"} {
|
||||
if !allowedEventTypes[et] {
|
||||
t.Fatalf("%s is not allow-listed — POST /event would 400", et)
|
||||
}
|
||||
if !notify.IsOperatorOnly(et) {
|
||||
t.Fatalf("%s is not operator-only", et)
|
||||
}
|
||||
}
|
||||
if !notify.IsOperatorOnly("app_held_reminder") || allowedEventTypes["app_held_reminder"] {
|
||||
t.Fatal("app_held_reminder must be operator-only and hub-origin (a box must not post the reminder)")
|
||||
}
|
||||
held := `{"customer_id":"c1","event_type":"app_held","severity":"info","message":"App kimai is held",
|
||||
"details":{"stack_name":"kimai","reason":"unhealthy_stop","since":"2026-10-14T08:00:00Z","why":"stopped by the box"}}`
|
||||
for i := 0; i < 2; i++ { // the first report and the daily repeat
|
||||
if rr := do(h, "POST", "/event", globalKey, held); rr.Code != 200 {
|
||||
t.Fatalf("app_held: %d %s", rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
rows, err := st.ListHeldApps("c1")
|
||||
if err != nil || len(rows) != 1 {
|
||||
t.Fatalf("want one held row, got %+v (%v)", rows, err)
|
||||
}
|
||||
if r := rows[0]; r.Stack != "kimai" || r.Reason != "unhealthy_stop" || r.Since.Format("2006-01-02T15:04:05Z07:00") != "2026-10-14T08:00:00Z" || r.Why == "" {
|
||||
t.Fatalf("row: %+v", r)
|
||||
}
|
||||
cleared := `{"customer_id":"c1","event_type":"app_hold_cleared","severity":"info","message":"App kimai is no longer held","details":{"stack_name":"kimai"}}`
|
||||
if rr := do(h, "POST", "/event", globalKey, cleared); rr.Code != 200 {
|
||||
t.Fatalf("app_hold_cleared: %d", rr.Code)
|
||||
}
|
||||
if rows, _ := st.ListHeldApps("c1"); len(rows) != 0 {
|
||||
t.Fatalf("the cleared hold is still listed: %+v", rows)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user