Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.
ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ
0058 подієві алерти: syslog, ncm, compliance спрацьовують у мить
події; правило з нереалізованим джерелом більше не зберігається
мовчки
0059 snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
описуються шаблоном, а не Go
0060 відкат конфігу: план як різниця, маскування паролів із підписом
плану, обов'язковий контрольний збір, verifying при обриві
0061 кнопки Telegram: довге опитування, авторизація не з callback_data
0062 аудит і архів хостів; тест на AST, що падає на ключі без назви
0063 RLS: три ролі, окремий пул для фонових тактів
0064 строки зберігання даних і сторінка сховища
0065 приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
переклад v1→v2 за RFC 3584 дає правильний OID
0066 ескалації сповіщень
0067 алерт про вичерпання диска
0068 поля заливки конфігу переїхали в каталог профілів
Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.
ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ
netpulse установник: одна команда замість 18 змінних і
593 рядків інструкції
RLS з першого запуску нова інсталяція під політиками одразу;
RLS-EXISTING-INSTALL.md лишається тільки для
старих інсталяцій
.forgejo + CI раннер не зареєстрований
Ці три перевірені компіляцією й міркуванням, але не виконанням.
ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ
Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.
Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.
Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
484 lines
21 KiB
Go
484 lines
21 KiB
Go
package store
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// Тести написані навколо однієї думки: зелений тест доводить рівно те,
|
||
// що він перевіряє. «Сходинка спрацювала» — найлегший і найменш цінний
|
||
// із можливих тестів ескалації, бо він нічого не каже про другу
|
||
// половину роботи. А друга половина — НЕ будити: підтверджений алерт,
|
||
// закритий алерт, заглушений хост, вікно обслуговування, протухла
|
||
// драбина. Кожен із цих випадків коштує чийогось сну (або чиєїсь
|
||
// аварії, яку ніхто не побачив), і кожен перевіряється нижче окремо.
|
||
|
||
// ladder — драбина «через 15 хв — черговий, через 45 — керівник».
|
||
func ladder() []EscalationStep {
|
||
return []EscalationStep{
|
||
{AfterMin: 15, ChannelIDs: []string{"ch-duty"}},
|
||
{AfterMin: 45, ChannelIDs: []string{"ch-lead"}},
|
||
}
|
||
}
|
||
|
||
// snap збирає знімок так, як його віддала б база.
|
||
func snap(state string, stepIdx int, mods ...func(*EscalationSnapshot)) EscalationSnapshot {
|
||
start := time.Date(2026, 8, 15, 2, 40, 0, 0, time.UTC)
|
||
s := EscalationSnapshot{
|
||
AlertID: "a1",
|
||
TenantID: "t1",
|
||
PolicyID: "p1",
|
||
PolicyName: "Нічне чергування",
|
||
StepIdx: stepIdx,
|
||
PassStart: start,
|
||
Deadline: start.Add(48 * time.Hour),
|
||
Steps: ladder(),
|
||
AlertState: state,
|
||
Alert: Alert{ID: "a1", State: state, Severity: "high", StartedAt: start},
|
||
}
|
||
for _, m := range mods {
|
||
m(&s)
|
||
}
|
||
return s
|
||
}
|
||
|
||
func at(base time.Time, min int) time.Time {
|
||
return base.Add(time.Duration(min) * time.Minute)
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Контроль: без цього тесту решта нічого не доводить
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Ніхто не підтвердив за 15 хвилин — будимо чергового.
|
||
func TestEscalationFiresWhenNobodyAcked(t *testing.T) {
|
||
s := snap("firing", 0)
|
||
d := PlanEscalation(s, at(s.PassStart, 15))
|
||
|
||
if d.Action != EscFire {
|
||
t.Fatalf("сходинка мала спрацювати, отримали %v (%s)", d.Action, d.Outcome)
|
||
}
|
||
if len(d.ChannelIDs) != 1 || d.ChannelIDs[0] != "ch-duty" {
|
||
t.Fatalf("не ті канали: %v", d.ChannelIDs)
|
||
}
|
||
if d.NextAt == nil {
|
||
t.Fatal("після першої сходинки має бути запланована друга")
|
||
}
|
||
if d.NextStepIdx != 1 {
|
||
t.Errorf("наступна сходинка мала стати 1, отримали %d", d.NextStepIdx)
|
||
}
|
||
if want := at(s.PassStart, 45); !d.NextAt.Equal(want) {
|
||
t.Errorf("друга сходинка о %s, очікували %s", d.NextAt, want)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Друга половина: коли сходинка НЕ має спрацювати
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Підтвердження зупиняє драбину. Це вся суть кнопки «Прийняти»: людина
|
||
// вже дивиться на аварію, і дзвінок керівникові за 15 хвилин повідомить
|
||
// його рівно про те, що вона й так робить.
|
||
func TestEscalationStopsOnAcknowledged(t *testing.T) {
|
||
d := PlanEscalation(snap("acknowledged", 0), time.Now())
|
||
|
||
if d.Action != EscStop {
|
||
t.Fatalf("підтверджений алерт не має ескалювати, отримали %v", d.Action)
|
||
}
|
||
if d.Outcome != "acked" {
|
||
t.Errorf("причина %q, очікували acked", d.Outcome)
|
||
}
|
||
if len(d.ChannelIDs) != 0 {
|
||
t.Errorf("зупинена драбина не має каналів, отримали %v", d.ChannelIDs)
|
||
}
|
||
if d.NextAt != nil {
|
||
t.Error("після зупинки наступної сходинки бути не може")
|
||
}
|
||
}
|
||
|
||
// Закритий алерт ескалація воскресити не може.
|
||
//
|
||
// Найдорожча помилка з можливих: аварію полагодили о 02:45, а о 03:10
|
||
// драбина піднімає керівника через алерт, якого вже немає. Саме тому
|
||
// стан читається перед КОЖНОЮ сходинкою, а не один раз при взведенні.
|
||
func TestEscalationNeverResurrectsClosedAlert(t *testing.T) {
|
||
for _, st := range []string{"resolved", "expired", ""} {
|
||
d := PlanEscalation(snap(st, 0), time.Now())
|
||
if d.Action != EscStop || d.Outcome != "closed" {
|
||
t.Errorf("стан %q: отримали %v/%s, очікували зупинку closed", st, d.Action, d.Outcome)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Те саме, але на СЕРЕДИНІ драбини — окремим тестом навмисно.
|
||
//
|
||
// Перевірка «на початку» проходить і в реалізації, яка дивиться на стан
|
||
// лише при взведенні. Ця — ні.
|
||
func TestEscalationChecksStateBeforeEveryStep(t *testing.T) {
|
||
// Перша сходинка вже пішла (step_idx = 1), алерт тим часом закрили.
|
||
d := PlanEscalation(snap("resolved", 1), time.Now())
|
||
if d.Action != EscStop || d.Outcome != "closed" {
|
||
t.Fatalf("друга сходинка закритого алерту: %v/%s", d.Action, d.Outcome)
|
||
}
|
||
|
||
// І те саме для підтвердження на середині драбини.
|
||
d = PlanEscalation(snap("acknowledged", 1), time.Now())
|
||
if d.Action != EscStop || d.Outcome != "acked" {
|
||
t.Fatalf("друга сходинка підтвердженого алерту: %v/%s", d.Action, d.Outcome)
|
||
}
|
||
}
|
||
|
||
// Заглушений хост і вікно обслуговування зупиняють сходинку так само,
|
||
// як звичайне сповіщення.
|
||
func TestEscalationDoesNotFireWhileSuppressed(t *testing.T) {
|
||
for _, why := range []string{"mute", "maintenance", "topology"} {
|
||
s := snap("suppressed", 0, func(s *EscalationSnapshot) {
|
||
s.Alert.SuppressedBy = why
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 15))
|
||
|
||
if d.Action == EscFire {
|
||
t.Fatalf("%s: придушений алерт розбудив людину", why)
|
||
}
|
||
if d.Action != EscDefer || d.Outcome != "suppressed" {
|
||
t.Fatalf("%s: отримали %v/%s", why, d.Action, d.Outcome)
|
||
}
|
||
if len(d.ChannelIDs) != 0 {
|
||
t.Errorf("%s: у придушеної сходинки з'явились канали %v", why, d.ChannelIDs)
|
||
}
|
||
if !strings.Contains(d.Detail, why) {
|
||
t.Errorf("%s: причина придушення не потрапила в журнал: %q", why, d.Detail)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Придушення не ВИТРАЧАЄ сходинку.
|
||
//
|
||
// Різниця принципова й непомітна на тесті «не надіслали»: якщо сходинку
|
||
// списати, півгодинне вікно обслуговування тихо роззброїть драбину до
|
||
// кінця життя алерту — тобто дасть рівно ту мовчазну відмову, від якої
|
||
// ескалація й рятує.
|
||
func TestSuppressionDefersStepInsteadOfSpendingIt(t *testing.T) {
|
||
s := snap("suppressed", 0)
|
||
now := at(s.PassStart, 15)
|
||
d := PlanEscalation(s, now)
|
||
|
||
if d.NextStepIdx != 0 {
|
||
t.Fatalf("сходинку списано попри придушення: наступна %d", d.NextStepIdx)
|
||
}
|
||
if d.NextAt == nil {
|
||
t.Fatal("відкладена драбина має отримати час наступної перевірки")
|
||
}
|
||
if !d.NextAt.After(now) {
|
||
t.Errorf("перевірку призначено не в майбутнє: %s", d.NextAt)
|
||
}
|
||
|
||
// Вікно скінчилось — сходинка, що чекала, спрацьовує тією самою.
|
||
back := snap("firing", d.NextStepIdx)
|
||
fired := PlanEscalation(back, *d.NextAt)
|
||
if fired.Action != EscFire || fired.StepIdx != 0 {
|
||
t.Fatalf("після вікна мала піти та сама перша сходинка, отримали %v/%d",
|
||
fired.Action, fired.StepIdx)
|
||
}
|
||
}
|
||
|
||
// Вічно заглушений алерт не має ходити по колу вічно.
|
||
func TestSuppressionStopsAtDeadline(t *testing.T) {
|
||
s := snap("suppressed", 0, func(s *EscalationSnapshot) {
|
||
s.Deadline = s.PassStart.Add(20 * time.Minute)
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 19))
|
||
|
||
if d.Action != EscStop || d.Outcome != "deadline" {
|
||
t.Fatalf("отримали %v/%s, очікували зупинку за стелею", d.Action, d.Outcome)
|
||
}
|
||
}
|
||
|
||
// Протухла драбина не будить нікого, навіть якщо алерт ще горить.
|
||
func TestEscalationStopsAtDeadline(t *testing.T) {
|
||
s := snap("firing", 0, func(s *EscalationSnapshot) {
|
||
s.Deadline = s.PassStart.Add(10 * time.Minute)
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 15))
|
||
|
||
if d.Action != EscStop || d.Outcome != "deadline" {
|
||
t.Fatalf("отримали %v/%s", d.Action, d.Outcome)
|
||
}
|
||
}
|
||
|
||
// Політику видалили, поки драбина йшла.
|
||
//
|
||
// Мовчки добивати за старою копією не можна: драбини, якої вже немає у
|
||
// формі, ніхто не знайде, коли питатиме «звідки дзвінок».
|
||
func TestEscalationStopsWhenPolicyDisappeared(t *testing.T) {
|
||
gone := snap("firing", 0, func(s *EscalationSnapshot) { s.Steps = nil })
|
||
if d := PlanEscalation(gone, time.Now()); d.Action != EscStop || d.Outcome != "no_policy" {
|
||
t.Fatalf("зникла політика: %v/%s", d.Action, d.Outcome)
|
||
}
|
||
|
||
// Політику скоротили — сходинки з таким номером більше немає.
|
||
short := snap("firing", 5)
|
||
if d := PlanEscalation(short, time.Now()); d.Action != EscStop || d.Outcome != "no_policy" {
|
||
t.Fatalf("скорочена політика: %v/%s", d.Action, d.Outcome)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Перезапуск процесу
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Рішення залежить ВИКЛЮЧНО від рядка в базі, а не від чогось у пам'яті.
|
||
//
|
||
// Це і є перевірка на перезапуск: «процес підняли заново» означає, що
|
||
// той самий знімок читається вдруге, і сходинка не має ані загубитись,
|
||
// ані піти двічі. Загубитись — бо рішення однакове; піти двічі — бо
|
||
// після запису стану знімок уже інший, і та сама сходинка з нього не
|
||
// відтворюється.
|
||
func TestEscalationSurvivesRestartMidLadder(t *testing.T) {
|
||
before := snap("firing", 0)
|
||
now := at(before.PassStart, 15)
|
||
|
||
first := PlanEscalation(before, now)
|
||
if first.Action != EscFire {
|
||
t.Fatal("перша сходинка мала спрацювати")
|
||
}
|
||
|
||
// Процес упав і піднявся, стан прочитано з бази заново — рішення те
|
||
// саме, бо в пам'яті нічого не було.
|
||
again := PlanEscalation(before, now)
|
||
if again.Action != first.Action || again.StepIdx != first.StepIdx ||
|
||
!equalTimes(again.NextAt, first.NextAt) {
|
||
t.Fatalf("рішення залежить від пам'яті процесу: %+v проти %+v", again, first)
|
||
}
|
||
|
||
// А після того, як стан записано (ApplyEscalation), знімок уже
|
||
// інший — і перша сходинка з нього не повториться.
|
||
after := snap("firing", first.NextStepIdx, func(s *EscalationSnapshot) {
|
||
s.RepeatIdx = first.NextRepeatIdx
|
||
s.PassStart = first.NextPassStart
|
||
})
|
||
second := PlanEscalation(after, *first.NextAt)
|
||
if second.Action != EscFire {
|
||
t.Fatalf("друга сходинка не спрацювала: %v/%s", second.Action, second.Outcome)
|
||
}
|
||
if second.StepIdx == first.StepIdx {
|
||
t.Fatal("після перезапуску повторилась та сама сходинка")
|
||
}
|
||
if second.ChannelIDs[0] != "ch-lead" {
|
||
t.Errorf("друга сходинка пішла не тому: %v", second.ChannelIDs)
|
||
}
|
||
}
|
||
|
||
func equalTimes(a, b *time.Time) bool {
|
||
if a == nil || b == nil {
|
||
return a == b
|
||
}
|
||
return a.Equal(*b)
|
||
}
|
||
|
||
// Драбина, яка чекала кінця вікна обслуговування, не має висипатись
|
||
// уся за одну хвилину: інакше після паузи будяться одразу всі.
|
||
func TestDelayedLadderKeepsSpacing(t *testing.T) {
|
||
s := snap("firing", 0)
|
||
// Перша сходинка спрацювала із запізненням на дві години.
|
||
late := at(s.PassStart, 135)
|
||
d := PlanEscalation(s, late)
|
||
|
||
if d.Action != EscFire || d.NextAt == nil {
|
||
t.Fatalf("очікували спрацювання з наступною сходинкою, отримали %v", d.Action)
|
||
}
|
||
// Проміжок між сходинками — 30 хв (15 → 45), він і має зберегтися.
|
||
if got := d.NextAt.Sub(late); got < 30*time.Minute {
|
||
t.Fatalf("наступна сходинка через %s після спізнілої — проміжок з'їдено", got)
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Повтори й подієві алерти
|
||
// ---------------------------------------------------------------------
|
||
|
||
// Метричний алерт: драбину можна пройти заново, бо саме існування
|
||
// алерту доводить, що умова досі виконується.
|
||
func TestMetricAlertRepeatsLadder(t *testing.T) {
|
||
s := snap("firing", 1, func(s *EscalationSnapshot) {
|
||
s.RepeatAfterMin = 60
|
||
s.MaxRepeats = 2
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 45))
|
||
|
||
if d.Action != EscFire {
|
||
t.Fatalf("остання сходинка мала спрацювати: %v", d.Action)
|
||
}
|
||
if d.NextAt == nil {
|
||
t.Fatal("після вичерпання драбини мав бути запланований повтор")
|
||
}
|
||
if d.NextRepeatIdx != 1 || d.NextStepIdx != 0 {
|
||
t.Fatalf("повтор має починати драбину спочатку: крок %d, повтор %d",
|
||
d.NextStepIdx, d.NextRepeatIdx)
|
||
}
|
||
// Повтор «через 60 хв» означає рівно 60, а не 60 + 15 хвилин першої
|
||
// сходинки: зсув відліку проходу має це врахувати.
|
||
want := at(s.PassStart, 45).Add(60 * time.Minute)
|
||
if !d.NextAt.Equal(want) {
|
||
t.Errorf("повтор о %s, очікували %s", d.NextAt, want)
|
||
}
|
||
if got := d.NextPassStart.Add(15 * time.Minute); !got.Equal(*d.NextAt) {
|
||
t.Errorf("відлік нового проходу зсунуто неправильно: %s", got)
|
||
}
|
||
}
|
||
|
||
// Стеля повторів дотримується.
|
||
func TestRepeatsStopAtMaxRepeats(t *testing.T) {
|
||
s := snap("firing", 1, func(s *EscalationSnapshot) {
|
||
s.RepeatAfterMin = 60
|
||
s.MaxRepeats = 2
|
||
s.RepeatIdx = 2 // два повтори вже зроблено
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 45))
|
||
|
||
if d.Action != EscFire {
|
||
t.Fatalf("остання сходинка мала піти: %v", d.Action)
|
||
}
|
||
if d.NextAt != nil {
|
||
t.Fatal("після вичерпання повторів драбина має зупинитись")
|
||
}
|
||
if d.Outcome != "done" {
|
||
t.Errorf("причина %q, очікували done", d.Outcome)
|
||
}
|
||
}
|
||
|
||
// Подієвий алерт (0058) проходить драбину рівно раз.
|
||
//
|
||
// Причина не в економії повідомлень. Метричний алерт живий рівно доти,
|
||
// доки виконується умова, і зникає сам — тому повтор є ставкою на те,
|
||
// що проблема триває. Подієвий алерт такого доказу не дає: рядок журналу
|
||
// стався один раз і «перестати ставатись» не може. Повторювати за ним
|
||
// драбину означало б будити всю зміну по колу через нічний блимок порту,
|
||
// який давно припинився.
|
||
func TestEventAlertLadderRunsOnce(t *testing.T) {
|
||
s := snap("firing", 1, func(s *EscalationSnapshot) {
|
||
s.IsEvent = true
|
||
s.RepeatAfterMin = 60
|
||
s.MaxRepeats = 3
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 45))
|
||
|
||
if d.Action != EscFire {
|
||
t.Fatalf("остання сходинка подієвого алерту мала піти: %v", d.Action)
|
||
}
|
||
if d.NextAt != nil {
|
||
t.Fatal("подієвий алерт не повторює драбину — повтор усе одно заплановано")
|
||
}
|
||
if !strings.Contains(d.Detail, "подієв") {
|
||
t.Errorf("журнал не пояснює, чому повтору немає: %q", d.Detail)
|
||
}
|
||
}
|
||
|
||
// Але сходинки в межах одного проходу подієвий алерт проходить як усі:
|
||
// «конфіг змінився на ядрі й ніхто не глянув» має піднімати наступного.
|
||
func TestEventAlertStillWalksItsLadder(t *testing.T) {
|
||
s := snap("firing", 0, func(s *EscalationSnapshot) { s.IsEvent = true })
|
||
d := PlanEscalation(s, at(s.PassStart, 15))
|
||
|
||
if d.Action != EscFire || d.NextStepIdx != 1 || d.NextAt == nil {
|
||
t.Fatalf("подієвий алерт має ескалюватись у межах проходу: %+v", d)
|
||
}
|
||
}
|
||
|
||
// Повтор, який не вміщається у стелю життя, не планується.
|
||
func TestRepeatNotScheduledPastDeadline(t *testing.T) {
|
||
s := snap("firing", 1, func(s *EscalationSnapshot) {
|
||
s.RepeatAfterMin = 60
|
||
s.MaxRepeats = 3
|
||
s.Deadline = s.PassStart.Add(50 * time.Minute)
|
||
})
|
||
d := PlanEscalation(s, at(s.PassStart, 45))
|
||
|
||
if d.Action != EscFire {
|
||
t.Fatalf("сходинка мала піти: %v", d.Action)
|
||
}
|
||
if d.NextAt != nil {
|
||
t.Fatal("повтор за межами стелі життя не має плануватись")
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------
|
||
// Перевірка драбини при збереженні
|
||
// ---------------------------------------------------------------------
|
||
|
||
func TestValidateEscalationStepsRejectsBrokenLadders(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
steps []EscalationStep
|
||
want string
|
||
}{
|
||
{"порожня", nil, "жодної сходинки"},
|
||
{
|
||
"нульова хвилина",
|
||
[]EscalationStep{{AfterMin: 0, ChannelIDs: []string{"c"}}},
|
||
"продублює",
|
||
},
|
||
{
|
||
"не зростає",
|
||
[]EscalationStep{
|
||
{AfterMin: 30, ChannelIDs: []string{"c"}},
|
||
{AfterMin: 30, ChannelIDs: []string{"c"}},
|
||
},
|
||
"вгору",
|
||
},
|
||
{
|
||
"сходинка без каналів",
|
||
[]EscalationStep{{AfterMin: 15}},
|
||
"жодного каналу",
|
||
},
|
||
{
|
||
"надто пізно",
|
||
[]EscalationStep{{AfterMin: 5000, ChannelIDs: []string{"c"}}},
|
||
"нагадування",
|
||
},
|
||
}
|
||
for _, c := range cases {
|
||
err := ValidateEscalationSteps(c.steps)
|
||
if err == nil {
|
||
t.Errorf("%s: драбину прийнято, а мало бути відхилено", c.name)
|
||
continue
|
||
}
|
||
if !strings.Contains(err.Error(), c.want) {
|
||
t.Errorf("%s: пояснення %q не містить %q", c.name, err, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestValidateEscalationStepsRejectsTooLongLadder(t *testing.T) {
|
||
var steps []EscalationStep
|
||
for i := 1; i <= MaxEscalationSteps+1; i++ {
|
||
steps = append(steps, EscalationStep{AfterMin: i * 5, ChannelIDs: []string{"c"}})
|
||
}
|
||
if err := ValidateEscalationSteps(steps); err == nil {
|
||
t.Fatal("драбина понад стелю має відхилятись")
|
||
}
|
||
}
|
||
|
||
func TestValidateEscalationStepsAcceptsSaneLadder(t *testing.T) {
|
||
if err := ValidateEscalationSteps(ladder()); err != nil {
|
||
t.Fatalf("нормальну драбину відхилено: %v", err)
|
||
}
|
||
}
|
||
|
||
// Стеля життя драбини має накривати саму драбину з усіма повторами —
|
||
// інакше вона обривала б ескалацію раніше, ніж та встигла спрацювати.
|
||
func TestEscalationSpanCoversRepeats(t *testing.T) {
|
||
p := EscalationPolicy{Steps: ladder(), RepeatAfterMin: 60, MaxRepeats: 2}
|
||
|
||
withRepeats := escalationSpan(p, false)
|
||
if withRepeats <= 45*time.Minute {
|
||
t.Fatalf("стеля %s не накриває навіть один прохід із повторами", withRepeats)
|
||
}
|
||
|
||
// Подієвий алерт повторів не має — і стеля в нього коротша.
|
||
if ev := escalationSpan(p, true); ev >= withRepeats {
|
||
t.Fatalf("подієвий алерт не повторюється, а стеля така сама: %s проти %s",
|
||
ev, withRepeats)
|
||
}
|
||
}
|