package rag import ( "context" "testing" ) // 这组测试钉死「一路挂了不能拖垮全部」以及「三种空必须分得清」。 // // 背景:embedding 未配置时,kb_search 与 Search() 都用 Ready() 当总闸直接返回空—— // 而全文(bleve)、图谱(Neo4j)两路根本不依赖 embedding/Milvus。结果是"模型配置没下发" // 表现为"整个知识库什么都搜不到",且无任何错误信息,排查时毫无线索。 // newFulltextOnlyEngine 造一个只有全文路可用的引擎:无 embedding、无 Milvus、无 Neo4j, // 正是 mcp-go 先于 gateway 启动(控制面配置尚未下发)时的真实状态。 func newFulltextOnlyEngine(t *testing.T) *Engine { t.Helper() t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve") e := &Engine{bleve: openBleve(), graph: &graphStore{}} t.Cleanup(func() { e.bleve.close() }) if !e.bleve.ready() { t.Skip("bleve 不可用") } if err := e.bleve.index("k1", "d1", []string{ "星云一号卫星于2023年由长征七号发射,项目总设计师是李明华。", "北斗增强终端解决了星间链路的可靠性难题。", }); err != nil { t.Fatal(err) } return e } // 没有 embedding/Milvus 时,全文路仍须照常召回——不能被总闸一刀切。 func TestSearch_FulltextSurvivesWithoutEmbedding(t *testing.T) { e := newFulltextOnlyEngine(t) if e.Ready() { t.Fatal("前提不成立:该引擎本应是 not ready(无 embedding/Milvus)") } hits, err := e.Search(context.Background(), "k1", "星间链路可靠性", 5) if err != nil { t.Fatalf("检索报错: %v", err) } if len(hits) == 0 { t.Fatal("embedding 未配置时全文路仍应有召回;返回空说明总闸回归了") } } // 诊断必须区分三种"空":没配置 / 报错 / 确实没匹配。 func TestSearchByModeDiag_DistinguishesEmptyKinds(t *testing.T) { e := newFulltextOnlyEngine(t) ctx := context.Background() _, diags := e.SearchByModeDiag(ctx, "k1", "星间链路可靠性", 5, "hybrid") byName := map[string]RouteDiag{} for _, d := range diags { byName[d.Name] = d } if len(byName) != 3 { t.Fatalf("应有 vector/fulltext/graph 三路诊断,得 %v", diags) } // 向量路:没配置 → disabled(不是 empty,更不是静默消失) if got := byName["vector"].Status; got != "disabled" { t.Fatalf("无 embedding 时向量路应为 disabled,得 %q", got) } if byName["vector"].Note == "" { t.Fatal("disabled 必须给出原因,否则界面上仍是一个无法解释的空") } // 图谱路:Neo4j 未连 → disabled if got := byName["graph"].Status; got != "disabled" { t.Fatalf("无 Neo4j 时图谱路应为 disabled,得 %q", got) } // 全文路:有命中 → ok if got := byName["fulltext"].Status; got != "ok" { t.Fatalf("全文路应为 ok,得 %q(hits=%d err=%q)", got, byName["fulltext"].Hits, byName["fulltext"].Error) } // 换一个库里没有的词:全文路应是 empty(确实没匹配),而不是 disabled/error _, diags2 := e.SearchByModeDiag(ctx, "k1", "完全不相关的查询内容xyz", 5, "fulltext") for _, d := range diags2 { if d.Name == "fulltext" && d.Status != "empty" { t.Fatalf("无匹配时全文路应为 empty,得 %q", d.Status) } } } // 内存兜底(索引不持久)必须在诊断里说出来:命中数正常但数据随时会没, // 只看 hits 数量完全看不出来。 func TestDiag_ReportsNonPersistentIndex(t *testing.T) { e := &Engine{bleve: memBleve(), graph: &graphStore{}} defer e.bleve.close() if err := e.bleve.index("k1", "d1", []string{"星间链路可靠性难题"}); err != nil { t.Fatal(err) } _, diags := e.SearchByModeDiag(context.Background(), "k1", "星间链路", 5, "fulltext") for _, d := range diags { if d.Name != "fulltext" { continue } if d.Status != "ok" { t.Fatalf("内存索引也应能召回,得 %q", d.Status) } if d.Note == "" { t.Fatal("内存兜底必须在 note 里点明(重启即清零),否则运维看不出这是降级态") } } }