Files
MailUI4Agents/server/internal/handler/toworkspace_test.go
JianFeeeee a696b2a141 fix(handler): 回信的 to_workspace 从会话 workspace 继承 —— 修「每封邮件多一条会话」
# 现象(生产实测)

在 DSH 界面上观察到的:每处理一封邮件就多出一条独立会话。

# 根因

`to_workspace` 是插件唯一能知道「这个任务该在哪个目录干活」的入口,而它取的是
**地址里的 path 位**。Agent 之间的回信、以及人在对话页点回复,地址里通常没有
path 位 —— 平台下发的 `reply_address` 就是这个形状(`FormatAddress(replyTo, "", alias)`)。

空着传下去的后果是可观测的:插件只能自己拼一个临时目录,于是**每封邮件落在一个
不同的空目录**里;DSH / opencode 按 cwd 给会话分组,界面上就成了「每处理一封邮件
就多出一条未分组会话」,而模型在那个空目录里什么项目文件也看不到。

实测取证:
  - 线上 5 个兜底目录 `~/.dsh/mail-sessions/mail-*` **全部是空的**(0 条目)
  - 全天 journalctl 里**没有任何**相关告警(代码用的是 `ctx.logger.warn`,
    而同一文件别处明确写着 DSH 的 logger 不进 journalctl)→ 完全静默
  - 走兜底的那条会话(8e982e96)里,`dsh → opencode` 那封 `to_workspace` 有值,
    而 `opencode → dsh` 的回信 **to_workspace 全为空** —— 而该会话自身的
    `sessions.workspace` 一直是有值的

# 修法

会话的 workspace 才是权威来源(见 models.SessionWorkspace 的注释):回信本来就是
回给**那条会话**的,而那条会话知道自己属于哪个项目。规则抽成纯函数
`resolveToWorkspace(addrPath, sessionWorkspace, toIsHuman)`:

  1. 地址里写了 path → 照用(人的明确意图优先)
  2. 没写且收件方是**人** → 保持空(人没有工作目录;填了前端会拼出
     `gui-lab@/path.别名` 这种错地址,ToHuman 字段就是为此加的)
  3. 没写且收件方是 Agent → 用会话的 workspace

**改的是 `to`,不是只改建库那一行**:同一个值还进投递载荷(`to_workspace` /
`self_address`)。改一处另一处不改,会出现「API 读到的与插件推到的不是同一个
目录」——那正是本项目一直在治的静默不一致。

`notify/mail.go` 只加了一段注释说明 reply_address 的 path 位为何**刻意留空**
(它的语义是「**发件人**该在哪儿干活」),免得后人以为那是漏填。

# 测试

- `handler/toworkspace_test.go`:6 条规则用例 + 1 条**反向对照**
  (固定其他输入只翻转 toIsHuman,要求结果必须不同 —— 防止该参数被忽略后
  「给人也填 path」静默回归)
- `repo/session_workspace_test.go`:锁住**列名与真实 schema**。这个查询读不到时
  按设计返回空串,与「这条会话没有工作目录」无法区分 → 列名写错的功能表现是
  「看起来还在跑,只是工作目录永远继承不到」
2026-09-12 11:19:19 +08:00

94 lines
3.4 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 "testing"
// resolveToWorkspace 的判据。
//
// 每一条都对着一个真实后果,不是凑覆盖率:
// - Agent 间回信丢 path → 收信方落进一次性空目录(界面上每封多一条会话)
// - 给人类填 path → 界面拼出 `gui-lab@/root/proj.别名` 这种错地址
// - 地址里明确写了 path → 必须照用(人的意图优先于兜底)
func TestResolveToWorkspace(t *testing.T) {
const ws = "/root/projects/demo"
cases := []struct {
name string
addrPath string
sessionWS string
toIsHuman bool
want string
why string
}{
{
name: "地址里没写 path收件方是 Agent → 继承会话 workspace",
addrPath: "", sessionWS: ws, toIsHuman: false,
want: ws,
why: "这是 Agent 间回信与人点回复的常态;不继承就会落在一次性空目录里",
},
{
name: "地址里没写 path收件方是人 → 保持空",
addrPath: "", sessionWS: ws, toIsHuman: true,
want: "",
why: "给人类填 path 会拼出 gui-lab@/root/projects/demo.别名 这种错地址",
},
{
name: "地址里写了 path → 照用,不用会话 workspace",
addrPath: "/other/place", sessionWS: ws, toIsHuman: false,
want: "/other/place",
why: "地址里的 path 是发件人的明确意图,兜底不该覆盖它",
},
{
name: "地址里写了 path即使收件方是人 → 照用",
addrPath: "/other/place", sessionWS: ws, toIsHuman: true,
want: "/other/place",
why: "人自己写了 path 就按他写的来(与上一条同一个原则)",
},
{
name: "会话确实没有 workspace收件方是 Agent → 仍是空",
addrPath: "", sessionWS: "", toIsHuman: false,
want: "",
why: "空串的语义是「不知道」,不能凭空编一个目录出来",
},
{
name: "会话没有 workspace收件方是人 → 空",
addrPath: "", sessionWS: "", toIsHuman: true,
want: "",
why: "两边都空,行为与改动前完全一致(无回归)",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := resolveToWorkspace(c.addrPath, c.sessionWS, c.toIsHuman)
if got != c.want {
t.Errorf("resolveToWorkspace(%q, %q, human=%v) = %q期望 %q —— %s",
c.addrPath, c.sessionWS, c.toIsHuman, got, c.want, c.why)
}
})
}
}
// 反向对照:确认「收件方是否人类」这个维度**真的在起作用**。
//
// 如果哪天有人把 toIsHuman 用错(比如传反了、或当成常量传),上面的表里
// 仍会有一半用例通过 —— 它们只在 want 恰好相同的情况下碰到。这里固定其他
// 输入、只翻转这一个维度,要求结果必须不同:
//
// 不翻转就通过,说明这个参数被忽略了(恒为默认、或被短路掉),
// 那时「给人也填 path」的错地址会静默回归。
func TestResolveToWorkspace_HumanDimensionActuallyMatters(t *testing.T) {
const ws = "/root/projects/demo"
agent := resolveToWorkspace("", ws, false)
human := resolveToWorkspace("", ws, true)
if agent == human {
t.Fatalf("收件方是人类与是 Agent 得到了同一个结果 %q —— "+
"要么 toIsHuman 没起作用,要么规则退化成了一律填/一律不填", agent)
}
if human != "" {
t.Errorf("收件方是人类时 to_workspace 应为空,实得 %q", human)
}
if agent != ws {
t.Errorf("收件方是 Agent 时应继承会话 workspace %q实得 %q", ws, agent)
}
}