用户(转发 zcode 的信给我):「它说的它发的这个邮件你收到了吗?它说没有收到回复……」—— 问题不在那封信,而在它引用的时间:用户看到的「2026-09-15 01:23:19」其实是 UTC, 本地时间应为 09:23:19(本机 UTC+8)。 根因:quoteBody 用 `m.CreatedAt.Format(...)` 直接格式化(库里存 UTC),而界面各处都转本地; 同一族的正确写法在 scheduler/calendar.go 里(`e.EventTime.Local().Format(...)`)。 ★ 顺带发现一条**旧断言一直在保护这个 bug**:TestQuoteBodyPrefixesEveryLine 里写死了 「2026-09-02 10:30:00」(fixture 是 time.UTC)——已改为按本地计算,不再写死字面量 (与前几天 nav-merge.test.mjs 那条同一形态:断言钉着错的意图)。 判据:新增 TestQuoteBodyTimeIsLocal(引用时间必须等于 CreateAt.Local() 渲染,且不得等于 UTC 原样)。变异:去掉 .Local() → 2 条判据变红(新加的 + 改过的旧断言);复原 → 包内全绿。 注:该判据在 TZ=UTC 的机器上是空转的(本地==UTC),本机 UTC+8 会真红。 ▲ 部署未完成:/opt/agentmail 现在对我不可写(touch 都被拒;uid/能力问题,08:57 之后出现), 加上一个 08:57 留下的陈旧 /opt/agentmail/.deploy.lock 挡在 redeploy-gateway.sh 前面。 需要能写那里的人跑:rm -f /opt/agentmail/.deploy.lock && bash deploy/redeploy-gateway.sh
105 lines
3.8 KiB
Go
105 lines
3.8 KiB
Go
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)
|
||
}
|
||
}
|