package scheduler import ( "strings" "testing" "time" "github.com/agentmail/gateway/internal/models" ) func TestRenderReminder(t *testing.T) { at := time.Date(2026, 9, 4, 9, 30, 0, 0, time.Local) e := models.CalendarEvent{ Title: "每日站会", Description: "同步昨天进展与今天计划", EventTime: at, } t.Run("三个变量都替换", func(t *testing.T) { got := RenderReminder("日程提醒:{title}\n时间:{time}\n{description}", e) for _, want := range []string{"每日站会", "2026-09-04 09:30", "同步昨天进展与今天计划"} { if !strings.Contains(got, want) { t.Errorf("渲染结果缺 %q:\n%s", want, got) } } if strings.Contains(got, "{") { t.Errorf("仍有未替换的占位符:\n%s", got) } }) t.Run("同一变量出现多次全部替换", func(t *testing.T) { // ReplaceAll 而非 Replace:模板里写两遍 {title} 时 // 只替换第一处会让 Agent 收到一封半成品邮件。 got := RenderReminder("{title} —— 请开始 {title}", e) if strings.Contains(got, "{title}") { t.Errorf("第二处 {title} 未替换:%s", got) } }) t.Run("空模板不产生占位符残留", func(t *testing.T) { if got := RenderReminder("", e); got != "" { t.Errorf("空模板应渲染成空串,得到 %q", got) } }) t.Run("没有变量的模板原样返回", func(t *testing.T) { const plain = "该跑测试了" if got := RenderReminder(plain, e); got != plain { t.Errorf("纯文本模板应原样返回,得到 %q", got) } }) t.Run("描述为空时不留下空行以外的痕迹", func(t *testing.T) { e2 := e e2.Description = "" got := RenderReminder("{title}|{description}|", e2) if got != "每日站会||" { t.Errorf("空描述应替换成空串,得到 %q", got) } }) // {time} 必须是本地时间。DSN 带 _timezone=UTC,从库里读回的 EventTime // 是 UTC;不转本地就会把人在 +0800 输入的 14:30 写成 06:30, // 而前端预览用的是本地时间 —— 两边差 8 小时且都不报错。 t.Run("time 用本地时区而非 UTC", func(t *testing.T) { // 刻意构造一个 UTC 时刻(模拟从库里 Scan 出来的样子) utcEvent := models.CalendarEvent{ Title: "跨时区检查", EventTime: time.Date(2026, 9, 3, 6, 30, 0, 0, time.UTC), } got := RenderReminder("{time}", utcEvent) want := time.Date(2026, 9, 3, 6, 30, 0, 0, time.UTC).Local().Format("2006-01-02 15:04") if got != want { t.Errorf("{time} = %q,期望本地时间 %q", got, want) } }) // 同一时刻无论以哪个时区的 Location 传进来,渲染结果必须一致 —— // 它代表的是「墙上时钟的那一刻」,与 Location 的表示方式无关。 t.Run("同一时刻不同 Location 渲染一致", func(t *testing.T) { base := time.Date(2026, 9, 3, 6, 30, 0, 0, time.UTC) a := RenderReminder("{time}", models.CalendarEvent{EventTime: base}) b := RenderReminder("{time}", models.CalendarEvent{EventTime: base.Local()}) if a != b { t.Errorf("UTC 与 Local 表示同一时刻却渲染出不同结果:%q vs %q", a, b) } }) } func TestShort(t *testing.T) { // 日志里截前 8 位;短 id(测试里可能出现)不能 panic if got := short("0123456789abcdef"); got != "01234567" { t.Errorf("长 id 应截断成 8 位,得到 %q", got) } if got := short("abc"); got != "abc" { t.Errorf("短 id 应原样返回,得到 %q", got) } if got := short(""); got != "" { t.Errorf("空串应原样返回,得到 %q", got) } } func TestStartStopIdempotent(t *testing.T) { // Stop 在没启动时被调(defer 里必然发生)不该 panic; // Start 两次也不该泄漏 goroutine(第二次先停旧的)。 Stop() Start() Start() Stop() Stop() }