mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-14 00:48:26 +08:00
### What problem does this PR solve? - OpenTelemetry integration - Checkpoint conformance tests - State inspector API - Callbacks - A series of fault injection tests - Pregel integration tests ### Type of change - [x] Refactoring
364 lines
9.8 KiB
Go
364 lines
9.8 KiB
Go
// Package graph provides advanced fault injection edge cases.
|
|
package graph
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"ragflow/internal/harness/graph/channels"
|
|
"ragflow/internal/harness/graph/checkpoint"
|
|
"ragflow/internal/harness/graph/constants"
|
|
)
|
|
|
|
// ============================================================
|
|
// P0: Node returns different key than channel
|
|
// ============================================================
|
|
|
|
// TestFault_NodeReturnsUnknownKey verifies node returning a key not in channels.
|
|
func TestFault_NodeReturnsUnknownKey(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("producer", func(ctx context.Context, state any) (any, error) {
|
|
return map[string]any{"unknown_key": "value"}, nil
|
|
})
|
|
b.AddNode("consumer", func(ctx context.Context, state any) (any, error) {
|
|
return state, nil
|
|
})
|
|
b.AddEdge(constants.Start, "producer")
|
|
b.AddEdge("producer", "consumer")
|
|
b.AddEdge("consumer", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
// Should not panic — unknown keys are ignored.
|
|
_, err = cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P0: Graph with single node (minimal)
|
|
// ============================================================
|
|
|
|
// TestFault_SingleNodeGraph verifies a graph with exactly one node.
|
|
func TestFault_SingleNodeGraph(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("only", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["result"] = "single"
|
|
return m, nil
|
|
})
|
|
b.AddEdge(constants.Start, "only")
|
|
b.AddEdge("only", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
if m["result"] != "single" {
|
|
t.Fatalf("expected result=single, got %v", m)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P1: Node returns large string
|
|
// ============================================================
|
|
|
|
// TestFault_LargeReturnValue verifies nodes returning large strings.
|
|
func TestFault_LargeReturnValue(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("big", func(ctx context.Context, state any) (any, error) {
|
|
large := ""
|
|
for i := 0; i < 10000; i++ {
|
|
large += "x"
|
|
}
|
|
return map[string]any{"data": large}, nil
|
|
})
|
|
b.AddNode("small", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["done"] = true
|
|
return m, nil
|
|
})
|
|
b.AddEdge(constants.Start, "big")
|
|
b.AddEdge("big", "small")
|
|
b.AddEdge("small", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
if m["done"] != true {
|
|
t.Fatalf("expected done=true, got %v", m)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P1: Chain with deep state mutation
|
|
// ============================================================
|
|
|
|
// TestFault_DeepStateMutation verifies a chain that accumulates state.
|
|
func TestFault_DeepStateMutation(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
prev := constants.Start
|
|
for i := 0; i < 20; i++ {
|
|
name := fmt.Sprintf("n_%d", i)
|
|
b.AddNode(name, func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
if v, ok := m["depth"]; ok {
|
|
m["depth"] = v.(int) + 1
|
|
} else {
|
|
m["depth"] = 1
|
|
}
|
|
return m, nil
|
|
})
|
|
b.AddEdge(prev, name)
|
|
prev = name
|
|
}
|
|
b.AddEdge(prev, constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
if m["depth"].(int) != 20 {
|
|
t.Fatalf("expected depth=20, got %v", m["depth"])
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: Branching with conditional on value not present
|
|
// ============================================================
|
|
|
|
// TestFault_ConditionalEdge_MissingKey verifies conditional routing
|
|
// when the routing key is missing from state.
|
|
func TestFault_ConditionalEdge_MissingKey(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("router", func(ctx context.Context, state any) (any, error) {
|
|
return state, nil
|
|
})
|
|
b.AddNode("default", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["route"] = "default"
|
|
return m, nil
|
|
})
|
|
b.AddEdge(constants.Start, "router")
|
|
b.AddConditionalEdges("router",
|
|
func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
if v, ok := m["route"]; ok {
|
|
return v, nil
|
|
}
|
|
return "default", nil
|
|
},
|
|
map[string]string{
|
|
"default": "default",
|
|
},
|
|
)
|
|
b.AddEdge("default", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
if m["route"] != "default" {
|
|
t.Fatalf("expected route=default, got %v", m)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: Checkpoint with concurrent Put/List
|
|
// ============================================================
|
|
|
|
// TestFault_ConcurrentPutList verifies concurrent Put+List on same thread.
|
|
func TestFault_ConcurrentPutList(t *testing.T) {
|
|
ms := checkpoint.NewMemorySaver()
|
|
ctx := context.Background()
|
|
tid := "cp-conc-put-list"
|
|
cfg := map[string]interface{}{constants.ConfigKeyThreadID: tid}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
err := ms.Put(ctx, cfg, map[string]interface{}{"i": idx})
|
|
if err != nil {
|
|
t.Errorf("Put: %v", err)
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
// List should return at least some entries.
|
|
entries, err := ms.List(ctx, cfg, 50)
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
if len(entries) == 0 {
|
|
t.Fatal("expected at least 1 entry")
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: 100 invocations of same graph (stress)
|
|
// ============================================================
|
|
|
|
// TestFault_100SequentialInvocations invokes the same graph 100 times.
|
|
func TestFault_100SequentialInvocations(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("echo", func(ctx context.Context, state any) (any, error) {
|
|
return state, nil
|
|
})
|
|
b.AddEdge(constants.Start, "echo")
|
|
b.AddEdge("echo", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for i := 0; i < 100; i++ {
|
|
_, err := cg.Invoke(ctx, map[string]any{"i": i})
|
|
if err != nil {
|
|
t.Fatalf("invocation %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: Reducer with append across parallel branches
|
|
// ============================================================
|
|
|
|
// TestFault_ParallelAppend verifies parallel branches appending to
|
|
// the same slice (sequential chain simulates this).
|
|
func TestFault_ParallelAppend(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddNode("a", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["items"] = []string{"a"}
|
|
return m, nil
|
|
})
|
|
b.AddNode("b", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
items, _ := m["items"].([]string)
|
|
m["items"] = append(items, "b")
|
|
return m, nil
|
|
})
|
|
b.AddEdge(constants.Start, "a")
|
|
b.AddEdge("a", "b")
|
|
b.AddEdge("b", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
items, ok := m["items"].([]string)
|
|
if !ok || len(items) != 2 {
|
|
t.Fatalf("expected 2 items, got %v", m["items"])
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: EphemeralValue with engine pattern
|
|
// ============================================================
|
|
|
|
// TestFault_AnyValueChannel verifies AnyValue channel.
|
|
func TestFault_AnyValueChannel(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
b.AddChannel("any", channels.NewAnyValue(""))
|
|
|
|
b.AddNode("writer", func(ctx context.Context, state any) (any, error) {
|
|
return map[string]any{"any": 42}, nil
|
|
})
|
|
b.AddNode("reader", func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["read"] = true
|
|
return m, nil
|
|
})
|
|
b.AddEdge(constants.Start, "writer")
|
|
b.AddEdge("writer", "reader")
|
|
b.AddEdge("reader", constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
_, err = cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// P2: Enum-like state values
|
|
// ============================================================
|
|
|
|
// TestFault_EnumStateValue verifies state transitions through phases.
|
|
func TestFault_EnumStateValue(t *testing.T) {
|
|
b := NewStateGraph(map[string]any{})
|
|
phases := []string{"init", "process", "finalize", "done"}
|
|
prev := constants.Start
|
|
for _, phase := range phases {
|
|
p := phase
|
|
b.AddNode(p, func(ctx context.Context, state any) (any, error) {
|
|
m := state.(map[string]any)
|
|
m["phase"] = p
|
|
return m, nil
|
|
})
|
|
b.AddEdge(prev, p)
|
|
prev = p
|
|
}
|
|
b.AddEdge(prev, constants.End)
|
|
|
|
cg, err := b.Compile()
|
|
if err != nil {
|
|
t.Fatalf("Compile: %v", err)
|
|
}
|
|
|
|
result, err := cg.Invoke(context.Background(), map[string]any{})
|
|
if err != nil {
|
|
t.Fatalf("Invoke: %v", err)
|
|
}
|
|
m := result.(map[string]any)
|
|
if m["phase"] != "done" {
|
|
t.Fatalf("expected phase=done, got %v", m["phase"])
|
|
}
|
|
}
|