package memory import ( "testing" "time" "yunyan/comm" "yunyan/pb" ) // 补偿要找的是「最近一个**已结束**的周期」,口径必须与 cron 的 tick 完全一致: // 差一周就会去补一个 cron 已经建过的周期(重复生成)或者永远差一格(永远补不上)。 func TestLastFinishedPeriods(t *testing.T) { loc := time.FixedZone("CST", 8*3600) cases := []struct { name string now time.Time wantWeek string // 周一~周日 wantMonth string }{ { name: "周中", now: time.Date(2026, 9, 18, 10, 0, 0, 0, loc), // 周五 wantWeek: "2026-09-07~2026-09-13", wantMonth: "2026-08-01~2026-08-31", }, { name: "周一 cron 触发那一刻", now: time.Date(2026, 9, 14, 0, 30, 0, 0, loc), wantWeek: "2026-09-07~2026-09-13", wantMonth: "2026-08-01~2026-08-31", }, { name: "周日(本周还没结束,仍看上一周)", now: time.Date(2026, 9, 20, 23, 59, 0, 0, loc), wantWeek: "2026-09-07~2026-09-13", wantMonth: "2026-08-01~2026-08-31", }, { name: "跨年:1 月 1 日的上个月是去年 12 月", now: time.Date(2026, 1, 1, 0, 30, 0, 0, loc), wantMonth: "2025-12-01~2025-12-31", }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if c.wantWeek != "" { w := lastFinishedWeek(c.now) if got := w.start + "~" + w.end; got != c.wantWeek { t.Errorf("上周范围应为 %s,得到 %s", c.wantWeek, got) } if w.ptype != comm.MemoryPeriodWeek { t.Errorf("周期类型应为 week,得到 %s", w.ptype) } } m := lastFinishedMonth(c.now) if got := m.start + "~" + m.end; got != c.wantMonth { t.Errorf("上月范围应为 %s,得到 %s", c.wantMonth, got) } if m.ptype != comm.MemoryPeriodMonth { t.Errorf("周期类型应为 month,得到 %s", m.ptype) } }) } } // 周一 00:30 的 cron 与 ensureRecent 必须算出同一个周期键, // 否则一个建单、另一个再建一次,同一周会有两份报告在跑。 func TestLastFinishedWeekMatchesCronKey(t *testing.T) { loc := time.FixedZone("CST", 8*3600) now := time.Date(2026, 9, 14, 0, 30, 0, 0, loc) // 周一,cron 触发时刻 cronKey := comm.MemoryWeekKey(now.AddDate(0, 0, -1)) if got := lastFinishedWeek(now).key; got != cronKey { t.Errorf("ensureRecent 算出 %s,cron 算出 %s,两者必须一致", got, cronKey) } } // 重试次数寄存在 error_msg 的 "retry=N;" 前缀里(不加列)。 // 计数丢了的后果是一份永远失败的报告可以无限重试,每次都烧一次 LLM。 func TestRetryCountRoundTrip(t *testing.T) { cases := []struct { msg string want int }{ {"", 0}, {"统计失败: connection refused", 0}, {"retry=1;统计失败", 1}, {"retry=3;统计失败", 3}, {"retry=;坏格式当没重试过", 0}, {"retry=abc;坏格式当没重试过", 0}, {"retry=2没有分号也当没重试过", 0}, } for _, c := range cases { if got := retryCount(c.msg); got != c.want { t.Errorf("retryCount(%q) = %d,期望 %d", c.msg, got, c.want) } } // 加计数要保住原来的错误正文,换计数不能越叠越长 got := withRetryCount("统计失败: x", 1) if got != "retry=1;统计失败: x" { t.Errorf("首次加计数得到 %q", got) } if got = withRetryCount(got, 2); got != "retry=2;统计失败: x" { t.Errorf("再次加计数得到 %q(正文应原样保留、前缀不叠加)", got) } } // fail 覆盖 error_msg 时必须保住已有计数——不然重试上限形同虚设。 func TestFailKeepsRetryCount(t *testing.T) { rec := &pb.DBMemoryReport{ErrorMsg: "retry=2;上一次的原因"} // 只验计数的保留逻辑本身,不走 fail(它要写库) msg := "这次的原因" if n := retryCount(rec.ErrorMsg); n > 0 { msg = withRetryCount(msg, n) } if retryCount(msg) != 2 { t.Errorf("覆盖错误正文后计数应仍为 2,得到 %q", msg) } }