mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-17 13:17:20 +08:00
### What problem does this PR solve? - Tools management - Pregel engine wrapper for better usage - UT race - Coding style ### Type of change - [x] Refactoring
472 lines
12 KiB
Go
472 lines
12 KiB
Go
// Package pregel provides subgraph support for Pregel.
|
|
package pregel
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"ragflow/internal/harness/graph/channels"
|
|
"ragflow/internal/harness/graph/checkpoint"
|
|
"ragflow/internal/harness/graph/constants"
|
|
"ragflow/internal/harness/graph/types"
|
|
)
|
|
|
|
func TestSubgraphManagerCreation(t *testing.T) {
|
|
parentEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
manager := NewSubgraphManager(parentEngine)
|
|
|
|
if manager == nil {
|
|
t.Fatal("Expected non-nil manager")
|
|
}
|
|
|
|
if manager.parentEngine != parentEngine {
|
|
t.Error("Expected parent engine to match")
|
|
}
|
|
|
|
if len(manager.subgraphs) != 0 {
|
|
t.Errorf("Expected empty subgraphs, got %d", len(manager.subgraphs))
|
|
}
|
|
}
|
|
|
|
func TestNamespaceStack(t *testing.T) {
|
|
manager := &SubgraphManager{
|
|
namespaceStack: make([]string, 0),
|
|
}
|
|
|
|
// Test pushing namespaces
|
|
manager.PushNamespace("subgraph1")
|
|
if len(manager.namespaceStack) != 1 {
|
|
t.Errorf("Expected stack size 1, got %d", len(manager.namespaceStack))
|
|
}
|
|
if manager.CurrentNamespace() != "subgraph1" {
|
|
t.Errorf("Expected current namespace 'subgraph1', got %s", manager.CurrentNamespace())
|
|
}
|
|
|
|
manager.PushNamespace("subgraph2")
|
|
if len(manager.namespaceStack) != 2 {
|
|
t.Errorf("Expected stack size 2, got %d", len(manager.namespaceStack))
|
|
}
|
|
if manager.CurrentNamespace() != "subgraph2" {
|
|
t.Errorf("Expected current namespace 'subgraph2', got %s", manager.CurrentNamespace())
|
|
}
|
|
|
|
// Test popping namespaces
|
|
manager.PopNamespace()
|
|
if len(manager.namespaceStack) != 1 {
|
|
t.Errorf("Expected stack size 1, got %d", len(manager.namespaceStack))
|
|
}
|
|
if manager.CurrentNamespace() != "subgraph1" {
|
|
t.Errorf("Expected current namespace 'subgraph1', got %s", manager.CurrentNamespace())
|
|
}
|
|
|
|
manager.PopNamespace()
|
|
if len(manager.namespaceStack) != 0 {
|
|
t.Errorf("Expected empty stack, got %d", len(manager.namespaceStack))
|
|
}
|
|
if manager.CurrentNamespace() != "" {
|
|
t.Errorf("Expected empty namespace, got %s", manager.CurrentNamespace())
|
|
}
|
|
}
|
|
|
|
func TestBuildNamespacePath(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
stack []string
|
|
expectedPath string
|
|
}{
|
|
{
|
|
name: "empty stack",
|
|
stack: []string{},
|
|
expectedPath: "",
|
|
},
|
|
{
|
|
name: "single namespace",
|
|
stack: []string{"subgraph1"},
|
|
expectedPath: "subgraph1",
|
|
},
|
|
{
|
|
name: "multiple namespaces",
|
|
stack: []string{"subgraph1", "subgraph2", "subgraph3"},
|
|
expectedPath: "subgraph1|subgraph2|subgraph3",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
manager := &SubgraphManager{
|
|
namespaceStack: tt.stack,
|
|
}
|
|
|
|
path := manager.BuildNamespacePath()
|
|
if path != tt.expectedPath {
|
|
t.Errorf("Expected path '%s', got '%s'", tt.expectedPath, path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNamespaceIsolatedRegistry(t *testing.T) {
|
|
baseRegistry := channels.NewRegistry()
|
|
|
|
// Register some channels in base registry
|
|
ch1 := channels.NewAnyValue(nil)
|
|
baseRegistry.Register("base_channel1", ch1)
|
|
|
|
// Create isolated registry
|
|
isolated := NewNamespaceIsolatedRegistry(baseRegistry, "subgraph1")
|
|
|
|
// Test get with namespace prefix
|
|
_, exists := isolated.Get("channel1")
|
|
if exists {
|
|
t.Error("Expected non-existent channel")
|
|
}
|
|
|
|
// Register in isolated registry
|
|
ch2 := channels.NewAnyValue(nil)
|
|
if err := isolated.Register("isolated_channel1", ch2); err != nil {
|
|
t.Fatalf("Failed to register isolated channel: %v", err)
|
|
}
|
|
|
|
// Get should work with full namespace prefix
|
|
_, exists = isolated.Get("isolated_channel1")
|
|
if !exists {
|
|
t.Error("Expected channel to exist")
|
|
}
|
|
}
|
|
|
|
func TestRecursiveSubgraphExecutor(t *testing.T) {
|
|
parentEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
manager := NewSubgraphManager(parentEngine)
|
|
executor := NewRecursiveSubgraphExecutor(manager, 5)
|
|
|
|
t.Run("depth limit enforcement", func(t *testing.T) {
|
|
// Try to exceed max depth
|
|
_, err := executor.executeRecursive(context.Background(), "test", "input", 10)
|
|
if err == nil {
|
|
t.Error("Expected depth limit error")
|
|
}
|
|
})
|
|
|
|
t.Run("normal execution", func(t *testing.T) {
|
|
// This would need proper graph setup for full test
|
|
_, err := executor.executeRecursive(context.Background(), "test", "input", 0)
|
|
if err == nil {
|
|
// Expected - node doesn't exist
|
|
t.Log("Expected error for missing node (ok)")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCheckpointMigration(t *testing.T) {
|
|
mockCheckpointer := &MockCheckpointSaver{
|
|
checkpoints: make(map[string]map[string]any),
|
|
tuples: make(map[string]*checkpoint.CheckpointTuple),
|
|
mu: sync.RWMutex{},
|
|
}
|
|
|
|
parentEngine := &Engine{
|
|
checkpointer: mockCheckpointer,
|
|
}
|
|
|
|
manager := NewSubgraphManager(parentEngine)
|
|
migration := NewCheckpointMigration(manager, mockCheckpointer)
|
|
|
|
t.Run("migrate to subgraph", func(t *testing.T) {
|
|
manager.PushNamespace("subgraph1")
|
|
ctx := context.Background()
|
|
|
|
// Create parent checkpoint
|
|
parentCP := map[string]any{
|
|
"channel1": "value1",
|
|
"channel2": "value2",
|
|
}
|
|
|
|
mockCheckpointer.tuples["parent"] = &checkpoint.CheckpointTuple{
|
|
Checkpoint: &checkpoint.Checkpoint{
|
|
State: parentCP,
|
|
},
|
|
}
|
|
|
|
// Migrate to subgraph
|
|
_, err := migration.MigrateToSubgraph(ctx, "thread1", "parent", "subgraph1")
|
|
if err != nil {
|
|
t.Errorf("MigrateToSubgraph failed: %v", err)
|
|
}
|
|
|
|
// Check namespace tracking
|
|
ns, exists := manager.checkpointNS["thread1"]
|
|
if !exists {
|
|
t.Error("Expected checkpoint namespace to be tracked")
|
|
}
|
|
expectedNS := "subgraph1" + string(constants.NSSep) + "subgraph1"
|
|
if ns != expectedNS {
|
|
t.Errorf("Expected namespace '%s', got '%s'", expectedNS, ns)
|
|
}
|
|
|
|
manager.PopNamespace()
|
|
})
|
|
|
|
t.Run("migrate from subgraph", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create subgraph checkpoint
|
|
subgraphCP := map[string]any{
|
|
"channel1": "updated_value1",
|
|
"channel2": "updated_value2",
|
|
}
|
|
|
|
mockCheckpointer.tuples["subgraph"] = &checkpoint.CheckpointTuple{
|
|
Checkpoint: &checkpoint.Checkpoint{
|
|
State: subgraphCP,
|
|
},
|
|
}
|
|
|
|
// Set checkpoint namespace
|
|
manager.checkpointNS["thread1"] = "subgraph1"
|
|
|
|
// Migrate from subgraph
|
|
err := migration.MigrateFromSubgraph(ctx, "thread1", "subgraph")
|
|
if err != nil {
|
|
t.Errorf("MigrateFromSubgraph failed: %v", err)
|
|
}
|
|
|
|
// Check namespace removed
|
|
_, exists := manager.checkpointNS["thread1"]
|
|
if exists {
|
|
t.Error("Expected checkpoint namespace to be removed")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestResolveParentCommand(t *testing.T) {
|
|
manager := NewSubgraphManager(&Engine{})
|
|
|
|
t.Run("resolve with namespace", func(t *testing.T) {
|
|
manager.PushNamespace("subgraph1")
|
|
|
|
cmd := &types.Command{
|
|
Goto: types.Parent,
|
|
}
|
|
|
|
resolved, err := manager.ResolveParentCommand(context.Background(), cmd)
|
|
if err != nil {
|
|
t.Errorf("ResolveParentCommand failed: %v", err)
|
|
}
|
|
|
|
if resolved.Goto != types.Parent {
|
|
t.Error("Expected Parent goto")
|
|
}
|
|
|
|
manager.PopNamespace()
|
|
})
|
|
|
|
t.Run("resolve at root", func(t *testing.T) {
|
|
cmd := &types.Command{
|
|
Goto: types.Parent,
|
|
}
|
|
|
|
_, err := manager.ResolveParentCommand(context.Background(), cmd)
|
|
if err == nil {
|
|
t.Error("Expected error at root namespace")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCreateSubgraph(t *testing.T) {
|
|
parentEngine := &Engine{
|
|
config: types.NewRunnableConfig(),
|
|
}
|
|
|
|
manager := NewSubgraphManager(parentEngine)
|
|
|
|
// Create a simple graph for subgraph
|
|
graph := NewMockGraph()
|
|
graph.AddNode("node1", &MockNode{name: "node1"})
|
|
graph.SetEntryPoint("node1")
|
|
|
|
t.Run("create subgraph", func(t *testing.T) {
|
|
config := &SubgraphConfig{
|
|
Name: "subgraph1",
|
|
ParentEngine: parentEngine,
|
|
Graph: graph,
|
|
}
|
|
|
|
subgraph, err := manager.CreateSubgraph(config)
|
|
if err != nil {
|
|
t.Fatalf("CreateSubgraph failed: %v", err)
|
|
}
|
|
|
|
if subgraph == nil {
|
|
t.Fatal("Expected non-nil subgraph")
|
|
}
|
|
|
|
// Check that subgraph is registered in manager
|
|
_, exists := manager.GetSubgraph("subgraph1")
|
|
if !exists {
|
|
t.Error("Expected subgraph to be registered in manager")
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate subgraph", func(t *testing.T) {
|
|
config := &SubgraphConfig{
|
|
Name: "subgraph1",
|
|
ParentEngine: parentEngine,
|
|
Graph: graph,
|
|
}
|
|
|
|
_, err := manager.CreateSubgraph(config)
|
|
if err == nil {
|
|
t.Error("Expected error for duplicate subgraph")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetSubgraph(t *testing.T) {
|
|
parentEngine := &Engine{}
|
|
manager := NewSubgraphManager(parentEngine)
|
|
|
|
// Create a subgraph
|
|
graph := NewMockGraph()
|
|
graph.AddNode("node1", &MockNode{name: "node1"})
|
|
graph.SetEntryPoint("node1")
|
|
|
|
config := &SubgraphConfig{
|
|
Name: "test_subgraph",
|
|
ParentEngine: parentEngine,
|
|
Graph: graph,
|
|
}
|
|
|
|
subgraph, err := manager.CreateSubgraph(config)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create subgraph: %v", err)
|
|
}
|
|
|
|
t.Run("get existing subgraph", func(t *testing.T) {
|
|
retrieved, exists := manager.GetSubgraph("test_subgraph")
|
|
if !exists {
|
|
t.Error("Expected subgraph to exist")
|
|
}
|
|
if retrieved != subgraph {
|
|
t.Error("Retrieved subgraph doesn't match")
|
|
}
|
|
})
|
|
|
|
t.Run("get non-existent subgraph", func(t *testing.T) {
|
|
_, exists := manager.GetSubgraph("non_existent")
|
|
if exists {
|
|
t.Error("Expected subgraph to not exist")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNamespaceSeparator(t *testing.T) {
|
|
// Verify constants.NSSep is set correctly
|
|
expectedSep := "|"
|
|
|
|
if string(constants.NSSep) != expectedSep {
|
|
t.Errorf("Expected NSSep '%s', got '%s'", expectedSep, string(constants.NSSep))
|
|
}
|
|
|
|
// Test path building with NSSep
|
|
manager := &SubgraphManager{}
|
|
manager.PushNamespace("ns1")
|
|
manager.PushNamespace("ns2")
|
|
|
|
path := manager.BuildNamespacePath()
|
|
expectedPath := "ns1|ns2"
|
|
|
|
if path != expectedPath {
|
|
t.Errorf("Expected path '%s', got '%s'", expectedPath, path)
|
|
}
|
|
}
|
|
|
|
// MockCheckpointSaver is a mock implementation of CheckpointSaver for testing.
|
|
type MockCheckpointSaver struct {
|
|
checkpoints map[string]map[string]any
|
|
tuples map[string]*checkpoint.CheckpointTuple
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) Put(ctx context.Context, config map[string]any, checkpoint map[string]any) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
id, _ := config["checkpoint_id"].(string)
|
|
m.checkpoints[id] = checkpoint
|
|
return nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) Get(ctx context.Context, config map[string]any) (map[string]any, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
id, _ := config["checkpoint_id"].(string)
|
|
cp, exists := m.checkpoints[id]
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
return cp, nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) List(ctx context.Context, config map[string]any, limit int) ([]map[string]any, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) PutWrites(ctx context.Context, config map[string]any, writes []*checkpoint.PendingWrite) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) GetTuple(ctx context.Context, config map[string]any) (*checkpoint.CheckpointTuple, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
id, _ := config["checkpoint_id"].(string)
|
|
tuple, exists := m.tuples[id]
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
return tuple, nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) GetLineage(ctx context.Context, threadID string) ([]*checkpoint.LineageEntry, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MockCheckpointSaver) DeleteThread(ctx context.Context, threadID string) error {
|
|
return nil
|
|
}
|
|
|
|
// MockNode is a mock implementation of Node for testing.
|
|
type MockNode struct {
|
|
name string
|
|
}
|
|
|
|
func (n *MockNode) Invoke(ctx context.Context, input any) (any, error) {
|
|
return map[string]any{
|
|
"output": n.name + "_result",
|
|
}, nil
|
|
}
|
|
|
|
// MockGraph is a mock implementation of a graph for testing.
|
|
type MockGraph struct{}
|
|
|
|
func (g *MockGraph) AddNode(name string, node any) {
|
|
// Mock implementation
|
|
}
|
|
|
|
func (g *MockGraph) SetEntryPoint(node string) {
|
|
// Mock implementation
|
|
}
|
|
|
|
// NewMockGraph creates a new mock graph.
|
|
func NewMockGraph() *MockGraph {
|
|
return &MockGraph{}
|
|
}
|