// Package pregel provides Pregel algorithm optimizations for graph execution. package pregel import ( "context" "testing" "ragflow/internal/harness/graph/channels" "ragflow/internal/harness/graph/types" ) func TestPriorityTaskQueue(t *testing.T) { pq := NewPriorityTaskQueue() // Test push and pop task1 := &Task{Name: "task1", Path: []string{"a"}} task2 := &Task{Name: "task2", Path: []string{"b"}} task3 := &Task{Name: "task3", Path: []string{"a", "b"}} pq.Push(task1) pq.Push(task2) pq.Push(task3) if pq.Len() != 3 { t.Errorf("Expected 3 tasks, got %d", pq.Len()) } // Test pop - should return shortest path first popped := pq.Pop() if popped == nil { t.Fatal("Expected non-nil popped task") } // task1 has path ["a"] which is shorter than task3's ["a","b"] if popped.Name != "task1" && popped.Name != "task2" { t.Errorf("Expected task1 or task2, got %s", popped.Name) } // Check queue size if pq.Len() != 2 { t.Errorf("Expected 2 tasks remaining, got %d", pq.Len()) } } func TestPriorityTaskQueueOrdering(t *testing.T) { pq := NewPriorityTaskQueue() // Add tasks with different path lengths tasks := []*Task{ {Name: "deep1", Path: []string{"a", "b", "c"}}, {Name: "shallow1", Path: []string{"a"}}, {Name: "deep2", Path: []string{"a", "b"}}, {Name: "shallow2", Path: []string{"b"}}, } for _, task := range tasks { pq.Push(task) } // Pop in priority order // Shallow paths should come first order := make([]string, 0, 4) for i := 0; i < 4; i++ { task := pq.Pop() if task != nil { order = append(order, task.Name) } } // Verify shallow tasks come first if order[0] != "shallow1" && order[0] != "shallow2" { t.Errorf("Expected shallow task first, got %s", order[0]) } } func TestTaskPriorityComparison(t *testing.T) { tests := []struct { name string tp1 *TaskPriority tp2 *TaskPriority expected int }{ { name: "different priorities", tp1: NewTaskPriority("t1", []string{"a"}, 2), tp2: NewTaskPriority("t2", []string{"b"}, 1), expected: 1, // t1 has higher priority (lower number) }, { name: "different path lengths", tp1: NewTaskPriority("t1", []string{"a"}, 0), tp2: NewTaskPriority("t2", []string{"a", "b"}, 0), expected: -1, // t1 has shorter path }, { name: "same path length, lexicographic", tp1: NewTaskPriority("t1", []string{"a"}, 0), tp2: NewTaskPriority("t2", []string{"b"}, 0), expected: -1, // t1 comes first alphabetically }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := tt.tp1.Compare(tt.tp2) if result != tt.expected { t.Errorf("Compare() = %d, expected %d", result, tt.expected) } }) } } func TestPregelOptimizedEngineCreation(t *testing.T) { baseEngine := &Engine{ config: types.NewRunnableConfig(), } optimized := NewPregelOptimizedEngine(baseEngine, nil) if optimized == nil { t.Fatal("Expected non-nil optimized engine") } if optimized.Engine != baseEngine { t.Error("Expected base engine to match") } if !optimized.config.BumpStep { t.Error("Expected bump_step to be enabled") } if !optimized.config.FinishNotification { t.Error("Expected finish notification to be enabled") } if optimized.taskPriorityQueue == nil { t.Error("Expected task queue to be initialized") } } func TestFinishNotification(t *testing.T) { t.Run("send finish notification", func(t *testing.T) { // This would test actual notification sending // For now, just verify the structure notification := &FinishNotification{ TaskName: "test_node", Output: map[string]any{"result": "success"}, Step: 1, Namespace: "test_namespace", } if notification.TaskName != "test_node" { t.Errorf("Expected task name 'test_node', got %s", notification.TaskName) } if notification.Step != 1 { t.Errorf("Expected step 1, got %d", notification.Step) } if notification.Namespace != "test_namespace" { t.Errorf("Expected namespace 'test_namespace', got %s", notification.Namespace) } }) } func TestBumpStep(t *testing.T) { baseEngine := &Engine{ config: types.NewRunnableConfig(), } optimized := NewPregelOptimizedEngine(baseEngine, nil) t.Run("bump step for dependent tasks", func(t *testing.T) { ctx := context.Background() // Simulate task completion updatedChannels := map[string]struct{}{ "channel1": {}, "channel2": {}, } err := optimized.BumpStep(ctx, "task1", 1, updatedChannels) if err != nil { t.Errorf("BumpStep failed: %v", err) } // Verify task is marked as finished if !optimized.finishedTasks["task1"] { t.Error("Expected task1 to be marked as finished") } }) } func TestCompareTaskPriority(t *testing.T) { optimized := NewPregelOptimizedEngine(&Engine{}, nil) tests := []struct { name string t1 *Task t2 *Task expected int }{ { name: "different path lengths", t1: &Task{Name: "a", Path: []string{"a"}}, t2: &Task{Name: "b", Path: []string{"a", "b"}}, expected: -1, // Shorter path first }, { name: "same path length, lexicographic", t1: &Task{Name: "a", Path: []string{"a"}}, t2: &Task{Name: "b", Path: []string{"a"}}, expected: -1, // Alphabetical first }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := optimized.compareTaskPriority(tt.t1, tt.t2) if result != tt.expected { t.Errorf("compareTaskPriority() = %d, expected %d", result, tt.expected) } }) } } func TestIsNodeReady(t *testing.T) { optimized := NewPregelOptimizedEngine(&Engine{}, nil) t.Run("node with no dependencies", func(t *testing.T) { // Nodes with no required channels are always ready ready := optimized.isNodeReady("node1") if !ready { t.Error("Expected node to be ready with no dependencies") } }) t.Run("node with seen channels", func(t *testing.T) { // Mark channels as ready optimized.readyChannels["channel1"] = true optimized.readyChannels["channel2"] = true // This would need to be set up with proper dependencies // For now, just test the mechanism _ = optimized.isNodeReady("node1") }) } func TestGetDependencies(t *testing.T) { optimized := NewPregelOptimizedEngine(&Engine{}, nil) t.Run("get dependencies", func(t *testing.T) { // This would require proper graph setup // For now, test that the function exists deps := optimized.getDependencies("task1") if deps == nil { t.Log("getDependencies returns nil (expected for empty graph)") } }) } func TestHasSeenChannel(t *testing.T) { optimized := NewPregelOptimizedEngine(&Engine{}, nil) t.Run("channel not seen", func(t *testing.T) { seen := optimized.hasSeenChannel("task1", "channel1") if seen { t.Error("Expected channel to not be seen") } }) t.Run("channel seen", func(t *testing.T) { // Initialize seen channels for task optimized.seenChannels["task1"] = map[string]bool{ "channel1": true, } seen := optimized.hasSeenChannel("task1", "channel1") if !seen { t.Error("Expected channel to be seen") } }) } func TestGetTriggersForNode(t *testing.T) { optimized := NewPregelOptimizedEngine(&Engine{}, nil) t.Run("get triggers", func(t *testing.T) { // This would require proper graph setup // For now, test that the function exists triggers := optimized.getTriggersForNode("node1") if triggers == nil { t.Log("getTriggersForNode returns nil (expected for empty graph)") } }) } func TestNewTaskPriority(t *testing.T) { tp := NewTaskPriority("test_task", []string{"a", "b"}, 1) if tp == nil { t.Fatal("Expected non-nil task priority") } if tp.Name != "test_task" { t.Errorf("Expected name 'test_task', got %s", tp.Name) } if len(tp.Path) != 2 { t.Errorf("Expected path length 2, got %d", len(tp.Path)) } if tp.Priority != 1 { t.Errorf("Expected priority 1, got %d", tp.Priority) } } func TestPriorityQueueHeap(t *testing.T) { pq := NewPriorityTaskQueue() // Test heap property maintenance tasks := []*Task{ {Name: "z", Path: []string{"z"}}, {Name: "m", Path: []string{"m"}}, {Name: "a", Path: []string{"a"}}, {Name: "n", Path: []string{"n"}}, } // Add in random order for _, task := range tasks { pq.Push(task) } // Verify heap property: each parent should have higher priority than children // This is a simplified check - in practice we'd verify the full heap property if pq.Len() != 4 { t.Errorf("Expected 4 tasks in queue, got %d", pq.Len()) } } func TestOptimizedApplyWrites(t *testing.T) { baseEngine := &Engine{ config: types.NewRunnableConfig(), } optimized := NewPregelOptimizedEngine(baseEngine, nil) t.Run("apply writes with bump_step", func(t *testing.T) { ctx := context.Background() // Create a registry with a channel registry := channels.NewRegistry() ch := channels.NewAnyValue(nil) registry.Register("key", ch) results := []*TaskResult{ {Name: "task1", Output: map[string]any{"key": "value"}}, } updatedChannels, err := optimized.OptimizedApplyWrites( ctx, registry, results, 1, map[string]struct{}{"trigger": {}}, ) if err != nil { t.Logf("OptimizedApplyWrites error: %v", err) } if updatedChannels == nil { t.Error("Expected non-nil updated channels") } }) } func TestGetCurrentNamespace(t *testing.T) { baseEngine := &Engine{ config: types.NewRunnableConfig(), } baseEngine.config.Set("namespace", "test_ns") optimized := NewPregelOptimizedEngine(baseEngine, nil) ns := optimized.getCurrentNamespace() if ns == "" { t.Log("getCurrentNamespace returns empty (config may not have namespace)") } } func TestPrepareNextTasksOptimized(t *testing.T) { baseEngine := &Engine{ config: types.NewRunnableConfig(), } optimized := NewPregelOptimizedEngine(baseEngine, nil) t.Run("prepare next tasks with optimization", func(t *testing.T) { ctx := context.Background() tasks, triggers, err := optimized.PrepareNextTasksOptimized( ctx, nil, // registry map[string]bool{}, "", nil, // current state ) if err != nil { t.Logf("PrepareNextTasksOptimized error: %v", err) } if tasks == nil { tasks = []*Task{} } if triggers == nil { triggers = map[string]struct{}{} } t.Logf("Prepared %d tasks with %d triggers", len(tasks), len(triggers)) }) }