package notify import ( "context" "os" "path/filepath" "testing" "time" ) func TestAlertStateSurvivesRestart(t *testing.T) { dir := t.TempDir() now := time.Now() // 首次:没有任何记录 → 应该告警 first := loadAlertState(dir) if !first.shouldAlert("dir gone", now, DegradedAlertInterval) { t.Fatal("first alert must be sent") } first.LastDegradedAlert = now first.LastDegradedReason = "dir gone" if err := saveAlertState(dir, first); err != nil { t.Fatal(err) } // 模拟进程退出后重启:重新从磁盘加载 reloaded := loadAlertState(dir) if reloaded.shouldAlert("dir gone", now.Add(time.Second), DegradedAlertInterval) { t.Fatal("after restart the same failure must NOT alert again immediately (this is the alert-flood bug)") } // 间隔到了才允许再报 if !reloaded.shouldAlert("dir gone", now.Add(DegradedAlertInterval+time.Second), DegradedAlertInterval) { t.Fatal("alert must be sent again after the interval elapses") } // 故障原因变化时立即告警 if !reloaded.shouldAlert("permission denied", now.Add(time.Second), DegradedAlertInterval) { t.Fatal("a changed failure reason must alert immediately") } } func TestExitCountIsPersistedAndIncremented(t *testing.T) { dir := t.TempDir() st := loadAlertState(dir) if st.ExitCount != 0 { t.Fatalf("fresh state exitCount=%d want 0", st.ExitCount) } st.ExitCount++ if err := saveAlertState(dir, st); err != nil { t.Fatal(err) } // 重启后计数必须延续,否则无法识别「反复重启」 if got := loadAlertState(dir).ExitCount; got != 1 { t.Fatalf("exitCount after reload=%d want 1", got) } } // 关键回归:挂载失效 → 退出 → 重启后,同一次故障只能推一条飞书。 func TestWatchAlertsOnceAcrossRestarts(t *testing.T) { shortenExitDelay(t) if !ExitOnDegraded { t.Skip("ExitOnDegraded disabled") } stateDir := t.TempDir() archiveDir := filepath.Join(t.TempDir(), "gone") fake := &fakeFeishu{} newSvc := func() Service { return Service{ archiveDir: archiveDir, environment: "test", stateDir: stateDir, pollInterval: 10 * time.Millisecond, store: newTestStore(stateDir), client: fake, logger: discardLogger(), } } // 第一次运行:应告警并退出 if err := newSvc().Watch(context.Background()); err == nil { t.Fatal("Watch must exit when the mount is degraded") } if fake.sent != 1 { t.Fatalf("first run must send exactly one alert, sent=%d", fake.sent) } // 模拟被 restart 策略拉起:新进程、新内存,但 state 目录相同 if err := newSvc().Watch(context.Background()); err == nil { t.Fatal("Watch must exit again") } if fake.sent != 1 { t.Fatalf("restart must NOT resend the same alert (alert flood), sent=%d", fake.sent) } // 退出计数应累计到 2 if got := loadAlertState(stateDir).ExitCount; got != 2 { t.Fatalf("exitCount=%d want 2", got) } } func TestAlertStateMissingDirIsNotFatal(t *testing.T) { // 状态目录不可写时不应影响告警逻辑(退化为每次都报,但至少不崩) st := loadAlertState("") if !st.shouldAlert("x", time.Now(), DegradedAlertInterval) { t.Fatal("empty state must allow alerting") } if err := saveAlertState("", st); err != nil { t.Fatalf("saving to empty state dir must be a no-op, got %v", err) } _ = os.Remove("nonexistent") }