package store import ( "testing" "time" ) // Тиха година 22:00–08:00 за Києвом, щодня. func nightSchedule() *RouteSchedule { var s RouteSchedule s.TZ = "Europe/Kyiv" s.Quiet = append(s.Quiet, struct { Days []int `json:"days"` From string `json:"from"` To string `json:"to"` }{From: "22:00", To: "08:00"}) return &s } func quietSnap(respect bool, severity string) EscalationSnapshot { kyiv, _ := time.LoadLocation("Europe/Kyiv") start := time.Date(2026, 8, 28, 21, 0, 0, 0, kyiv) return EscalationSnapshot{ AlertID: "a1", TenantID: "t1", PolicyID: "p1", AlertState: "firing", StepIdx: 0, PassStart: start, Deadline: start.Add(48 * time.Hour), Steps: []EscalationStep{{AfterMin: 15, ChannelIDs: []string{"c1"}}}, RespectQuietHours: respect, Schedule: nightSchedule(), Alert: Alert{Severity: severity, State: "firing"}, } } // Кабінет сказав «драбина вночі мовчить» — сходинка чекає ранку. // // І головне: вона НЕ витрачається. Списана сходинка означала б, що одна // ніч тихо роззброює драбину до кінця життя алерту, тобто рівно та // мовчазна відмова, від якої ескалація й рятує. func TestQuietHoursDeferStepWhenRespected(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") night := time.Date(2026, 8, 28, 23, 30, 0, 0, kyiv) d := PlanEscalation(quietSnap(true, "high"), night) if d.Action != EscDefer { t.Fatalf("сходинка мала відкластись, а не %v (%s)", d.Action, d.Outcome) } if d.Outcome != "quiet_hours" { t.Errorf("причина відкладання нечитабельна: %q", d.Outcome) } if d.NextStepIdx != 0 { t.Errorf("сходинку витрачено: наступна %d замість 0", d.NextStepIdx) } if d.NextAt == nil || !d.NextAt.After(night) { t.Error("немає часу повторної перевірки — драбина застрягне") } } // Той самий кабінет удень: тиха година скінчилась, драбина йде. func TestQuietHoursDoNotHoldByDay(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") day := time.Date(2026, 8, 28, 11, 0, 0, 0, kyiv) if d := PlanEscalation(quietSnap(true, "high"), day); d.Action != EscFire { t.Fatalf("удень сходинка мала спрацювати: %v/%s", d.Action, d.Outcome) } } // Кабінет НЕ просив зважати на тиху годину — драбина пробивається. // // Це типове значення й поведінка, що діяла досі: оновлення не має тихо // роззброїти нічні дзвінки в тих, у кого вони працюють. func TestQuietHoursIgnoredByDefault(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") night := time.Date(2026, 8, 28, 23, 30, 0, 0, kyiv) if d := PlanEscalation(quietSnap(false, "high"), night); d.Action != EscFire { t.Fatalf("драбина без прапорця мала пробитись: %v/%s", d.Action, d.Outcome) } } // Disaster будить попри все — так само, як у першому сповіщенні. // // Одне слово в двох місцях мусить означати одне: targets() уже пропускає // disaster крізь тиху годину маршруту й правила. func TestDisasterBreaksThroughQuietHours(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") night := time.Date(2026, 8, 28, 23, 30, 0, 0, kyiv) if d := PlanEscalation(quietSnap(true, "disaster"), night); d.Action != EscFire { t.Fatalf("disaster мав пробитись крізь тиху годину: %v/%s", d.Action, d.Outcome) } } // Драбина, яку ніч не пускала аж до стелі життя, зупиняється — а не // відкладається вічно. func TestQuietHoursCannotOutliveDeadline(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") night := time.Date(2026, 8, 28, 23, 30, 0, 0, kyiv) s := quietSnap(true, "high") s.Deadline = night.Add(time.Minute) d := PlanEscalation(s, night) if d.Action != EscStop || d.Outcome != "deadline" { t.Fatalf("драбина мала протухнути, а не %v/%s", d.Action, d.Outcome) } } // Розкладу немає — тиха година не може тримати те, чого не описано. func TestNoScheduleNoHold(t *testing.T) { kyiv, _ := time.LoadLocation("Europe/Kyiv") night := time.Date(2026, 8, 28, 23, 30, 0, 0, kyiv) s := quietSnap(true, "high") s.Schedule = nil if d := PlanEscalation(s, night); d.Action != EscFire { t.Fatalf("без розкладу сходинка мала спрацювати: %v/%s", d.Action, d.Outcome) } }