Files
iotlcm/internal/notify/alertstate_test.go
T
MiFaZhan 5732bf3384 chore: 初始开源发布
iotlcm-bot:独立飞书应用机器人(读取 Worker 终态 Run JSON,渲染并发送全量同步结果卡片)。

- Go module:git.5iot.com/iotlcm/iotlcm-bot → git.5iot.com/iotlcm-public/iotlcm-bot
- 飞书凭据占位:appId/appSecret/chatId → ${FEISHU_APP_ID}/${FEISHU_APP_SECRET}/${FEISHU_CHAT_ID}
- 移除内部资产:CI 触发器
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → scratch)
- README 重写(命令/配置/挂载契约/常见问题),去掉内部环境路径
- 补 Apache-2.0 LICENSE,LF 行尾 + .gitattributes
2026-10-22 16:14:24 +08:00

108 lines
3.3 KiB
Go

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")
}