package main import ( "net/http" "net/http/httptest" "net/url" "strings" "testing" ) /* read_thread 的两条路径都必须真的落在网关路由上。 # 缺陷(dsh 2026-09-29 报,已在本机复现) `tools.go` 里曾写死 `scopeQuery("&")`。模型默认**不传 offset** ⇒ offset 为空串 ⇒ 拼出 `/agent/mail/{id}/thread&session_id=…`。`&` 不是 `?`,所以它被当成 **路径的一部分**:网关路由 `/agent/mail/{id}/thread`({id} 只吃一个路径段) 匹配不上 ⇒ **404**。生产日志里同一封邮件相隔 19 秒两条请求, 只带 session_id 的那条 200、带 workspace 的那条 404 —— 后者就是本桥。 # 为什么原来的守卫测试没拦住 `plugin_read_scope_test.go` 断言的是「URL 里**有没有** session_id=」—— 缺陷 URL 里**有**,所以绿着。判据必须落在**路径**上,而不是查询串的存在性。 # 陷阱:不能只把 & 改成 ? `offset>0` 那条的 offset 自带 `?`,所以它原本是**对的**。把 `"&"` 一刀切成 `"?"` 反而会把这条改坏。两条路径必须都验 —— 所以这个测试跑两组。 */ func TestReadThreadBothPathsRoutable(t *testing.T) { var seen []string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { seen = append(seen, r.URL.RequestURI()) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{}`)) })) defer srv.Close() const sid = "593988da-0000-0000-0000-0000000000aa" const wantPath = "/api/v1/agent/mail/m-1/thread" cases := []struct { name string args map[string]interface{} wantPath string // wantQ 只钉**这条判据要管的参数**(路径拼接 / offset 传递)。 // ★ 2026-10-01:原断言用的是整串相等,而补上 cwd 回落之后 // scopeQuery 会多带一个 `workspace=` 参数 ⇒ 整串比较必然不等。 // 要防的东西没变(路径必须逐字匹配路由、offset 必须传下去), // 所以改成「wantQ 的每个片段都必须在实际查询串里」。 wantQ []string }{ {"offset 省略", map[string]interface{}{"mail_id": "m-1"}, wantPath, []string{"session_id=" + sid}}, {"offset>0", map[string]interface{}{"mail_id": "m-1", "offset": float64(5)}, wantPath, []string{"offset=5", "session_id=" + sid}}, } for _, c := range cases { p := &Plugin{gwURL: srv.URL, client: srv.Client(), currentSessionID: sid} seen = nil if _, err := p.handleReadThread(c.args); err != nil { t.Fatalf("%s:调用失败 %v", c.name, err) } if len(seen) != 1 { t.Fatalf("%s:应当只请求一次(实际 %d 次:%v)", c.name, len(seen), seen) } u, err := url.Parse(seen[0]) if err != nil { t.Fatalf("%s:拼出的 URL 不合法 %q:%v", c.name, seen[0], err) } // ★ 关键判据:路径必须与网关路由逐字相等。缺陷版本这里是 // "/api/v1/agent/mail/m-1/thread&session_id=…" ⇒ 一定不相等。 if u.Path != c.wantPath { t.Fatalf("★ %s:路径匹配不上网关路由\n 实际: %s\n 期望: %s\n (完整请求 %s)", c.name, u.Path, c.wantPath, seen[0]) } for _, frag := range c.wantQ { if !strings.Contains(u.RawQuery, frag) { t.Fatalf("★ %s:查询串缺 %q\n 实际: %s", c.name, frag, u.RawQuery) } } } }