mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-08 20:34:48 +08:00
### Summary 1. update doc 2. refactor route code --------- Signed-off-by: Jin Hai <haijin.chn@gmail.com>
565 lines
18 KiB
Go
565 lines
18 KiB
Go
//
|
|
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
//
|
|
|
|
package graph
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"ragflow/internal/engine"
|
|
"ragflow/internal/engine/types"
|
|
modelModule "ragflow/internal/entity/models"
|
|
)
|
|
|
|
type mockRetrievalEngine struct {
|
|
engine.DocEngine
|
|
results map[string]*types.SearchResult
|
|
}
|
|
|
|
func (m *mockRetrievalEngine) Search(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
// --- Contract validation (matches real ES/Infinity preconditions) ---
|
|
if len(req.IndexNames) == 0 {
|
|
return nil, fmt.Errorf("mock: IndexNames cannot be empty")
|
|
}
|
|
if len(req.KbIDs) == 0 {
|
|
return nil, fmt.Errorf("mock: KbIDs cannot be empty")
|
|
}
|
|
// --- Original stubbing logic ---
|
|
kgType, _ := req.Filter["knowledge_graph_kwd"].(string)
|
|
key := kgType
|
|
if ents, ok := req.Filter["entity_kwd"].([]interface{}); ok && len(ents) > 0 {
|
|
key = kgType + ":" + ents[0].(string)
|
|
}
|
|
if r, ok := m.results[key]; ok {
|
|
return r, nil
|
|
}
|
|
if r, ok := m.results[""]; ok {
|
|
return r, nil
|
|
}
|
|
return &types.SearchResult{}, nil
|
|
}
|
|
|
|
// --- entityFromChunk ---
|
|
|
|
func TestEntityFromChunk_Basic(t *testing.T) {
|
|
chunk := map[string]interface{}{
|
|
"_score": 0.85,
|
|
"rank_flt": 0.9,
|
|
"content_with_weight": "Founder of SpaceX",
|
|
"n_hop_with_weight": `[{"path":["A","B"],"weights":[0.8]}]`,
|
|
}
|
|
e := entityFromChunk("Elon Musk", chunk)
|
|
if e.Similarity != 0.85 {
|
|
t.Errorf("expected Sim=0.85, got %f", e.Similarity)
|
|
}
|
|
if e.PageRank != 0.9 {
|
|
t.Errorf("expected PageRank=0.9, got %f", e.PageRank)
|
|
}
|
|
if e.Description != "Founder of SpaceX" {
|
|
t.Errorf("expected Description, got %q", e.Description)
|
|
}
|
|
if len(e.NhopEnts) != 1 || len(e.NhopEnts[0].Path) != 2 {
|
|
t.Errorf("expected 1 NhopEnt with 2-path, got %+v", e.NhopEnts)
|
|
}
|
|
}
|
|
|
|
func TestEntityFromChunk_ScoreFallback(t *testing.T) {
|
|
chunk := map[string]interface{}{"score": 0.75}
|
|
e := entityFromChunk("Test", chunk)
|
|
if e.Similarity != 0.75 {
|
|
t.Errorf("expected Sim=0.75 from score field, got %f", e.Similarity)
|
|
}
|
|
}
|
|
|
|
func TestEntityFromChunk_MissingFields(t *testing.T) {
|
|
chunk := map[string]interface{}{}
|
|
e := entityFromChunk("Empty", chunk)
|
|
if e.Similarity != 0 || e.PageRank != 0 || len(e.NhopEnts) != 0 {
|
|
t.Errorf("expected zero defaults, got %+v", e)
|
|
}
|
|
}
|
|
|
|
// --- relationFromChunk ---
|
|
|
|
func TestRelationFromChunk_Basic(t *testing.T) {
|
|
chunk := map[string]interface{}{
|
|
"from_entity_kwd": "Elon Musk",
|
|
"to_entity_kwd": "SpaceX",
|
|
"weight_int": float64(5),
|
|
"content_with_weight": "Founder",
|
|
}
|
|
edge, rel := relationFromChunk(chunk)
|
|
if edge.From != "Elon Musk" || edge.To != "SpaceX" {
|
|
t.Errorf("expected Elon Musk→SpaceX, got %v", edge)
|
|
}
|
|
if rel.PageRank != 5 {
|
|
t.Errorf("expected weight 5, got %f", rel.PageRank)
|
|
}
|
|
}
|
|
|
|
func TestRelationFromChunk_MissingFrom(t *testing.T) {
|
|
chunk := map[string]interface{}{"to_entity_kwd": "B"}
|
|
edge, _ := relationFromChunk(chunk)
|
|
if edge.From != "" {
|
|
t.Error("expected empty from")
|
|
}
|
|
}
|
|
|
|
// --- searchTypeSamples ---
|
|
|
|
func TestSearchTypeSamples_Success(t *testing.T) {
|
|
data, _ := json.Marshal(map[string][]string{"PERSON": {"Elon Musk"}})
|
|
mock := &mockRetrievalEngine{
|
|
results: map[string]*types.SearchResult{
|
|
"ty2ents": {Chunks: []map[string]interface{}{
|
|
{"content_with_weight": string(data)},
|
|
}},
|
|
},
|
|
}
|
|
result, err := searchTypeSamples(context.Background(), mock, []string{"ragflow_tenant1"}, []string{"kb1"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(result) != 1 || len(result["PERSON"]) != 1 || result["PERSON"][0] != "Elon Musk" {
|
|
t.Errorf("expected PERSON→[Elon Musk], got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestSearchTypeSamples_Empty(t *testing.T) {
|
|
mock := &mockRetrievalEngine{}
|
|
result, err := searchTypeSamples(context.Background(), mock, []string{"ragflow_tenant1"}, []string{"kb1"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(result) != 0 {
|
|
t.Errorf("expected empty, got %v", result)
|
|
}
|
|
}
|
|
|
|
// --- Retrieval ---
|
|
|
|
func TestRetrieval_Basic(t *testing.T) {
|
|
mock := &mockRetrievalEngine{
|
|
results: map[string]*types.SearchResult{
|
|
"entity": {Chunks: []map[string]interface{}{
|
|
{"entity_kwd": "Elon Musk", "entity_type_kwd": "PERSON", "rank_flt": 0.9, "_score": 0.85},
|
|
}},
|
|
"relation": {Chunks: []map[string]interface{}{
|
|
{"from_entity_kwd": "Elon Musk", "to_entity_kwd": "SpaceX", "weight_int": float64(5), "_score": 0.85},
|
|
}},
|
|
"community_report": {Chunks: []map[string]interface{}{
|
|
{"docnm_kwd": "Community 1", "content_with_weight": "Report text", "weight_flt": 0.95},
|
|
}},
|
|
"ty2ents": {Chunks: []map[string]interface{}{
|
|
{"content_with_weight": `{"PERSON":["Elon Musk"]}`},
|
|
}},
|
|
},
|
|
}
|
|
result, err := Retrieval(context.Background(), mock, nil, nil, []string{"kb1"}, []string{"tenant1"}, "Elon Musk")
|
|
if err != nil {
|
|
t.Fatalf("Retrieval failed: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
content, ok := result["content_with_weight"].(string)
|
|
if !ok {
|
|
t.Fatal("expected content_with_weight string")
|
|
}
|
|
if content == "" {
|
|
t.Error("expected non-empty KG content")
|
|
}
|
|
if result["similarity"] != 1.0 {
|
|
t.Errorf("expected similarity 1.0, got %v", result["similarity"])
|
|
}
|
|
if result["docnm_kwd"] != "Related content in Knowledge Graph" {
|
|
t.Errorf("unexpected docnm_kwd: %v", result["docnm_kwd"])
|
|
}
|
|
}
|
|
|
|
func TestRetrieval_NoEntities(t *testing.T) {
|
|
mock := &mockRetrievalEngine{}
|
|
result, err := Retrieval(context.Background(), mock, nil, nil, []string{"kb1"}, []string{"tenant1"}, "test")
|
|
if err != nil {
|
|
t.Fatalf("Retrieval failed: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
content, _ := result["content_with_weight"].(string)
|
|
if content != "" {
|
|
t.Errorf("expected empty when no entities found, got %q", content)
|
|
}
|
|
}
|
|
|
|
// TestEntitySearch_MultiEntities verifies that all entities are used in search query.
|
|
|
|
func TestRetrieval_WithChatModel(t *testing.T) {
|
|
mock := &mockRetrievalEngine{
|
|
results: map[string]*types.SearchResult{
|
|
"entity": {Chunks: []map[string]interface{}{
|
|
{"entity_kwd": "Elon Musk", "entity_type_kwd": "PERSON", "rank_flt": 0.9, "_score": 0.85},
|
|
}},
|
|
"relation": {Chunks: []map[string]interface{}{
|
|
{"from_entity_kwd": "Elon Musk", "to_entity_kwd": "SpaceX", "weight_int": float64(5), "_score": 0.85},
|
|
}},
|
|
},
|
|
}
|
|
// chatModel with nil ModelName so queryRewrite falls back to raw question,
|
|
// but the ty2entsJSON construction path is still exercised.
|
|
chatModel := &modelModule.ChatModel{ModelName: nil, APIConfig: nil}
|
|
result, err := Retrieval(context.Background(), mock, chatModel, nil, []string{"kb1"}, []string{"tenant1"}, "Elon Musk")
|
|
if err != nil {
|
|
t.Fatalf("Retrieval failed: %v", err)
|
|
}
|
|
if result == nil {
|
|
t.Fatal("expected non-nil result")
|
|
}
|
|
content, ok := result["content_with_weight"].(string)
|
|
if !ok {
|
|
t.Fatal("expected content_with_weight string")
|
|
}
|
|
if content == "" {
|
|
t.Error("expected non-empty KG content")
|
|
}
|
|
// Verify "null" does not appear — the ty2entsJSON fix ensures "{}" not "null"
|
|
if strings.Contains(content, "null") {
|
|
t.Error("content should not contain 'null' from ty2entsJSON")
|
|
}
|
|
}
|
|
|
|
func TestEntitySearch_MultiEntities(t *testing.T) {
|
|
var capturedText string
|
|
mock := &searchCaptureEngine{}
|
|
mock.searchFn = func(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
if kgType, _ := req.Filter["knowledge_graph_kwd"].(string); kgType == "entity" && len(req.MatchExprs) > 0 {
|
|
if expr, ok := req.MatchExprs[0].(*types.MatchTextExpr); ok {
|
|
capturedText = expr.MatchingText
|
|
}
|
|
}
|
|
return &types.SearchResult{}, nil
|
|
}
|
|
entities := []string{"Elon Musk", "SpaceX"}
|
|
entsReq := &types.SearchRequest{
|
|
IndexNames: []string{"ragflow_tenant1"},
|
|
KbIDs: []string{"kb1"},
|
|
SelectFields: []string{"entity_kwd", "n_hop_with_weight"},
|
|
Limit: 50,
|
|
Filter: map[string]interface{}{"knowledge_graph_kwd": "entity"},
|
|
MatchExprs: []interface{}{
|
|
&types.MatchTextExpr{
|
|
Fields: []string{"entity_kwd^10", "content_ltks^2"},
|
|
MatchingText: strings.Join(entities, " "),
|
|
TopN: 50,
|
|
},
|
|
},
|
|
}
|
|
mock.Search(context.Background(), entsReq)
|
|
if !strings.Contains(capturedText, "Elon Musk") || !strings.Contains(capturedText, "SpaceX") {
|
|
t.Errorf("expected both entities in query, got %q", capturedText)
|
|
}
|
|
}
|
|
|
|
// searchCaptureEngine is a minimal mock for testing search requests.
|
|
type searchCaptureEngine struct {
|
|
engine.DocEngine
|
|
searchFn func(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error)
|
|
}
|
|
|
|
func (e *searchCaptureEngine) Search(ctx context.Context, req *types.SearchRequest) (*types.SearchResult, error) {
|
|
if len(req.IndexNames) == 0 {
|
|
return nil, fmt.Errorf("mock: IndexNames cannot be empty")
|
|
}
|
|
if e.searchFn != nil {
|
|
return e.searchFn(ctx, req)
|
|
}
|
|
return &types.SearchResult{}, nil
|
|
}
|
|
|
|
// --- queryRewrite ---
|
|
|
|
func TestQueryRewrite_Fallback(t *testing.T) {
|
|
typeKeywords, entities := queryRewrite(nil, "What is SpaceX?", "{}")
|
|
if typeKeywords != nil {
|
|
t.Errorf("expected nil typeKeywords when no LLM, got %v", typeKeywords)
|
|
}
|
|
if len(entities) != 1 || entities[0] != "What is SpaceX?" {
|
|
t.Errorf("expected [What is SpaceX?], got %v", entities)
|
|
}
|
|
}
|
|
|
|
func TestQueryRewrite_EmptyQuestion(t *testing.T) {
|
|
typeKeywords, entities := queryRewrite(nil, "", "")
|
|
if typeKeywords != nil || entities != nil {
|
|
t.Errorf("expected nil for empty question, got type=%v entities=%v", typeKeywords, entities)
|
|
}
|
|
}
|
|
|
|
// spyEmbedDriver captures Embed input for testing — enables assertions on what text
|
|
// was embedded, not just that embedding succeeded.
|
|
type spyEmbedDriver struct {
|
|
modelModule.ModelDriver
|
|
capturedTexts []string
|
|
vector []float64
|
|
err error
|
|
}
|
|
|
|
func (s *spyEmbedDriver) Embed(_ *string, texts []string, _ *modelModule.APIConfig, _ *modelModule.EmbeddingConfig) ([]modelModule.EmbeddingData, error) {
|
|
s.capturedTexts = texts
|
|
if s.err != nil {
|
|
return nil, s.err
|
|
}
|
|
return []modelModule.EmbeddingData{{Embedding: s.vector}}, nil
|
|
}
|
|
|
|
// --- pure function: buildMatchDenseExpr ---
|
|
|
|
func TestBuildMatchDenseExpr_Basic(t *testing.T) {
|
|
vector := []float64{0.1, 0.2, 0.3}
|
|
expr := buildMatchDenseExpr(vector, 10, 0.2)
|
|
if expr.VectorColumnName != "q_3_vec" {
|
|
t.Errorf("expected q_3_vec, got %q", expr.VectorColumnName)
|
|
}
|
|
if len(expr.EmbeddingData) != 3 || expr.EmbeddingData[0] != 0.1 {
|
|
t.Errorf("unexpected embedding data: %v", expr.EmbeddingData)
|
|
}
|
|
if expr.EmbeddingDataType != "float" {
|
|
t.Errorf("expected float, got %q", expr.EmbeddingDataType)
|
|
}
|
|
if expr.DistanceType != "cosine" {
|
|
t.Errorf("expected cosine, got %q", expr.DistanceType)
|
|
}
|
|
if expr.TopN != 10 {
|
|
t.Errorf("expected TopN=10, got %d", expr.TopN)
|
|
}
|
|
sim, ok := expr.ExtraOptions["similarity"].(float64)
|
|
if !ok || sim != 0.2 {
|
|
t.Errorf("expected similarity=0.2, got %v", expr.ExtraOptions["similarity"])
|
|
}
|
|
}
|
|
|
|
func TestBuildMatchDenseExpr_ZeroVector(t *testing.T) {
|
|
expr := buildMatchDenseExpr(nil, 5, 0.0)
|
|
if expr.VectorColumnName != "q_0_vec" {
|
|
t.Errorf("expected q_0_vec for empty vector, got %q", expr.VectorColumnName)
|
|
}
|
|
}
|
|
|
|
// --- pure function: buildFusionExpr ---
|
|
|
|
func TestBuildFusionExpr_DefaultWeights(t *testing.T) {
|
|
expr := buildFusionExpr(0.5, 0.5, 20)
|
|
if expr.Method != "weighted_sum" {
|
|
t.Errorf("expected weighted_sum, got %q", expr.Method)
|
|
}
|
|
if expr.TopN != 20 {
|
|
t.Errorf("expected TopN=20, got %d", expr.TopN)
|
|
}
|
|
weights, ok := expr.FusionParams["weights"].(string)
|
|
if !ok || weights != "0.50,0.50" {
|
|
t.Errorf("expected weights=0.50,0.50, got %v", expr.FusionParams["weights"])
|
|
}
|
|
}
|
|
|
|
func TestBuildFusionExpr_AsymmetricWeights(t *testing.T) {
|
|
expr := buildFusionExpr(0.3, 0.7, 10)
|
|
weights := expr.FusionParams["weights"].(string)
|
|
if weights != "0.30,0.70" {
|
|
t.Errorf("expected 0.30,0.70, got %q", weights)
|
|
}
|
|
}
|
|
|
|
// --- buildSearchExprs ---
|
|
|
|
func TestBuildSearchExprs_NoEmbModel(t *testing.T) {
|
|
matchText := &types.MatchTextExpr{
|
|
Fields: []string{"entity_kwd^10"},
|
|
MatchingText: "test",
|
|
TopN: 10,
|
|
}
|
|
exprs := buildSearchExprs(nil, matchText, 0, 0)
|
|
if len(exprs) != 1 {
|
|
t.Fatalf("expected 1 expr, got %d", len(exprs))
|
|
}
|
|
mt, ok := exprs[0].(*types.MatchTextExpr)
|
|
if !ok {
|
|
t.Fatalf("expected MatchTextExpr, got %T", exprs[0])
|
|
}
|
|
if mt.MatchingText != "test" {
|
|
t.Errorf("expected 'test', got %q", exprs[0].(*types.MatchTextExpr).MatchingText)
|
|
}
|
|
}
|
|
|
|
func TestBuildSearchExprs_WithEmbModel(t *testing.T) {
|
|
driver := &spyEmbedDriver{vector: []float64{0.1, 0.2, 0.3}}
|
|
embModel := modelModule.NewEmbeddingModel(driver, strPtr("text-embedding"), &modelModule.APIConfig{}, 512)
|
|
matchText := &types.MatchTextExpr{
|
|
Fields: []string{"entity_kwd^10"},
|
|
MatchingText: "Elon Musk SpaceX",
|
|
TopN: 50,
|
|
}
|
|
exprs := buildSearchExprs(embModel, matchText, defaultSimThreshold, defaultDenseTopK)
|
|
// Verify Embed was called with matchText.MatchingText, not raw question
|
|
if len(driver.capturedTexts) != 1 || driver.capturedTexts[0] != "Elon Musk SpaceX" {
|
|
t.Errorf("expected Embed to receive %q, got %v", "Elon Musk SpaceX", driver.capturedTexts)
|
|
}
|
|
if len(exprs) != 3 {
|
|
t.Fatalf("expected 3 exprs (text+dense+fusion), got %d", len(exprs))
|
|
}
|
|
// Index 0: MatchTextExpr
|
|
mt, ok := exprs[0].(*types.MatchTextExpr)
|
|
if !ok {
|
|
t.Fatalf("expected MatchTextExpr at [0], got %T", exprs[0])
|
|
}
|
|
if mt.MatchingText != "Elon Musk SpaceX" {
|
|
t.Errorf("expected 'Elon Musk SpaceX', got %q", mt.MatchingText)
|
|
}
|
|
// Index 1: MatchDenseExpr
|
|
md, ok := exprs[1].(*types.MatchDenseExpr)
|
|
if !ok {
|
|
t.Fatalf("expected MatchDenseExpr at [1], got %T", exprs[1])
|
|
}
|
|
if md.VectorColumnName != "q_3_vec" {
|
|
t.Errorf("expected q_3_vec, got %q", md.VectorColumnName)
|
|
}
|
|
if md.TopN != defaultDenseTopK {
|
|
t.Errorf("expected TopN=%d (Python alignment), got %d", defaultDenseTopK, md.TopN)
|
|
}
|
|
if md.ExtraOptions["similarity"] != defaultSimThreshold {
|
|
t.Errorf("expected similarity=%v (Python alignment), got %v", defaultSimThreshold, md.ExtraOptions["similarity"])
|
|
}
|
|
// Index 2: FusionExpr
|
|
fu, ok := exprs[2].(*types.FusionExpr)
|
|
if !ok {
|
|
t.Fatalf("expected FusionExpr at [2], got %T", exprs[2])
|
|
}
|
|
if fu.Method != "weighted_sum" {
|
|
t.Errorf("expected weighted_sum, got %q", fu.Method)
|
|
}
|
|
}
|
|
|
|
func TestBuildSearchExprs_EmbModelFallback(t *testing.T) {
|
|
driver := &spyEmbedDriver{err: assertError("embed failed")}
|
|
embModel := modelModule.NewEmbeddingModel(driver, strPtr("text-embedding"), &modelModule.APIConfig{}, 512)
|
|
matchText := &types.MatchTextExpr{
|
|
Fields: []string{"entity_kwd^10"},
|
|
MatchingText: "fallback test",
|
|
TopN: 10,
|
|
}
|
|
exprs := buildSearchExprs(embModel, matchText, defaultSimThreshold, defaultDenseTopK)
|
|
// Should fall back to text-only when Embed fails
|
|
if len(exprs) != 1 {
|
|
t.Fatalf("expected 1 expr (text-only fallback), got %d", len(exprs))
|
|
}
|
|
if _, ok := exprs[0].(*types.MatchTextExpr); !ok {
|
|
t.Errorf("expected MatchTextExpr, got %T", exprs[0])
|
|
}
|
|
}
|
|
|
|
// --- Python alignment defaults ---
|
|
|
|
func TestDefaultValuesMatchPython(t *testing.T) {
|
|
if defaultSimThreshold != 0.3 {
|
|
t.Errorf("expected 0.3 (Python ent_sim_threshold), got %f", defaultSimThreshold)
|
|
}
|
|
if defaultDenseTopK != 1024 {
|
|
t.Errorf("expected 1024 (Python get_vector topk), got %d", defaultDenseTopK)
|
|
}
|
|
}
|
|
|
|
// assertError is a simple error for testing fallback behaviour.
|
|
type assertError string
|
|
|
|
func (e assertError) Error() string { return string(e) }
|
|
|
|
// --- indexName ---
|
|
|
|
func TestIndexName_Normal(t *testing.T) {
|
|
result := indexName("tenant1")
|
|
if result != "ragflow_tenant1" {
|
|
t.Errorf("expected ragflow_tenant1, got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestIndexName_Empty(t *testing.T) {
|
|
result := indexName("")
|
|
if result != "ragflow_" {
|
|
t.Errorf("expected ragflow_, got %q", result)
|
|
}
|
|
}
|
|
|
|
// --- searchCommunityContent ---
|
|
|
|
func TestSearchKGCommunityContent_EmptyEntities(t *testing.T) {
|
|
mock := &mockRetrievalEngine{}
|
|
result := searchCommunityContent(context.Background(), mock, []string{"ragflow_t1"}, []string{"kb1"}, nil, 1, intPtr(100))
|
|
if result != "" {
|
|
t.Errorf("expected empty, got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestSearchKGCommunityContent_WithContent(t *testing.T) {
|
|
mock := &mockRetrievalEngine{
|
|
results: map[string]*types.SearchResult{
|
|
"community_report": {Chunks: []map[string]interface{}{
|
|
{
|
|
"docnm_kwd": "Community Alpha",
|
|
"content_with_weight": `{"report": "Report text", "evidences": "Evidence text"}`,
|
|
},
|
|
}},
|
|
},
|
|
}
|
|
result := searchCommunityContent(context.Background(), mock, []string{"ragflow_t1"}, []string{"kb1"}, []ScoredEntity{{Entity: "E1"}}, 1, intPtr(500))
|
|
if result == "" {
|
|
t.Fatal("expected non-empty result")
|
|
}
|
|
if !strings.Contains(result, "Community Alpha") {
|
|
t.Errorf("expected title 'Community Alpha', got %q", result)
|
|
}
|
|
if !strings.Contains(result, "Report text") {
|
|
t.Errorf("expected report content, got %q", result)
|
|
}
|
|
if !strings.Contains(result, "Evidence text") {
|
|
t.Errorf("expected evidence, got %q", result)
|
|
}
|
|
if !strings.Contains(result, "# 1.") {
|
|
t.Errorf("expected numbered report (# 1.), got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestSearchKGCommunityContent_NilMaxToken(t *testing.T) {
|
|
mock := &mockRetrievalEngine{}
|
|
result := searchCommunityContent(context.Background(), mock, []string{"ragflow_t1"}, []string{"kb1"}, []ScoredEntity{{Entity: "E1"}}, 1, nil)
|
|
if result != "" {
|
|
t.Errorf("expected empty when maxToken is nil, got %q", result)
|
|
}
|
|
}
|
|
|
|
func TestSearchKGCommunityContent_ZeroMaxToken(t *testing.T) {
|
|
mock := &mockRetrievalEngine{}
|
|
result := searchCommunityContent(context.Background(), mock, []string{"ragflow_t1"}, []string{"kb1"}, []ScoredEntity{{Entity: "E1"}}, 1, intPtr(0))
|
|
if result != "" {
|
|
t.Errorf("expected empty when maxToken=0, got %q", result)
|
|
}
|
|
}
|
|
|
|
// intPtr returns a pointer to n.
|
|
func intPtr(n int) *int { return &n }
|