Асиметрія: аварія о 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>
101 lines
5.1 KiB
Go
101 lines
5.1 KiB
Go
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)
|
||
}
|
||
}
|