Files
MailUI4Agents/server/internal/notify/parent_direction_test.go
JianFeeeee f1c74fc4ce test(网关): 载荷必须带父邮件发件人 —— 并更正我说它"要新增查询"是错的
## 新增 server/internal/notify/parent_direction_test.go(当前**故意红**)

`docs/DEBTS.json` 的 `in-reply-to-ignores-direction` 的**数据层**判据,
与插件侧 `cross-bridge-prompt.test.mjs` 第 5 条配对(一条钉服务端、
一条钉四个桥的读法,两头都红才算这条债被完整挡住)。

## ★ 更正:上一条 commit(018d5b3)里我说错了一处

我在那里面写「修法:服务端补 `parent_from` 字段**更便宜**,不用多一次
往返」—— 方向对,但**没查证就下了结论**,而且把成本说满了。
现已回读确认,实际比那更便宜:

`resolveTarget` 的 `reply_to` 分支(`internal/handler/mail.go:80-85`)
**已经把父邮件整行 `repo.GetMailByID` 读进内存**(`mail` 变量),
只用了它的 `SessionID` 就把它丢掉;而 `models.Mail` 上就有 `FromName`
(`internal/models/models.go:142`)。

⇒ 判方向所需的**全部数据已经在函数里**,不需要新查询、不需要新 join、
不需要改表。**这不是"补一个字段",是"别把已经在手的数据扔掉"。**
已在 DEBTS 的 note 里留下更正,不静默改口(与 aab92f17 同一个教训:
说过的话要能在记录里看到被改掉)。

## 为什么这条判据是「读源码」而不是「跑行为」

缺陷形状是**载荷少一个字段**。直接跑行为可以断言"payload 里有
parent_from",但那要求先在 repo 里造出「父邮件由别人发出」的数据 ——
而造那串数据的前提正是这个字段已经存在 ⇒ **写不出一个不预设修法的红灯**。
故改为按形状断言源码(AST):判据钉 `Recipients`(载荷是它内部的闭包),
找有没有从父邮件取发件人的取值。

★ 这条判据自己踩了一次同类坑并已修:初版锚的是 `mailToEvent`,
那是我**臆测的函数名**,真机上直接报"找不到"。现已改锚 `Recipients`,
且 `t.Fatal` 的文案明确要求"同步更新判据而不是删掉它" ——
不能因为重构改了函数名就让判据悄悄失去锚点(那正是 `044a664` 的形状:
注释说判据在,而它其实没钉住任何东西)。

## 验证:判据确实有牙(不是空判)

未修 → 红(报"载荷里没有父邮件发件人");
模拟加一行 `"parent_from"` 到载荷 → **转绿**;随即完整还原,
`git diff` 对 `notify/mail.go` 为空(已复验)。
★ 第一次模拟时我写成 `m.ParentFrom`(结构体没这个字段)⇒ 编译失败,
  那是模拟没写对、不是判据的问题;改成字面量再验,绿。

## 全量

`GOCACHE=.tmp/gocache go test ./...`:除本条**故意红**的 notify 外全绿
(repo 1.3s / handler 12s / sse / sse 等 16 包)。
⚠ 默认 `GOCACHE=/root/.cache/go-build` 权限被拒,须显式指定。

## 共享工作树实况(`shared-workspace-unserialized-deploy` 正在发生)

本次 `git status` 看到 `server/internal/repo/zz_toctou_probe_test.go`
与 `zz_proposedfix_probe_test.go` 两个**不属于我**的未跟踪文件
(opencode 的 throwaway probe,同一包内 `go test ./internal/repo/` 仍绿)。
⇒ 本 commit **只 stage 我这两个文件**,那两个探针原样留在工作树里未动。
2026-09-28 10:18:43 +08:00

114 lines
4.3 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 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
}