Files
ragflow/internal/agent/harness/orchestrator_test.go
Zhichang Yu 4e78f1f440 Port Python agentic search to Go (nav service, harness, tools) (#17702)
Port Python rag/advanced_rag agentic search to Go: ES-backed dataset-nav
service, agentic-search harness, and agent tools.

Includes agentic-search port plan and self-review docs.
2026-08-03 11:16:16 +08:00

136 lines
5.4 KiB
Go

package harness
import (
"context"
"testing"
)
// TestCrossCheckClaim_NumberMatch asserts a verified report whose numbers appear
// in the evidence chunk passes the cross-check.
func TestCrossCheckClaim_NumberMatch(t *testing.T) {
agent := &AgentResult{ClaimID: "c0", IsVerified: true, Report: "speed is 88 and 12", EvidenceIDs: []int{0}}
chunks := map[int]map[string]interface{}{0: {"content_with_weight": "speed is 88 and 12 here"}}
r := CrossCheckClaim(agent, chunks)
if !r.CrossCheckPassed {
t.Errorf("cross-check should pass, got mismatches %v", r.Mismatches)
}
}
// TestKbinfosMerge_GlobalIndices asserts Merge returns the GLOBAL indices of the
// contributed chunks in the accumulated list, so a later claim's EvidenceIDs
// still resolve correctly after earlier claims pushed more chunks in.
func TestKbinfosMerge_GlobalIndices(t *testing.T) {
kb := &Kbinfos{}
// First claim's search returns chunks a,b.
first := kb.Merge([]map[string]interface{}{
{"chunk_id": "a", "content_with_weight": "alpha 7"},
{"chunk_id": "b", "content_with_weight": "beta"},
}, nil)
// a->0, b->1 in the global list.
if len(first) != 2 || first[0] != 0 || first[1] != 1 {
t.Fatalf("first merge global indices = %v, want [0 1]", first)
}
// Second claim's search returns chunk c (now global index 2) and a dup of a.
second := kb.Merge([]map[string]interface{}{
{"chunk_id": "c", "content_with_weight": "gamma 9"},
{"chunk_id": "a", "content_with_weight": "alpha 7"},
}, nil)
// c->2, a(dup)->0; NOT per-search [0 1].
if len(second) != 2 || second[0] != 2 || second[1] != 0 {
t.Fatalf("second merge global indices = %v, want [2 0]", second)
}
// CrossCheckClaim must resolve both claims' evidence against the global list.
allChunks := map[int]map[string]interface{}{}
for i, c := range kb.Chunks {
allChunks[i] = c
}
r1 := CrossCheckClaim(&AgentResult{ClaimID: "c1", IsVerified: true, Report: "alpha 7", EvidenceIDs: first}, allChunks)
if !r1.HasEvidence {
t.Error("claim 1 should have evidence at global indices")
}
r2 := CrossCheckClaim(&AgentResult{ClaimID: "c2", IsVerified: true, Report: "gamma 9", EvidenceIDs: second}, allChunks)
if !r2.HasEvidence {
t.Error("claim 2 should have evidence at global index 2 (not per-search 0)")
}
}
// TestCrossCheckClaim_Unverified asserts an unverified agent fails the check.
func TestCrossCheckClaim_Unverified(t *testing.T) {
r := CrossCheckClaim(&AgentResult{ClaimID: "c0", IsVerified: false}, nil)
if r.CrossCheckPassed {
t.Error("unverified agent must fail cross-check")
}
}
// TestComputeFusionScore_Sufficient asserts a fully-verified high-score set is
// SUFFICIENT.
func TestComputeFusionScore_Sufficient(t *testing.T) {
agents := []AgentResult{{ClaimID: "c0", IsVerified: true, Report: "value 42", EvidenceIDs: []int{0}}}
cross := []ClaimCrossCheckResult{{ClaimID: "c0", CrossCheckPassed: true, CrossCheckScore: 1.0, HasEvidence: true}}
v := ComputeFusionScore(agents, cross, THINKING_MODES["medium"])
if v.Status != "SUFFICIENT" {
t.Errorf("status = %q, want SUFFICIENT", v.Status)
}
}
// TestComputeFusionScore_NoEvidence asserts a claim with no examined evidence is
// UNANSWERABLE (empty-evidence guard).
func TestComputeFusionScore_NoEvidence(t *testing.T) {
cross := []ClaimCrossCheckResult{{ClaimID: "c0", CrossCheckPassed: true, CrossCheckScore: 1.0, HasEvidence: false}}
v := ComputeFusionScore([]AgentResult{{ClaimID: "c0", IsVerified: true}}, cross, THINKING_MODES["medium"])
if v.Status != "UNANSWERABLE" {
t.Errorf("status = %q, want UNANSWERABLE", v.Status)
}
if len(v.MissingClaims) != 1 {
t.Errorf("missing claims = %v, want [c0]", v.MissingClaims)
}
}
// TestRouteSufficiencyVerdict asserts SUFFICIENT → ANSWER.
func TestRouteSufficiencyVerdict(t *testing.T) {
action, cont := RouteSufficiencyVerdict(SufficiencyVerdict{Status: "SUFFICIENT", Score: 0.9}, "medium", 0, 3)
if action != "ANSWER" || cont {
t.Errorf("got (%q,%v), want (ANSWER,false)", action, cont)
}
}
// TestDirectSearch_Merges asserts direct search merges chunks and flags empty.
func TestDirectSearch_Merges(t *testing.T) {
kb := &Kbinfos{}
res := DirectSearch(context.Background(), func(_ context.Context, _, _ string) ([]map[string]interface{}, []map[string]interface{}) {
return []map[string]interface{}{{"chunk_id": "a", "content_with_weight": "alpha"}}, nil
}, "q", "", kb)
if res.EmptyResult || !kb.HasChunks() {
t.Errorf("expected merged chunks, empty=%v chunks=%d", res.EmptyResult, len(kb.Chunks))
}
}
// TestDirectSearch_Empty asserts direct search flags empty when no chunks.
func TestDirectSearch_Empty(t *testing.T) {
kb := &Kbinfos{}
res := DirectSearch(context.Background(), func(_ context.Context, _, _ string) ([]map[string]interface{}, []map[string]interface{}) {
return nil, nil
}, "q", "", kb)
if !res.EmptyResult {
t.Error("expected empty_result=true")
}
}
// TestDecomposeAndSearch_Verifies asserts a searchable claim gets verified and
// the loop stops on ANSWER.
func TestDecomposeAndSearch_Verifies(t *testing.T) {
claims := []*ClaimTarget{{ClaimID: "c0", Description: "fact 42 about X"}}
kb := &Kbinfos{}
res := DecomposeAndSearch(context.Background(),
func(_ context.Context, _, _ string) ([]map[string]interface{}, []map[string]interface{}) {
return []map[string]interface{}{{"chunk_id": "x", "content_with_weight": "the value is 42"}}, nil
},
"Q", "", claims, "medium", kb)
if !claims[0].IsVerified {
t.Error("claim should be verified")
}
if res.Verdict == nil {
t.Error("expected a verdict")
}
}