Files
MailUI4Agents/plugins/homeagent-mail-bridge/read_inbox_workspace_test.go
JianFeeeee f86f08c7dc fix(桥): read_inbox 在非邮件驱动会话上也得带 workspace —— homeagent 与 opencode 两处
同一个形状的缺陷在两个桥上,**根因都是架构差异,不是"忘了写"**:
pi/opencode/dsh 的工具**只在邮件驱动回合里装配** ⇒ workspace 永远有值;
homeagent 的 15 个工具是 `registerTool` **全局注册**(webui/a2a/对话都能调),
opencode 是 `export default` 全局插件 ⇒ 非邮件驱动会话上查不到工作区
⇒ 拼不出 `&workspace=` ⇒ 服务端 400。

实测:homeagent 2026-10-01 14:32:26 一次真实失败,近 24h **3 失败 / 0 成功**。
opencode 那次是**静默**失败(工具返回错误文本,模型照走)⇒ 线上 0 次报错
不代表没问题,是靠两桥的架构差异推出来的,不是靠日志。

修法不同(各自的可用信号不同):
- homeagent: `effectiveWorkspace()` = 信封 → **回落到进程 cwd**。
  cwd 是对的默认值:homed 按调用上下文以子进程拉起插件,实测对话中那个桥
  cwd=/home/program/agentmail,而从插件目录拉起的那个是 plugins/...。
  ⚠ 只是默认不是保证(库里 17 个工作区),收窄语义不变。
- opencode: handler 拿得到 `context.sessionID`,而 opencode `Session`
  **带 directory**(types.gen.d.ts 的 `export type Session` 可见),
  且 `client.session.get` 本桥已在用 ⇒ 现场问权威值,不猜也不另存一份。
  readInboxTool 是模块级常量,故新增 hostClient 在 init 时捕获。

部署:homeagent 首次走正规 hmap 路径(解包 + cp manifest + install -m 0755,
skill §5 四步全绿)。判据:homeagent 新增 4 格 + 修正 3 条失效的既有断言
(补 cwd 回落使其旧前提失效:整串相等 vs 分片包含、"q[1:] 不能有 &" vs 合法分隔符、
"没工作区就不带"vs"带的是不是真值"——判据失败时先判断是判据错了还是行为错了)。
opencode 新增 5 格。全量:server 全绿 + race 干净;opencode 桥 359/359。

端到端:homeagent webui 一轮 `tool read_inbox result: {"content":[{"text":"收件箱为空。"
(修复前是 400);opencode 两条非邮件驱动会话均 status: completed,
"空"是正确的收窄结果(74 封全是 read/archived,unread=0),近 10 分钟 0 次 400。
2026-10-01 19:41:16 +08:00

91 lines
3.5 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 main
import (
"os"
"path/filepath"
"strings"
"testing"
)
// ★ read_inbox 在**非邮件轮次**里必须仍能带上 workspace(2026-10-01)
//
// ## 缺陷
//
// homeagent 的工具是**全局注册**的(webui / a2a / 对话都能调),而
// `currentWorkspace` 只在 `InjectInputSync` 期间有值 ⇒ 从对话里调
// `read_inbox` 拼不出 `&workspace=` ⇒ 服务端 400。
//
// 实测(2026-10-01 14:32:26):近 24h **3 次失败、0 次成功**。
// 另三桥没这个问题,因为它们的工具**只在邮件驱动回合里被装配**
// —— 那是架构差异,不是「某个桥忘了写」。
//
// ## 为什么回落到进程 cwd 是对的取值
//
// homed 按调用上下文以子进程方式拉起插件,那个进程的 cwd 就是
// **调用方的工作目录**(实测:对话里那个桥 cwd=/home/program/agentmail)。
//
// ⚠ 它是**默认**不是**保证**:库里 17 个工作区,cwd 只反映当前这一个调用方。
// 收窄语义不变(仍只列这个工作区的信)—— 跨工作区误读正是
// 2026-09-26 那个缺陷本身,拿不准时宁可少列。
func TestEffectiveWorkspaceFallsBackToProcessCwd(t *testing.T) {
p := &Plugin{}
// 场景 A:邮件轮次内 —— currentWorkspace 有值,**必须优先用它**
p.currentWorkspace = "/home/program/agentmail"
if got := p.effectiveWorkspace(); got != "/home/program/agentmail" {
t.Fatalf("轮次内必须用信封上的 path 位,实际 %q", got)
}
// 场景 B:对话里(currentWorkspace 为空)—— 回落到 cwd
p.currentWorkspace = ""
got := p.effectiveWorkspace()
if got == "" {
t.Fatal("★ 对话里必须有 workspace 可用,否则 read_inbox 必然 400")
}
cwd, _ := os.Getwd()
if got != cwd {
t.Fatalf("回落值应等于进程 cwd:得到 %q,cwd=%q", got, cwd)
}
if !filepath.IsAbs(got) {
t.Fatalf("回落值必须是绝对路径(服务端要求),实际 %q", got)
}
}
func TestEffectiveWorkspaceTrimsBlanks(t *testing.T) {
// 空白串不是有效取值 —— 当作没有,回落到 cwd。
// 否则会拼出 `workspace=%20%20`,服务端拿到一个不存在的目录。
p := &Plugin{currentWorkspace: " "}
got := p.effectiveWorkspace()
cwd, _ := os.Getwd()
if strings.TrimSpace(got) == "" {
t.Fatal("空白 currentWorkspace 不得被当作有效取值")
}
if got != cwd {
t.Fatalf("空白应回落到 cwd,得到 %q", got)
}
}
func TestInboxURLAlwaysCarriesWorkspace(t *testing.T) {
// 这条是**端到端形状**:拼出来的 URL 必须带 workspace。
// 上面两格验的是取值,这一格验的是「真的拼进去了」——
// 2026-09-26 那次就是取值对了、拼接处漏了,两者是不同的形状。
p := &Plugin{}
u := p.inboxURL("unread", 5)
if !strings.Contains(u, "workspace=") {
t.Fatalf("★ inboxURL 必须带 workspace(缺了服务端直接 400):%s", u)
}
}
func TestScopeQuerySkippedOutsideTurnButInboxNever(t *testing.T) {
// scopeQuery 在非轮次时**整体**返回空串(那是刻意的:没有会话可收窄),
// 但 inboxURL 仍必须带 workspace —— 两者不是同一个开关。
// 混起来就会出现「非轮次时 workspace 也一起没了」,即 2026-10-01 那个缺陷。
p := &Plugin{}
if q := p.scopeQuery("&"); q != "" {
t.Fatalf("非轮次时 scopeQuery 应为空,实际 %q", q)
}
if !strings.Contains(p.inboxURL("unread", 5), "workspace=") {
t.Fatal("★ 非轮次时 inboxURL 仍必须带 workspace")
}
}