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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user