package handler import ( "strings" "testing" "time" "github.com/agentmail/gateway/internal/models" "github.com/google/uuid" ) // 转发主题不能无限叠加 Fwd: 前缀,否则转发几轮后主题栏全是前缀。 func TestForwardSubject(t *testing.T) { cases := []struct{ custom, original, want string }{ {"", "修复登录态", "Fwd: 修复登录态"}, {"", "Fwd: 修复登录态", "Fwd: 修复登录态"}, // 已有前缀不再叠加 {"自定义主题", "修复登录态", "自定义主题"}, {" ", "修复登录态", "Fwd: 修复登录态"}, // 全空白视为未指定 } for _, c := range cases { if got := forwardSubject(c.custom, c.original); got != c.want { t.Errorf("forwardSubject(%q, %q) = %q, want %q", c.custom, c.original, got, c.want) } } } // 引用块必须逐行加 "> ":原文含代码块或列表时, // 只有逐行前缀才能在任何 Markdown 渲染器里保持引用语义。 func TestQuoteBodyPrefixesEveryLine(t *testing.T) { m := &models.Mail{ ID: uuid.New(), FromName: "opencode", FromWorkspace: "/root", Subject: "巡检结果", Body: "第一行\n\n```go\nfmt.Println(1)\n```\n- 列表项", CreatedAt: time.Date(2026, 9, 2, 10, 30, 0, 0, time.UTC), CCList: []models.Address{ {Name: "pi", Path: "root", Raw: "pi@root.new"}, }, } out := quoteBody(m) for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") { if line == "---" || line == "" { continue } if !strings.HasPrefix(line, ">") { t.Errorf("引用块出现未加前缀的行: %q", line) } } // 元信息必须齐全,否则收件人不知道这封转发的来路 // ★ 2026-09-15:这里原先写死 "2026-09-02 10:30:00"(UTC 原样),而 fixture 是 time.UTC —— // 等于这条断言一直在**保护那个 bug**(引用块显示 UTC 时间)。时间一项按本地算,不写死字面量。 for _, want := range []string{"opencode@/root", "巡检结果", m.CreatedAt.Local().Format("2006-01-02 15:04:05"), "pi@root.new"} { if !strings.Contains(out, want) { t.Errorf("引用块缺少 %q\n%s", want, out) } } // 原文正文本身要在引用里 if !strings.Contains(out, "> fmt.Println(1)") { t.Errorf("原文代码行未被引用:\n%s", out) } } // 无抄送时不该渲染出空的「抄送」行。 func TestQuoteBodyOmitsEmptyCC(t *testing.T) { m := &models.Mail{ FromName: "admin", Subject: "x", Body: "y", CreatedAt: time.Now(), } if strings.Contains(quoteBody(m), "抄送") { t.Error("无抄送时不应出现「抄送」行") } } // TestQuoteBodyTimeIsLocal —— 转发引用里的「时间」必须是**本地**时间,不是 UTC 原样。 // // 2026-09-15 用户报:引用块显示 01:23:19,而实际发生时间是 09:23:19(本机 UTC+8)。 // 库里存 UTC、界面各处都转本地,只有 quoteBody 漏了 .Local()。 // // ★ 这条判据在 **TZ=UTC 的机器上是空转的**(本地 == UTC,两种写法同结果)—— // 本机是 UTC+8,所以这里真会红。CI 若在 UTC 下跑,它是"只能验形态"的那一类, // 形态判据另有一条:不得与 UTC 原样相等(当两者不同时)。 func TestQuoteBodyTimeIsLocal(t *testing.T) { // 取一个"本地与 UTC 不同"的瞬间来测(否则本判据无从区分) m := &models.Mail{ FromName: "zcode", Subject: "时间判定", CreatedAt: time.Date(2026, 9, 15, 1, 23, 19, 0, time.UTC), } got := quoteBody(m) want := m.CreatedAt.Local().Format("2006-01-02 15:04:05") if !strings.Contains(got, "> **时间** "+want) { t.Fatalf("引用里的时间不是本地时间:想要 %q,实际渲染:\n%s", want, got) } if u := m.CreatedAt.UTC().Format("2006-01-02 15:04:05"); u != want && strings.Contains(got, "> **时间** "+u) { t.Fatalf("引用里出现的是 UTC 原样 %q(应为本地 %q)", u, want) } }