Files
MailUI4Agents/server/internal/repo/session_workspace_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

47 lines
1.8 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 repo
import (
"context"
"testing"
)
// SessionWorkspaceOf 必须真的读得到 `sessions.workspace`。
//
// 为什么值得单独立一条测试:这个查询是**列名对不上就静默返回空串**的形态
// (读不到时按设计返回 "",不报错)。空串在这里的语义是「这条会话没有工作
// 目录」,与「我查错列了」无法区分 —— 于是列名写错的表现是:功能看起来还在
// 跑只是工作目录永远继承不到Agent 继续落在一次性空目录里。
//
// 本轮演练里我刚因为猜错表名(`mail_attachments` 不存在)而崩过一次,
// 这类错误只能靠对着真实 schema 跑一次来发现。
func TestSessionWorkspaceOf(t *testing.T) {
ctx := context.Background()
setupTestDB(t)
sid, err := CreateSession(ctx, nil, "pi", "工作目录继承", "/root/projects/demo")
if err != nil {
t.Fatalf("建会话: %v", err)
}
got := SessionWorkspaceOf(ctx, sid)
if got != "/root/projects/demo" {
t.Fatalf("SessionWorkspaceOf = %q期望 /root/projects/demo —— "+
"空串说明列名或表名对不上(本函数读不到时静默返回空串)", got)
}
// 反向对照:另一条没有 workspace 的会话必须返回空,而不是串上一条的值。
empty, err := CreateSession(ctx, nil, "pi", "无工作目录", "")
if err != nil {
t.Fatalf("建第二条会话: %v", err)
}
if got := SessionWorkspaceOf(ctx, empty); got != "" {
t.Errorf("没有 workspace 的会话应返回空串,实得 %q —— "+
"这可能是查询漏了 WHERE session_id 条件", got)
}
// 不存在的会话:按设计返回空串而不是 panic/报错(投递路径不能因它中断)。
if got := SessionWorkspaceOf(ctx, sid); got == "" {
t.Errorf("已存在的会话不应返回空串")
}
}