yao/agent/robot/executor/standard/delivery_test.go
Max 5f5bcba2b2 Refactor delivery event handling and payload structure for improved clarity and functionality
- Update DeliveryPayload to use structured types for Content and Preferences, enhancing type safety and readability.
- Modify tests to reflect changes in payload structure, ensuring proper serialization and deserialization of delivery content.
- Implement a new robotHandler for processing delivery events, streamlining the handling of different delivery channels (email, webhook, process).
- Remove the deprecated DeliveryCenter, consolidating delivery logic within the new handler for better maintainability.
- Enhance error handling and logging during delivery processing to improve observability and debugging capabilities.
2026-02-27 18:19:57 +08:00

300 lines
9.1 KiB
Go

package standard_test
import (
"context"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent/robot/executor/standard"
"github.com/yaoapp/yao/agent/robot/types"
"github.com/yaoapp/yao/agent/testutils"
)
// ============================================================================
// P4 Delivery Phase Tests
// ============================================================================
func TestRunDeliveryBasic(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("generates delivery content from execution results", func(t *testing.T) {
robot := createDeliveryTestRobot(t, "robot.delivery")
exec := createDeliveryTestExecution(robot)
exec.Inspiration = &types.InspirationReport{
Content: "Morning analysis suggests focus on Q4 review.",
}
exec.Goals = &types.Goals{
Content: "## Goals\n1. Review Q4 data\n2. Generate summary report",
}
exec.Tasks = []types.Task{
{ID: "task-001", ExecutorID: "experts.data-analyst", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted},
{ID: "task-002", ExecutorID: "experts.summarizer", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted},
}
exec.Results = []types.TaskResult{
{TaskID: "task-001", Success: true, Duration: 1500, Output: map[string]interface{}{"total_sales": 1500000}},
{TaskID: "task-002", Success: true, Duration: 800, Output: "Q4 sales exceeded expectations by 15%."},
}
e := standard.New()
err := e.RunDelivery(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Delivery)
require.NotNil(t, exec.Delivery.Content)
assert.NotEmpty(t, exec.Delivery.Content.Summary)
assert.NotEmpty(t, exec.Delivery.Content.Body)
assert.True(t, exec.Delivery.Success)
})
t.Run("handles partial failure in results", func(t *testing.T) {
robot := createDeliveryTestRobot(t, "robot.delivery")
exec := createDeliveryTestExecution(robot)
exec.Goals = &types.Goals{
Content: "## Goals\n1. Analyze data\n2. Generate report",
}
exec.Tasks = []types.Task{
{ID: "task-001", ExecutorID: "experts.data-analyst", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted},
{ID: "task-002", ExecutorID: "experts.summarizer", ExecutorType: types.ExecutorAssistant, Status: types.TaskFailed},
}
exec.Results = []types.TaskResult{
{TaskID: "task-001", Success: true, Duration: 1500, Output: map[string]interface{}{"data": "analyzed"}},
{TaskID: "task-002", Success: false, Duration: 500, Error: "Summarization failed: timeout"},
}
e := standard.New()
err := e.RunDelivery(ctx, exec, nil)
require.NoError(t, err)
require.NotNil(t, exec.Delivery)
require.NotNil(t, exec.Delivery.Content)
body := strings.ToLower(exec.Delivery.Content.Body)
hasFailureInfo := strings.Contains(body, "fail") ||
strings.Contains(body, "error") ||
strings.Contains(body, "partial") ||
strings.Contains(body, "✗")
assert.True(t, hasFailureInfo || exec.Delivery.Content.Summary != "", "should mention failure or have valid summary")
})
}
func TestRunDeliveryErrorHandling(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
ctx := types.NewContext(context.Background(), testAuth())
t.Run("returns error when robot is nil", func(t *testing.T) {
exec := &types.Execution{
ID: "test-exec-1",
TriggerType: types.TriggerClock,
}
e := standard.New()
err := e.RunDelivery(ctx, exec, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "robot not found")
})
t.Run("returns error when agent not found", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
Config: &types.Config{
Identity: &types.Identity{Role: "Test"},
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseDelivery: "non.existent.agent",
},
},
},
}
exec := createDeliveryTestExecution(robot)
exec.Results = []types.TaskResult{
{TaskID: "task-001", Success: true, Duration: 100},
}
e := standard.New()
err := e.RunDelivery(ctx, exec, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "call failed")
})
}
// ============================================================================
// Email Channel Config Tests
// ============================================================================
func TestDefaultEmailChannel(t *testing.T) {
t.Run("returns default email channel", func(t *testing.T) {
assert.Equal(t, "default", types.DefaultEmailChannel())
})
t.Run("can set custom email channel", func(t *testing.T) {
original := types.DefaultEmailChannel()
defer types.SetDefaultEmailChannel(original)
types.SetDefaultEmailChannel("custom-email")
assert.Equal(t, "custom-email", types.DefaultEmailChannel())
})
t.Run("ignores empty channel", func(t *testing.T) {
original := types.DefaultEmailChannel()
defer types.SetDefaultEmailChannel(original)
types.SetDefaultEmailChannel("")
assert.Equal(t, original, types.DefaultEmailChannel())
})
}
func TestRobotEmailInDelivery(t *testing.T) {
t.Run("robot email field is loaded from map", func(t *testing.T) {
data := map[string]interface{}{
"member_id": "robot-001",
"team_id": "team-001",
"robot_email": "robot@example.com",
}
robot, err := types.NewRobotFromMap(data)
require.NoError(t, err)
assert.Equal(t, "robot@example.com", robot.RobotEmail)
})
t.Run("robot email can be empty", func(t *testing.T) {
data := map[string]interface{}{
"member_id": "robot-001",
"team_id": "team-001",
}
robot, err := types.NewRobotFromMap(data)
require.NoError(t, err)
assert.Empty(t, robot.RobotEmail)
})
}
// ============================================================================
// FormatDeliveryInput Tests
// ============================================================================
func TestFormatDeliveryInput(t *testing.T) {
formatter := standard.NewInputFormatter()
t.Run("formats complete execution context", func(t *testing.T) {
robot := &types.Robot{
MemberID: "test-robot",
Config: &types.Config{
Identity: &types.Identity{
Role: "Sales Analyst",
Duties: []string{"Analyze data", "Generate reports"},
},
},
}
startTime := time.Now().Add(-5 * time.Minute)
endTime := time.Now()
exec := &types.Execution{
ID: "exec-123",
TriggerType: types.TriggerClock,
Status: types.ExecCompleted,
StartTime: startTime,
EndTime: &endTime,
Inspiration: &types.InspirationReport{
Content: "Morning analysis suggests focus on Q4.",
},
Goals: &types.Goals{
Content: "## Goals\n1. Review Q4 data",
},
Tasks: []types.Task{
{ID: "task-001", ExecutorID: "data-analyst", ExecutorType: types.ExecutorAssistant, Status: types.TaskCompleted, ExpectedOutput: "JSON with sales data"},
},
Results: []types.TaskResult{
{TaskID: "task-001", Success: true, Duration: 1500, Output: map[string]interface{}{"sales": 1000000}},
},
}
result := formatter.FormatDeliveryInput(exec, robot)
assert.Contains(t, result, "## Robot Identity")
assert.Contains(t, result, "Sales Analyst")
assert.Contains(t, result, "## Execution Context")
assert.Contains(t, result, "clock")
assert.Contains(t, result, "## Inspiration (P0)")
assert.Contains(t, result, "Morning analysis")
assert.Contains(t, result, "## Goals (P1)")
assert.Contains(t, result, "Review Q4 data")
assert.Contains(t, result, "## Tasks (P2)")
assert.Contains(t, result, "task-001")
assert.Contains(t, result, "## Results (P3)")
assert.Contains(t, result, "✓ Task: task-001")
})
t.Run("handles empty execution", func(t *testing.T) {
exec := &types.Execution{
ID: "exec-empty",
TriggerType: types.TriggerHuman,
Status: types.ExecPending,
StartTime: time.Now(),
}
result := formatter.FormatDeliveryInput(exec, nil)
assert.Contains(t, result, "## Execution Context")
assert.Contains(t, result, "human")
})
}
// ============================================================================
// Helper Functions
// ============================================================================
func createDeliveryTestRobot(t *testing.T, agentID string) *types.Robot {
t.Helper()
return &types.Robot{
MemberID: "test-robot-1",
TeamID: "test-team-1",
DisplayName: "Test Robot",
Config: &types.Config{
Identity: &types.Identity{
Role: "Test Assistant",
Duties: []string{"Testing", "Validation"},
},
Resources: &types.Resources{
Phases: map[types.Phase]string{
types.PhaseDelivery: agentID,
},
},
},
}
}
func createDeliveryTestExecution(robot *types.Robot) *types.Execution {
exec := &types.Execution{
ID: "test-exec-delivery-1",
MemberID: robot.MemberID,
TeamID: robot.TeamID,
TriggerType: types.TriggerClock,
StartTime: time.Now(),
Status: types.ExecRunning,
Phase: types.PhaseDelivery,
}
exec.SetRobot(robot)
return exec
}