package core import ( "context" "testing" "time" "ragflow/internal/harness/core/schema" ) // ======================== Tests: Sequential Workflow ======================== func TestWorkflow_SequentialAgent(t *testing.T) { m1 := &mockModel{} m1.addResp("A1") m2 := &mockModel{} m2.addResp("A2") a1 := reActAgentSetup(m1, nil) a1.name = "seq_a1" a2 := reActAgentSetup(m2, nil) a2.name = "seq_a2" ctx := context.Background() wf, err := NewSequential(ctx, &SequentialConfig{ Name: "seq", Description: "test", SubAgents: []Agent{a1, a2}, }) if err != nil { t.Fatalf("NewSequential: %v", err) } iter := wf.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("run")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events from sequential workflow") } } func TestWorkflow_ParallelAgent(t *testing.T) { m1 := &mockModel{} m1.addResp("P1") m2 := &mockModel{} m2.addResp("P2") a1 := reActAgentSetup(m1, nil) a1.name = "par_a1" a2 := reActAgentSetup(m2, nil) a2.name = "par_a2" ctx := context.Background() wf, err := NewParallel(ctx, &ParallelConfig{ Name: "par", Description: "test", SubAgents: []Agent{a1, a2}, }) if err != nil { t.Fatalf("NewParallel: %v", err) } iter := wf.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("run")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events from parallel workflow") } } func TestWorkflow_NestedParallel(t *testing.T) { m1 := &mockModel{} m1.addResp("inner1") m2 := &mockModel{} m2.addResp("inner2") m3 := &mockModel{} m3.addResp("outer") a1 := reActAgentSetup(m1, nil) a1.name = "inner_a" a2 := reActAgentSetup(m2, nil) a2.name = "inner_b" innerPar, err := NewParallel(context.Background(), &ParallelConfig{ Name: "inner-par", Description: "inner parallel", SubAgents: []Agent{a1, a2}, }) if err != nil { t.Fatalf("NewParallel: %v", err) } a3 := reActAgentSetup(m3, nil) a3.name = "outer" wf, err := NewSequential(context.Background(), &SequentialConfig{ Name: "nested", Description: "nested parallel", SubAgents: []Agent{innerPar, a3}, }) if err != nil { t.Fatalf("NewSequential: %v", err) } iter := wf.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("nested")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events from nested workflow") } t.Logf("nested workflow: %d events", len(events)) } func TestWorkflow_LoopAgent(t *testing.T) { m := &mockModel{} m.addResp("loop body") body := reActAgentSetup(m, nil) body.name = "loop_body" ctx := context.Background() wf, err := NewLoop(ctx, &LoopConfig{ Name: "loop", Description: "test", SubAgents: []Agent{body}, MaxIterations: 3, }) if err != nil { t.Fatalf("NewLoop: %v", err) } iter := wf.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("iterate")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events from loop workflow") } } func TestWorkflow_UnsupportedMode(t *testing.T) { wf := &workflowAgent{name: "bad", mode: workflowModeUnknown} iter := wf.Run(context.Background(), &AgentInput{}) ev, ok := iter.Next() if !ok { t.Fatal("expected an event") } if ev.Err == nil { t.Error("expected error for unsupported mode") } else if ev.Err.Error() != "unsupported mode 0" { t.Errorf("expected 'unsupported mode 0', got %v", ev.Err) } } // ======================== Tests: Agentic Integration ======================== func TestAgenticIntegration_BasicGenerate(t *testing.T) { model := &mockModel{} model.addResp("Hello!") agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: model}).WithName("e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("Hi")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Fatal("expected events") } } func TestAgenticIntegration_ToolInvocation(t *testing.T) { model := &mockModel{} model.addResp("I'll use a tool") model.addResp("Here are results") tool := &mockTool{name: "search", desc: "search tool"} agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: []Tool{tool}, }).WithName("tool_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("search something")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("tool integration: %d events", len(events)) } func TestAgenticIntegration_StreamingOutput(t *testing.T) { model := &mockModel{} model.addResp("streaming response") agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: model}).WithName("stream_e2e") ctx := context.Background() iter := agent.Run(ctx, &AgentInput{Messages: []Message{schema.UserMessage("stream test")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } } func TestAgenticIntegration_EmptyInput(t *testing.T) { model := &mockModel{} model.addResp("response") agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: model}).WithName("empty") ctx := context.Background() iter := agent.Run(ctx, &AgentInput{}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events even with empty input") } } // ======================== Tool Calling Integration Tests ======================== func TestAgenticIntegration_ToolInvokeMiddlewareChain(t *testing.T) { model := &mockModel{} model.addResp("I'll call a tool") model.addResp("Done") tool := &mockTool{name: "search", desc: "search"} agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: []Tool{tool}, ToolsConfig: &ToolsNodeConfig{ Tools: []Tool{tool}, ToolInvokeMiddlewares: []ToolInvokeMiddleware{ NewTimeoutToolMiddleware(5 * time.Second), }, }, }).WithName("mw_chain_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("search")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("mw chain integration: %d events", len(events)) } func TestAgenticIntegration_ReflectToolAgent(t *testing.T) { weatherTool, err := ReflectTool("get_weather", "Get weather", func(ctx context.Context, args *weatherArgs) (string, error) { return "Weather in " + args.City + ": 22°C", nil }) if err != nil { t.Fatalf("ReflectTool: %v", err) } model := &mockModel{} model.addResp("Let me check weather") model.addResp("Done") agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: []Tool{weatherTool}, }).WithName("reflect_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("weather in Tokyo")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("reflect tool e2e: %d events", len(events)) } func TestAgenticIntegration_ToolRegistryAgent(t *testing.T) { r := NewToolRegistry() searchTool := MustReflectTool("web_search", "Search web", func(ctx context.Context, args *weatherArgs) (string, error) { return "Search results for " + args.City, nil }) r.Register(searchTool, WithAlias("search"), WithCategory("web")) model := &mockModel{} model.addResp("I'll search") model.addResp("Results ready") agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: r.ToSlice(), }).WithName("registry_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("search for London")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("registry e2e: %d events", len(events)) } func TestAgenticIntegration_RetryToolMiddleware(t *testing.T) { model := &mockModel{} model.addResp("Calling tool") model.addResp("Finally done") tool := &mockTool{name: "flakey_tool", desc: "might fail"} agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: []Tool{tool}, ToolsConfig: &ToolsNodeConfig{ Tools: []Tool{tool}, ToolInvokeMiddlewares: []ToolInvokeMiddleware{ NewRetryToolMiddleware(&ToolRetryConfig{ MaxAttempts: 2, Backoff: time.Millisecond, IsRetryable: func(err error) bool { return true }, }), }, }, }).WithName("retry_tool_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("run")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("retry tool e2e: %d events", len(events)) } func TestAgenticIntegration_ToolFallback(t *testing.T) { model := &mockModel{} model.addResp("Using primary tool") model.addResp("Fallback complete") primary := &mockTool{name: "primary", desc: "primary"} agent := NewReActAgent(&ReActConfig[*schema.Message]{ Model: model, Tools: []Tool{primary}, ToolsConfig: &ToolsNodeConfig{ Tools: []Tool{primary}, ToolInvokeMiddlewares: []ToolInvokeMiddleware{ NewFallbackToolMiddleware(func(ctx context.Context, args *schema.ToolArgument) (*schema.ToolResult, error) { return &schema.ToolResult{Content: "fallback result", ToolCallID: args.CallID}, nil }), }, }, }).WithName("fallback_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("run")}}) events := drainAgentEvents(t, iter) if len(events) == 0 { t.Error("expected events") } t.Logf("fallback e2e: %d events", len(events)) } func TestAgenticIntegration_ModelErrorRecovery(t *testing.T) { model := &countingModelForRetry{failTimes: 2} cfg := &ModelRetryConfig{MaxRetries: 5, IsRetryAble: func(_ context.Context, err error) bool { return true }} wrapped := WithModelRetry(model, cfg) agent := NewReActAgent(&ReActConfig[*schema.Message]{Model: wrapped}).WithName("retry_e2e") iter := agent.Run(context.Background(), &AgentInput{Messages: []Message{schema.UserMessage("retry test")}}) _ = drainAgentEvents(t, iter) }