- 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.
300 lines
9.1 KiB
Go
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
|
|
}
|