Files
MailUI4Agents/server/internal/handler/forward_test.go
JianFeeeee a8d6811cd7 style(server): gofmt 注释规范 + init.sql 补 parent_session_id + 新增 ratelimit 判据
## 主体是 gofmt

`server/` 下 20 个文件的改动里,**142/147 行是注释与缩进** ——
Go 1.19+ 的 gofmt 把块注释里的 `//   x` 规范成 `//\tx`。
功能零变化。

## 一处实质:init.sql 补 parent_session_id

`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS parent_session_id
 TEXT REFERENCES sessions(session_id)`

远端已在 `db/migrate.go` 的 `sqliteAddColumns` 结构迁移通道里加了同一列;
init.sql 是**全新库**的建库脚本,缺这列会让新库少一个字段。

- `IF NOT EXISTS` ⇒ 与结构迁移通道幂等,两者并存无害
- 默认空 = 根节点;**不据历史数据猜父子**(猜错会让线索拓扑失真,比没有更糟)

## 新增 handler/ratelimit_test.go(162 行)

速率限制的判据。此前这一块没有独立判据。

## 验证(本机实跑,非推断)

    go build ./...                              通过
    go vet ./internal/{handler,repo,db,sse}/   通过
    go test -count=1(handler/repo/db/sse/blob)
      handler  41.3s  ok
      repo    101.9s  ok
      db       1.1s  ok
      sse      0.01s ok
      blob     0.08s ok

`-count=1` 是刻意的:第一遍跑出来全是 `(cached)`,那种绿灯不算数。
2026-10-05 09:26:59 +08:00

134 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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, "", "") // 这条用例的 fixture 里 from_workspace 本身是真路径,走退化路径
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)
}
}
// TestQuoteBodySenderAddressNeverNameAtName —— 转发引用里的「转发自」必须是个**地址**。
//
// 2026-09-15 用户报:「这个 zcode@zcode 转发生成的地址绝对有问题,原本是 zcode@/home…」
// 成因:发信路径给 Agent 存的 from_workspace 是 **agent 名**(zcode/homeagent/opencode 全如此),
// 而引用块按 `name@from_workspace` 拼 ⇒ zcode@zcode。
// 判据钉两条:① 用**会话**的 workspace/alias 拼出 `name@path.session`;
//
// ② path 与 name 相同时也**不许**渲染成 name@name(宁可只写名字)。
func TestQuoteBodySenderAddressNeverNameAtName(t *testing.T) {
legacy := &models.Mail{FromName: "zcode", FromWorkspace: "zcode", Subject: "打个招呼", CreatedAt: time.Now()}
got := quoteBody(legacy, "/home", "zcode-打个招呼")
if !strings.Contains(got, "> **转发自** zcode@/home.zcode-打个招呼") {
t.Fatalf("转发自应为 name@path.session,实际:\n%s", got)
}
if strings.Contains(got, "zcode@zcode") {
t.Fatalf("又拼出了 name@name(那不是地址):\n%s", got)
}
// 退化路径:没有会话信息、且 from_workspace 就是 agent 名 → 只写名字,不编地址
if got := quoteBody(legacy, "", ""); strings.Contains(got, "zcode@zcode") {
t.Fatalf("退化时不该出现 name@name:\n%s", got)
}
// 空 path 但别名在:平台形式是 name@.alias
pure := &models.Mail{FromName: "jianf", Subject: "x", CreatedAt: time.Now()}
if got := quoteBody(pure, "", "会话别名"); !strings.Contains(got, "> **转发自** jianf@.会话别名") {
t.Fatalf("空 path 时应为 name@.alias,实际:\n%s", got)
}
}