package notify import ( "go/ast" "go/parser" "go/token" "testing" ) /* * `in_reply_to` 的**方向判据**:`docs/DEBTS.json` 的 `in-reply-to-ignores-direction`。 * * # 这个文件为什么是「读源码」而不是「跑行为」 * * 缺陷的形状是**载荷里少了一个字段**(父邮件的发件人),而载荷由 * `mailToEvent` 之类的事件装配函数拼出。直接跑行为可以断言「某封信的 * payload 里有 parent_from」,但那要求先在 repo 里造出「父邮件由别人发出」 * 这串数据 —— 而**造那串数据的前提正是这个字段已经存在**,于是写不出 * 一个不预设修法的红灯。 * * 于是改成**按形状断言源码**:判据只问「判 parent_from 的那个值, * 是不是从已经读进内存的父邮件上取的」。 * * ★ 这条判据**当前是红的**,它就是那笔债的判据。判据先写好、修完转绿。 */ // parentFromOwner 是我们要的形状:它必须**从父邮件上取发件人**。 // 只钉住「有一个这样的判据」,不钉住它叫什么名字 —— 名字不该成为债的一部分。 func TestInReplyToCarriesParentSender(t *testing.T) { // 装配载荷的函数在哪:notify/mail.go 里的事件构造函数。 fset := token.NewFileSet() file, err := parser.ParseFile(fset, "mail.go", nil, parser.ParseComments) if err != nil { t.Fatalf("解析 mail.go 失败:%v", err) } /* * 载荷不是独立函数,而是 `Recipients` 内部的一个闭包 * (`payload := func(role, workspace, forName string) map[string]interface{}`), * 因为它要闭包住 reply_path / self_address 这几个按收件人现算的值。 * ⇒ 这里锚定 `Recipients`,不锚 `mailToEvent`(那个函数名是本文件 * 上一版的**臆测**,已删;`t.Fatal` 报的就是它)。 */ var payloadFn *ast.FuncDecl for _, d := range file.Decls { fd, ok := d.(*ast.FuncDecl) if ok && fd.Name.Name == "Recipients" { payloadFn = fd break } } if payloadFn == nil { t.Fatal("notify/mail.go 里找不到 Recipients —— " + "这条判据钉的是它(载荷是它内部的闭包),请同步更新本判据而不是删掉它") } // 判据:载荷里必须能区分「父邮件是我发的」与「父邮件是别人发的」。 // 按形状找:出现 parent 相关的发件人取值(parent_from / parentFrom …)。 found := false ast.Inspect(payloadFn, func(n ast.Node) bool { switch v := n.(type) { case *ast.BasicLit: if v.Kind == token.STRING { s := v.Value if contains(s, "parent_from") || contains(s, "parentFrom") { found = true } } case *ast.Ident: // 也接受「从父邮件结构体上取 FromName」这种不加新字段的落法: // 它满足同一个性质,且更便宜(数据本来就在手)。 if v.Name == "ParentFromName" || v.Name == "ParentFrom" { found = true } } return true }) if !found { t.Errorf(`载荷里没有父邮件发件人 ⇒ 插件无法判方向。 后果(生产已兜现,docs/DEBTS.json 的 in-reply-to-ignores-direction): 压测线索 stress-thread-21863-15348 里 8 封全是 opencode → pi, 投递通知却逐封宣称「回的是你那封:<上一封的 id>」—— 没有一封是 pi 发出的。 单向续信链同样满足「有父邮件」,于是纯单向的链被读成双向对话, Agent 把「收到」当新任务,客套到撞 hop 上限。 ★ 数据**已经在手**,不需要新查询: resolveTarget 的 reply_to 分支(internal/handler/mail.go:80-85) 已经把父邮件整行 GetMailByID 读进内存,只用了 SessionID 就丢掉; 而 models.Mail 上就有 FromName(internal/models/models.go:142)。 ⇒ 这不是"补一个字段",是"别把已经在手的数据扔掉"。 修法(任一,判据不预选): · resolveTarget 一并返回父邮件 FromName,填进 notify.Mail, 载荷带出 parent_from;或 · 直接复用已读到的父邮件的 FromName,不新增字段。 插件侧对读判据:client/electron/test/cross-bridge-prompt.test.mjs 第 5 条。`) } } func contains(s, sub string) bool { if len(sub) == 0 { return true } for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false }