Files
ragflow/internal/harness/graph/pregel/subgraph_test.go
Yingfeng 706e0d2d06 Refactor harness framework (#16271)
### What problem does this PR solve?

- Tools management
- Pregel engine wrapper for better usage
- UT race
- Coding style

### Type of change

- [x] Refactoring
2026-06-23 20:18:04 +08:00

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{}
}