用户(指着转发出来的那封信):「这个 zcode@zcode 转发生成的地址绝对有问题,原本是 zcode@/home…」
成因是**两个错叠在一起**:
① 写入侧遗留:发信路径给 Agent 存 from_workspace 存的是 **agent 名**而不是路径
(实测近两天:zcode 10 封全是 'zcode'、homeagent 'homeagent'、opencode 'opencode';
pi 399 封有 4 种值、dsh 116 封有 3 种 —— 混着真实路径与自己的名字);
② 展示侧无脑拼 `name@from_workspace` ⇒ 两者一乘就是 `zcode@zcode`,
而**那永远不是一个地址**(地址是 name@path.session)。
被转发那封的会话工作区其实是 `/home`、别名 `zcode-打个招呼`,所以正确形式是
`zcode@/home.zcode-打个招呼`(用户说的"原本是 zcode@/home…"就是这个)。
改法:
- quoteBody 增加两个入参(会话 workspace/alias),转发处理器用
repo.SessionWorkspaceOf / SessionAliasOf 取(都是既有函数,空串语义是"不知道");
- 新增 senderAddress:优先用会话的 workspace+alias 拼 `name@path.session`
(空 path 时给平台形式 `name@.alias`);**path 与 name 相同时当"不知道"丢掉** ——
宁可只渲染 `zcode`,也不渲染一个看着像地址其实不是的东西。
判据:新增 TestQuoteBodySenderAddressNeverNameAtName(三条形态:正常地址 / 退化不许 name@name /
空 path 的 .alias 形式);旧的 TestQuoteBodyPrefixesEveryLine 改走退化路径以保持它原本的意图。
变异:去掉"拒绝 name@name"那段 → 红;复原 → 包内全绿。
▲ 仍未部署(与上一笔时间修复一起卡在同一个地方):/opt/agentmail 现在对我不可写
(touch 都被拒),且有 08:57 留下的 /opt/agentmail/.deploy.lock。
需要能写那儿的人:rm -f /opt/agentmail/.deploy.lock && bash deploy/redeploy-gateway.sh
132 lines
5.6 KiB
Go
132 lines
5.6 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, "", "") // 这条用例的 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)
|
||
}
|
||
}
|