Files
ragflow/internal/harness/harness_test.go

216 lines
5.0 KiB
Go

package harness
import (
"context"
"sync"
"testing"
"time"
"ragflow/internal/harness/graph/types"
)
func TestNewStateGraph(t *testing.T) {
sg := NewStateGraph(map[string]interface{}{})
if sg == nil {
t.Fatal("expected non-nil StateGraph")
}
}
func TestNewMemorySaver(t *testing.T) {
ms := NewMemorySaver()
if ms == nil {
t.Fatal("expected non-nil MemorySaver")
}
}
func TestCompileSimpleGraph(t *testing.T) {
sg := NewStateGraph(map[string]interface{}{"value": ""})
sg.AddNode("echo", func(ctx context.Context, state interface{}) (interface{}, error) {
return state, nil
})
sg.AddEdge(Start, "echo")
sg.AddEdge("echo", End)
cg, err := sg.Compile()
if err != nil {
t.Fatalf("Compile failed: %v", err)
}
if cg == nil {
t.Fatal("expected non-nil compiled graph")
}
// Verify graph structure
if cg.GetGraph() == nil {
t.Fatal("expected non-nil underlying graph")
}
}
func TestCompileWithRecursionLimit(t *testing.T) {
sg := NewStateGraph(map[string]interface{}{})
sg.AddNode("n", func(ctx context.Context, state interface{}) (interface{}, error) {
return state, nil
})
sg.AddEdge(Start, "n")
sg.AddEdge("n", End)
cg, err := sg.Compile(WithRecursionLimit(5))
if err != nil {
t.Fatalf("Compile failed: %v", err)
}
if cg == nil {
t.Fatal("expected non-nil compiled graph")
}
}
func TestStateGraphNodeOperations(t *testing.T) {
sg := NewStateGraph(map[string]interface{}{})
sg.AddNode("agent", func(ctx context.Context, state interface{}) (interface{}, error) {
return state, nil
})
node, ok := sg.GetNode("agent")
if !ok {
t.Fatal("expected to find node 'agent'")
}
if node.Name != "agent" {
t.Errorf("expected node name 'agent', got '%s'", node.Name)
}
}
func TestNewCommand(t *testing.T) {
cmd := NewCommand()
if cmd == nil {
t.Fatal("expected non-nil Command")
}
}
func TestNewSend(t *testing.T) {
s := NewSend("target", "arg")
if s == nil {
t.Fatal("expected non-nil Send")
}
if s.Node != "target" {
t.Errorf("expected Node='target', got '%s'", s.Node)
}
}
func TestDefaultRetryPolicy(t *testing.T) {
rp := DefaultRetryPolicy()
if rp.MaxAttempts == 0 {
t.Error("expected non-zero MaxAttempts in default retry policy")
}
}
// TestGraph_DAG_SlowBranchMerge verifies AllPredecessor mode: merge waits for
// slow branch. Uses pregel engine (via harness.init() which sets PregelRunFunc)
// and channel-based state (pregel-compatible pattern).
func TestGraph_DAG_SlowBranchMerge(t *testing.T) {
var orderMu sync.Mutex
var order []string
slowStarted := make(chan struct{})
fastDone := make(chan struct{})
releaseSlow := make(chan struct{})
mergeRan := make(chan struct{}, 1)
record := func(name string) {
orderMu.Lock()
order = append(order, name)
orderMu.Unlock()
}
sg := NewStateGraph(map[string]any{})
// Start → dispatch → {fast, slow} → merge
sg.AddNode("dispatch", func(ctx context.Context, state any) (any, error) {
record("dispatch")
return map[string]any{"messages": []string{"dispatch done"}}, nil
})
sg.AddNode("fast", func(ctx context.Context, state any) (any, error) {
record("fast")
close(fastDone)
return map[string]any{"messages": []string{"fast done"}}, nil
})
sg.AddNode("slow", func(ctx context.Context, state any) (any, error) {
close(slowStarted)
<-releaseSlow
record("slow")
return map[string]any{"messages": []string{"slow done"}}, nil
})
sg.AddNode("merge", func(ctx context.Context, state any) (any, error) {
select {
case mergeRan <- struct{}{}:
default:
}
record("merge")
return map[string]any{"messages": []string{"merge done"}}, nil
})
sg.AddEdge(Start, "dispatch")
sg.AddEdge("dispatch", "fast")
sg.AddEdge("dispatch", "slow")
sg.AddEdge("fast", "merge")
sg.AddEdge("slow", "merge")
sg.AddEdge("merge", End)
compiled, err := sg.Compile(
WithNodeTriggerMode(types.NodeTriggerAllPredecessor),
WithRecursionLimit(10),
)
if err != nil {
t.Fatal(err)
}
done := make(chan error, 1)
go func() {
_, err := compiled.Invoke(context.Background(), map[string]any{})
done <- err
}()
select {
case <-slowStarted:
case <-time.After(time.Second):
t.Fatal("slow branch never started")
}
select {
case <-fastDone:
case <-time.After(time.Second):
t.Fatal("fast branch never completed")
}
select {
case <-mergeRan:
t.Fatal("BUG: merge ran before slow branch completed")
case <-time.After(50 * time.Millisecond):
}
close(releaseSlow)
select {
case err = <-done:
case <-time.After(time.Second):
t.Fatal("compiled graph did not finish after slow branch release")
}
if err != nil {
t.Fatal(err)
}
if len(order) != 4 {
t.Errorf("expected 4 nodes (dispatch+fast+slow+merge), got %d: %v", len(order), order)
}
slowIdx, mergeIdx := -1, -1
for i, name := range order {
switch name {
case "slow":
slowIdx = i
case "merge":
mergeIdx = i
}
}
if mergeIdx < slowIdx {
t.Errorf("BUG: merge before slow branch. merge=%d, slow=%d", mergeIdx, slowIdx)
} else {
t.Log("DAG slow-branch merge: merge correctly waited for slow branch")
}
}