Files
MailUI4Agents/server/internal
JianFeeeee 3459605dc0 修复: TestStaleLunarRecurringDoesNotFlood 是**日期相关的假红** —— 它要求一个不存在的日期
这条红不是"既有代码缺陷",是**测试的期望写错了**。`go test ./...` 现已 **rc=0 全绿**。

## 形状:断言要求「农历日原样保持」,而目标月可能根本没那一天

    if d := lunar.FromSolar(after.EventTime.In(time.Local)); d.Day != lunar.FromSolar(e.EventTime).Day

起点是 `time.Now().AddDate(-1,0,0)`,推进落到哪个农历月**随日期浮动**。
农历月 29/30 天不定 ⇒ 落到 29 天的月份时,"30 日"**不存在**,夹到 29 是
`ToSolar` 明确设计的行为(它连 `clamped` 都返回了)。旧断言却要求 30 ⇒ 必红。

实测(2026-09-21,起点 = 农历七月三十):
    落点 = 2026 年农历**八月廿九**,而该月只有 **29** 天
    ⇒ 正确结果就是 29;旧断言要 30 ⇒ 无论代码对不对都红。

**所以它是一条"日期相关"的假红**:每年那几天必红,与 `git bisect` 的结果无关。
pi 独立复核过它在**上次部署时的 HEAD `e8b260d`** 上同样 FAIL —— 与我一致。

## 我不是靠"看不见"修的:先排除了另外两种解释

- **不是时区/基准不一致**(我第一版猜这个):探针打印了两个基准,
  `e.EventTime` 未转本地 / 转本地,**农历日都是 30**,而左侧是 29 ⇒ 基准不是原因。
- 也不是推进逻辑多走了一步:`AddMonths(1)` 对 7月30 得 8月30(`Date` 只加月份),
  是**后面 `ToSolar` 往 29 天的月里落时才夹**。

## 修法:期望取 `min(原日, 目标月天数)`

    days, err := lunar.DaysInMonth(got.Year, got.Month)   // 读不到就 Fatal,不静默
    if want > days { want = days }

**没有放松真正的约束**:落到**长月**却少一天,仍然红。
变异验证:把 `RecurLunarMonthly` 的 `AddMonths(1)` 改成 `AddMonths(2)` ⇒
`农历日从 30 变成 29(目标月 30 天,期望 30)` **红**(在最终代码上重跑确认)。

## 由此**新发现**一条真缺陷(本次**不修**,因为要改产品语义)

顺着"夹取"往下量,发现 `addSolarMonthClamped` / 农历 `AddMonths` 的夹取是**粘的**:

    公历 每月31日:  1-31 → 2-28 → 3-28 → 4-28 …    (3 月有 31 天却停在 28)
    农历 每月30日:  7月30 → 8月29 → 9月29 → 10月29 …(9 月有 30 天却停在 29)

一旦被夹过一次,**此后再也回不到原始日**。而 `calendar.go:401-405` 的注释恰恰把
"一次溢出永久改变规则"称作**要避免的** bug —— **注释说的和实现对不上**。
根因:落点被写回 `event_time`,而**没有一列保存原始锚点日**(`calendar_events` 无此列)。
要真修得加锚点列 + 迁移,改的是**产品语义**,不是本次部署能顺带做的 ⇒ 已如实报给 pi。
2026-09-21 05:06:53 +08:00
..