package main import ( "os" "strings" "testing" ) /* 非邮件轮次必须能声明一个**合法**的 session_id(2026-10-02,B 方案配套)。 # 背景 服务端 `AgentMayReadSession` 收严后(`15e4fe9`),未声明 session_id 的读信 一律 403。而 homeagent 的工具**全局可调** ⇒ 对话里自主调 read_mail/read_thread 时 `currentSessionID` 为空 ⇒ 不带 session_id ⇒ 403。 「非邮件轮次读信」这个能力不能因此消失(它是 2026-10-01 那个 `read_inbox` 修复的用户可见部分),所以要给它一个合法声明。 # 为什么回落答案必须向服务端问 `workspace` 能回落到进程 cwd,而 **session_id 是 AgentMail 会话的 UUID, cwd 给不出它**。所以只有一条路:`GET /agent/session/default?workspace=/tmp` (服务端纯只读;没通信过就返回 session_id=null,不建会话)。 */ // ① 取值函数 effectiveSessionID **不发请求**(纯取值)。 // // 我第一版把 HTTP 塞进这里,于是 `&Plugin{}` 构造的测试直接 nil panic // (p.client 为 nil),而且 scopeQuery 变成「拼 URL 时顺带发一次请求」。 // 判据钉住这个形状:取值与 IO 混在一起,测试与真实调用都会变得难以推理。 func TestEffectiveSessionIDIsPureLookup(t *testing.T) { p := &Plugin{} // 不设置 client —— 若取值函数敢发请求,这里就 panic if got := p.effectiveSessionID(); got != "" { t.Fatalf("未探测且无当前会话时应为空,实际 %q", got) } } // ② 邮件轮次内优先用信封上的 currentSessionID(不回落)。 func TestEffectiveSessionIDPrefersEnvelope(t *testing.T) { p := &Plugin{currentSessionID: "env-session"} if got := p.effectiveSessionID(); got != "env-session" { t.Fatalf("轮次内必须用信封上的会话,实际 %q", got) } } // ③ scopeQuery 在非邮件轮次、且已问出默认会话时,**仍要带 session_id**。 // 这是本次改动的核心形状:旧写法是 currentSessionID 为空就整体放弃。 func TestScopeQueryCarriesDefaultSessionOutsideTurn(t *testing.T) { p := &Plugin{defaultSessionID: "sess-default-123"} q := p.scopeQuery("&") if !strings.Contains(q, "session_id=sess-default-123") { t.Fatalf("★ 非邮件轮次也必须声明合法 session_id(否则服务端 403):%q", q) } } // ④ 两样都拿不到时才放弃 —— 且是**明确放弃**(返回空串), // 不是「退化成不带 session_id 的裸奔」。 func TestScopeQueryGivesUpOnlyWhenBothAbsent(t *testing.T) { p := &Plugin{} // 既无 currentSessionID,也无 defaultSessionID if q := p.scopeQuery("&"); q != "" { t.Fatalf("两样都没有时整体放弃(服务端会 403,这是可见的错误而非静默放行):%q", q) } } // ⑤ client 没就绪时**不该**标记「已问」——那不是「答案是空」, // 而是「还没资格问」。标了就会把空值永久缓存,之后再也不会问。 // // 这条是被判据打出来的:我第一版没这道判据,register() 里调它会在 // 插件加载阶段 nil panic;而加了判据后若还沿用旧断言,就会把 // 「正确地没标记」当成失败 —— 断言本身要跟着实现意图走,不能反过来。 func TestEnsureDefaultSessionDoesNotProbeBeforeClientReady(t *testing.T) { p := &Plugin{} // client 未就绪 p.ensureDefaultSession() if p.defaultSessionProbed { t.Fatal("★ client 未就绪时不该标记已问(否则永久缓存空值,之后再也不问)") } if p.defaultSessionID != "" { t.Fatalf("更不该有 id,实际 %q", p.defaultSessionID) } } // ⑤b 一旦标记过就**不再重复**问(否则每个工具调用都给服务端添压力)。 // 这里直接检查缓存标记的粘性(真发 HTTP 的形状由 repo/handler 侧判据覆盖)。 func TestDefaultSessionProbeFlagIsSticky(t *testing.T) { p := &Plugin{defaultSessionProbed: true} p.ensureDefaultSession() // 不该再动任何状态 if !p.defaultSessionProbed { t.Fatal("已问标记应是幂等的") } if p.defaultSessionID != "" { t.Fatalf("标记已置时不该凭空产生 id,实际 %q", p.defaultSessionID) } } // ⑥ 邮件轮次内**不**去问默认会话(那次问是纯浪费,且当前SessionID 已有值)。 func TestEnsureDefaultSessionSkipsInsideTurn(t *testing.T) { p := &Plugin{currentSessionID: "env"} p.ensureDefaultSession() if p.defaultSessionProbed { t.Fatal("★ 轮次内不该问默认会话(currentSessionID 已有值)") } } // ⑦ 默认落点与服务端一致:/tmp。 // 两边不一致 = 桥问的是一个 key、服务端建的是另一个 key ⇒ 永远问不到。 func TestDefaultFallbackWorkspaceMatchesServer(t *testing.T) { if defaultFallbackWorkspace != "/tmp" { t.Fatalf("必须与 handler.DefaultFallbackWorkspace 一致,实际 %q", defaultFallbackWorkspace) } if !strings.HasPrefix(defaultFallbackWorkspace, os.Getenv("HOME")) && !strings.HasPrefix(defaultFallbackWorkspace, "/") { t.Fatal("默认落点必须是绝对路径(UnreadWorkspaces 要求 workspace LIKE '/%')") } }