Files
ragflow/internal/harness/graph/graph/graph_advanced_fault_edge_test.go
Yingfeng 5b0b86c276 More resilient graph engine (#16325)
### 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
2026-06-24 23:05:07 +08:00

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"])
}
}