Files
Blizzard 7c8c723245 fix(admin): 诊断解析放错了函数 + 补 kb_search 形状测试
上一个提交里,诊断解析被塞进了用户面的 KbSearch,而真正传 diag 的
AdminKbSearch 还在用老代码 —— 于是试验台永远拿不到 routes。写法上是
字符串替换只改了文件里第一处匹配,而这两个 handler 的收尾代码一模一样。
用户面已还原(它不传 diag,拿到的本就是裸数组,多一层解析是错的)。

补 3 组 mcp 层测试钉死 kb_search 的两种返回形状:
  - 不带 diag(生产调用:dispatcher / Agent 工具链)→ 裸 JSON 数组,契约不变;
  - 带 diag → {hits, routes} 对象;
  - not ready 时不被总闸短路,仍逐路诊断。
这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若无条件生效,所有
调用方的解析都会静默失败。

真环境验证(本地 gateway + mcp-go + 真 PG/Milvus/bleve):
  vector    empty     该知识库在 Milvus 中没有向量(未入库或集合被重建过)
  fulltext  ok        15 命中
  graph     disabled  Neo4j 未连接或未配置
以前这三行都只是"0 条"。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:24:49 +08:00

95 lines
3.4 KiB
Go

package mcp
import (
"context"
"encoding/json"
"testing"
"github.com/sundynix/sundynix-shared/contract"
"github.com/sundynix/sundynix-mcp-go/internal/rag"
)
// kb_search 的两种返回形状必须泾渭分明:
// - 不带 diag(生产调用:dispatcher、Agent 工具链)→ 裸 JSON 数组,契约不能变;
// - 带 diag(只有检索试验台会传)→ {hits, routes} 对象,带每一路的诊断。
//
// 把它钉住是因为这层是"改可观测性顺手改坏生产"的高风险位置:diag 分支若写成无条件
// 生效,所有调用方拿到的就从数组变成了对象,工具链会静默解析失败。
// newFulltextOnlyGateway 造一个只有全文路可用的 Gateway:无 embedding、无 Milvus、无 Neo4j。
// 这正是 mcp-go 先于 gateway 启动(控制面配置尚未下发)时的真实状态。
func newFulltextOnlyGateway(t *testing.T) *Gateway {
t.Helper()
t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve")
e := rag.Open(context.Background(), rag.Config{}) // 不给 Milvus/Neo4j/embedding
return &Gateway{rag: e}
}
func TestKbSearch_ProductionShapeIsBareArray(t *testing.T) {
g := newFulltextOnlyGateway(t)
res := g.kbSearch(context.Background(), &contract.ToolCall{
Tool: "kb_search",
Args: map[string]any{"kb": "k1", "q": "星间链路"},
})
if !res.OK {
t.Fatalf("不应失败: %s", res.Error)
}
var arr []map[string]any
if err := json.Unmarshal([]byte(res.Content), &arr); err != nil {
t.Fatalf("生产调用必须返回裸数组(工具链据此解析),得 %q: %v", res.Content, err)
}
}
func TestKbSearch_DiagShapeCarriesRoutes(t *testing.T) {
g := newFulltextOnlyGateway(t)
res := g.kbSearch(context.Background(), &contract.ToolCall{
Tool: "kb_search",
Args: map[string]any{"kb": "k1", "q": "星间链路", "diag": true, "mode": "hybrid"},
})
if !res.OK {
t.Fatalf("不应失败: %s", res.Error)
}
var out struct {
Hits []map[string]any `json:"hits"`
Routes []rag.RouteDiag `json:"routes"`
}
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
t.Fatalf("diag 调用应返回 {hits,routes} 对象,得 %q: %v", res.Content, err)
}
if len(out.Routes) != 3 {
t.Fatalf("应有 vector/fulltext/graph 三路诊断,得 %d 条: %+v", len(out.Routes), out.Routes)
}
for _, r := range out.Routes {
if r.Status == "" {
t.Fatalf("每一路都必须有 status,否则界面仍无法解释这个空: %+v", r)
}
// 没配置的路必须给出原因——这正是当初排查半天的痛点。
if r.Status == "disabled" && r.Note == "" {
t.Fatalf("%s 路 disabled 却没写原因", r.Name)
}
}
}
// 未配置 embedding/Milvus 时不能整体短路返回空:以前用 rag.Ready() 当总闸,
// 连不依赖它们的全文/图谱路一起毙掉,表现为"知识库什么都搜不到"且不报错。
func TestKbSearch_NotShortCircuitedByReadiness(t *testing.T) {
g := newFulltextOnlyGateway(t)
if g.rag.Ready() {
t.Fatal("前提不成立:该引擎本应 not ready")
}
res := g.kbSearch(context.Background(), &contract.ToolCall{
Tool: "kb_search",
Args: map[string]any{"kb": "k1", "q": "星间链路", "diag": true},
})
var out struct {
Routes []rag.RouteDiag `json:"routes"`
}
if err := json.Unmarshal([]byte(res.Content), &out); err != nil {
t.Fatalf("not ready 时也应正常走到各路诊断,得 %q", res.Content)
}
if len(out.Routes) == 0 {
t.Fatal("not ready 时被总闸短路了(返回空数组而非逐路诊断)")
}
}