Netpulse_SasS/server/internal/alerting/escalation_test.go
byrsapty ec4b2cd54b
All checks were successful
CI / hygiene (push) Successful in 10s
CI / web (push) Successful in 1m18s
CI / server (push) Successful in 1m54s
CI / agent (push) Successful in 1m1s
Тиха година й драбина: вада окремо, вибір окремо
Асиметрія: аварія о 21:59 ескалювала всю ніч, о 22:01 не ескалювала
ніколи. Дві хвилини різниці — протилежні наслідки, причому гірший
(повна тиша) виглядав як тиша справна.

ВАДА. targets() повертав порожньо в тиху годину, а взведення читало це
як «немає куди слати». Взводять лише новий алерт, тож драбина не
з'являлась уже ніколи: тиха година вимикала механізм саме тоді, коли
перше сповіщення не спрацювало. Тепер targets() розрізняє «каналів
немає» і «канали є, просто зараз ніч».

ВИБІР. 0072 додає respect_quiet_hours на драбину:
  false (типово, як діяло) — драбина пробивається;
  true  — сходинка відкладається до ранку і НЕ витрачається.
Залежить від того, чи є в кабінету нічна зміна — це вирішує кабінет.
disaster пробивається за будь-якого значення, як і в targets().

Відлік драбини — від першого сповіщення, а не від started_at: інакше
для розглушеного алерту вона протухла б ще у вікні.

І сам прогін проти бази брехав: dbtest.sh котив схему готовим образом
(старі міграції), а тести брав із нового дерева. Тепер міграції з того
ж дерева. 64 міграції, усе зелене.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 18:08:27 +03:00

101 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package alerting
import (
"strings"
"testing"
"time"
"github.com/netpulse/netpulse/server/internal/store"
)
// Взведення драбини перевіряється з боку «не взвели».
//
// Тест «драбину взведено» доводить мало: він проходить і в реалізації,
// яка взводить її завжди. Кожен із випадків нижче — це людина, яку
// система розбудила б о третій ночі без причини, або (у випадку з
// каналами) сповіщення, яке о нульовій хвилині свідомо не надіслали, а
// о п'ятнадцятій раптом надіслали б.
const policy = "p1"
func act() store.RuleAction {
return store.RuleAction{EscalationPolicyID: policy, Source: "icmp"}
}
// Контроль: нормальний випадок драбину взводить.
func TestEscalationArmedForFiringAlert(t *testing.T) {
if !shouldArmEscalation(store.Alert{State: "firing"}, act(), true, true) {
t.Fatal("алерт, про який справді повідомили, має отримати драбину")
}
}
// Правило без політики — типовий стан, і він має лишатись мовчазним.
//
// Це головна вимога до оновлення: жоден наявний кабінет не має раптом
// почати будити людей тільки тому, що з'явилась нова можливість.
func TestNoPolicyMeansNoEscalation(t *testing.T) {
empty := store.RuleAction{Source: "icmp"}
if shouldArmEscalation(store.Alert{State: "firing"}, empty, true, true) {
t.Fatal("правило без політики не має ескалювати")
}
// Правила взагалі немає (його видалили, алерт лишився).
if shouldArmEscalation(store.Alert{State: "firing"}, store.RuleAction{}, false, true) {
t.Fatal("алерт без правила ескалювати нікуди")
}
}
// Придушений алерт драбини не отримує: заглушення означає «не
// турбувати», а драбина — це рівно турбування, тільки за розкладом.
func TestSuppressedAlertIsNotArmed(t *testing.T) {
for _, st := range []string{"suppressed", "acknowledged", "resolved", "expired"} {
if shouldArmEscalation(store.Alert{State: st}, act(), true, true) {
t.Errorf("стан %q отримав драбину", st)
}
}
}
// Найтонший випадок: алерту не було куди піти.
//
// Каналів не знайшлось через поріг серйозності, вимкнений канал або
// порожній перелік. Взвести драбину означало б надіслати о 15-й хвилині
// те, що на нульовій ми свідомо не надсилали, — і людина прочитала б це
// як несправність. Нею це й було б.
//
// Тиха година сюди НЕ належить: там канали є, просто зараз ніч, і
// targets() повідомляє це окремо. Раніше вона потрапляла в цю ж гілку й
// роззброювала драбину назавжди — див. TestQuietHourStillArms.
func TestEscalationNotArmedWhenNothingWasSent(t *testing.T) {
if shouldArmEscalation(store.Alert{State: "firing"}, act(), true, false) {
t.Fatal("драбина взведена попри те, що перше сповіщення нікуди не пішло")
}
}
// ---------------------------------------------------------------------
// Шапка ескалації має з першого рядка пояснювати, чому це третє
// повідомлення про ту саму аварію. Без цього розбуджений керівник
// вимкне канал, вирішивши, що розсилка зациклилась.
func TestEscalationHeaderExplainsItself(t *testing.T) {
start := time.Now().Add(-45 * time.Minute)
snap := store.EscalationSnapshot{
PolicyName: "Нічне чергування",
Steps: []store.EscalationStep{
{AfterMin: 15}, {AfterMin: 45},
},
Alert: store.Alert{StartedAt: start},
}
head := escalationHeader(snap, store.EscalationDecision{StepIdx: 1})
for _, want := range []string{"ЕСКАЛАЦІЯ", "2/2", "Нічне чергування", "не підтверджено"} {
if !strings.Contains(head, want) {
t.Errorf("у шапці немає %q:\n%s", want, head)
}
}
// Повтор має бути видно окремо: «друга сходинка» і «друга сходинка
// втретє» — це різні новини для того, хто їх читає.
rep := escalationHeader(snap, store.EscalationDecision{StepIdx: 1, RepeatIdx: 2})
if !strings.Contains(rep, "повтор 2") {
t.Errorf("номер повтору не показано:\n%s", rep)
}
}