- Added assertions in multiple integration tests to verify that robots are correctly loaded into the cache during various execution scenarios. - Updated tests for clock triggers, concurrent executions, control tests, event triggers, and human interventions to ensure comprehensive coverage of cache functionality. - Improved test reliability by confirming the presence of expected robots in the cache, enhancing overall test robustness.
571 lines
15 KiB
Go
571 lines
15 KiB
Go
package manager_test
|
|
|
|
// Integration tests for Event triggers
|
|
// Tests Manager.HandleEvent() with various event types and scenarios
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/gou/model"
|
|
"github.com/yaoapp/xun/capsule"
|
|
"github.com/yaoapp/yao/agent/robot/manager"
|
|
"github.com/yaoapp/yao/agent/robot/pool"
|
|
"github.com/yaoapp/yao/agent/robot/types"
|
|
"github.com/yaoapp/yao/agent/testutils"
|
|
)
|
|
|
|
// ==================== Event Trigger Tests ====================
|
|
|
|
// TestIntegrationEventTrigger tests event trigger flow
|
|
func TestIntegrationEventTrigger(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupIntegrationRobots(t)
|
|
defer cleanupIntegrationRobots(t)
|
|
|
|
t.Run("webhook event success", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_webhook", "team_integ_event")
|
|
|
|
config := &manager.Config{
|
|
TickInterval: 100 * time.Millisecond,
|
|
PoolConfig: &pool.Config{WorkerSize: 3, QueueSize: 20},
|
|
}
|
|
m := manager.NewWithConfig(config)
|
|
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
// Verify robot is loaded into cache
|
|
robot := m.Cache().Get("robot_integ_event_webhook")
|
|
require.NotNil(t, robot, "Robot should be loaded into cache")
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_webhook",
|
|
Source: "webhook",
|
|
EventType: "lead.created",
|
|
Data: map[string]interface{}{
|
|
"name": "John Doe",
|
|
"email": "john@example.com",
|
|
"company": "Acme Corp",
|
|
},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
assert.Equal(t, types.ExecPending, result.Status)
|
|
assert.Contains(t, result.Message, "webhook")
|
|
assert.Contains(t, result.Message, "lead.created")
|
|
|
|
// Wait for execution
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Verify execution completed
|
|
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1)
|
|
})
|
|
|
|
t.Run("database event success", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_db", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_db",
|
|
Source: "database",
|
|
EventType: "order.paid",
|
|
Data: map[string]interface{}{
|
|
"order_id": "ORD-12345",
|
|
"amount": 1500.00,
|
|
"customer": "customer_001",
|
|
},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
})
|
|
|
|
t.Run("event with complex data", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_complex", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_complex",
|
|
Source: "webhook",
|
|
EventType: "crm.contact.updated",
|
|
Data: map[string]interface{}{
|
|
"contact": map[string]interface{}{
|
|
"id": "contact_001",
|
|
"name": "Jane Smith",
|
|
"email": "jane@example.com",
|
|
"tags": []string{"vip", "enterprise"},
|
|
},
|
|
"changes": map[string]interface{}{
|
|
"old_status": "active",
|
|
"new_status": "premium",
|
|
},
|
|
"timestamp": time.Now().Unix(),
|
|
},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
})
|
|
}
|
|
|
|
// TestIntegrationEventTriggerErrors tests error cases for event triggers
|
|
func TestIntegrationEventTriggerErrors(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupIntegrationRobots(t)
|
|
defer cleanupIntegrationRobots(t)
|
|
|
|
t.Run("robot not found", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_nonexistent",
|
|
Source: "webhook",
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotNotFound, err)
|
|
})
|
|
|
|
t.Run("robot paused", func(t *testing.T) {
|
|
setupEventTestRobotPaused(t, "robot_integ_event_paused", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_paused",
|
|
Source: "webhook",
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrRobotPaused, err)
|
|
})
|
|
|
|
t.Run("event trigger disabled", func(t *testing.T) {
|
|
setupEventTestRobotDisabled(t, "robot_integ_event_disabled", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_disabled",
|
|
Source: "webhook",
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Equal(t, types.ErrTriggerDisabled, err)
|
|
})
|
|
|
|
t.Run("invalid request - empty member_id", func(t *testing.T) {
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "", // Empty
|
|
Source: "webhook",
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "member_id")
|
|
})
|
|
|
|
t.Run("invalid request - empty source", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_nosource", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_nosource",
|
|
Source: "", // Empty
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "source")
|
|
})
|
|
|
|
t.Run("invalid request - empty event_type", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_notype", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_notype",
|
|
Source: "webhook",
|
|
EventType: "", // Empty
|
|
}
|
|
|
|
_, err = m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "event_type")
|
|
})
|
|
|
|
t.Run("manager not started", func(t *testing.T) {
|
|
m := manager.New()
|
|
// Don't start
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_test",
|
|
Source: "webhook",
|
|
EventType: "test.event",
|
|
}
|
|
|
|
_, err := m.HandleEvent(ctx, req)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "not started")
|
|
})
|
|
}
|
|
|
|
// TestIntegrationEventTriggerTypes tests various event types
|
|
func TestIntegrationEventTriggerTypes(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupIntegrationRobots(t)
|
|
defer cleanupIntegrationRobots(t)
|
|
|
|
// Common event types to test
|
|
eventTypes := []struct {
|
|
name string
|
|
eventType string
|
|
data map[string]interface{}
|
|
}{
|
|
{
|
|
name: "lead.created",
|
|
eventType: "lead.created",
|
|
data: map[string]interface{}{"name": "John", "email": "john@example.com"},
|
|
},
|
|
{
|
|
name: "order.paid",
|
|
eventType: "order.paid",
|
|
data: map[string]interface{}{"order_id": "ORD-001", "amount": 100.0},
|
|
},
|
|
{
|
|
name: "customer.signup",
|
|
eventType: "customer.signup",
|
|
data: map[string]interface{}{"customer_id": "cust_001", "plan": "premium"},
|
|
},
|
|
{
|
|
name: "ticket.created",
|
|
eventType: "ticket.created",
|
|
data: map[string]interface{}{"ticket_id": "TKT-001", "priority": "high"},
|
|
},
|
|
{
|
|
name: "inventory.low",
|
|
eventType: "inventory.low",
|
|
data: map[string]interface{}{"product_id": "PRD-001", "quantity": 5},
|
|
},
|
|
}
|
|
|
|
for _, tc := range eventTypes {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
memberID := "robot_integ_event_type_" + tc.name
|
|
setupEventTestRobot(t, memberID, "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: memberID,
|
|
Source: "webhook",
|
|
EventType: tc.eventType,
|
|
Data: tc.data,
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err, "Event type %s should succeed", tc.eventType)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
assert.Equal(t, types.ExecPending, result.Status)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIntegrationEventTriggerSources tests different event sources
|
|
func TestIntegrationEventTriggerSources(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupIntegrationRobots(t)
|
|
defer cleanupIntegrationRobots(t)
|
|
|
|
sources := []string{"webhook", "database", "api", "scheduler", "internal"}
|
|
|
|
for _, source := range sources {
|
|
t.Run("source_"+source, func(t *testing.T) {
|
|
memberID := "robot_integ_event_src_" + source
|
|
setupEventTestRobot(t, memberID, "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: memberID,
|
|
Source: source,
|
|
EventType: "test.event",
|
|
Data: map[string]interface{}{"source": source},
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err, "Source %s should succeed", source)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIntegrationEventTriggerWithEmptyData tests event with empty or nil data
|
|
func TestIntegrationEventTriggerWithEmptyData(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test")
|
|
}
|
|
|
|
testutils.Prepare(t)
|
|
defer testutils.Clean(t)
|
|
|
|
cleanupIntegrationRobots(t)
|
|
defer cleanupIntegrationRobots(t)
|
|
|
|
t.Run("nil data", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_nildata", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_nildata",
|
|
Source: "webhook",
|
|
EventType: "ping",
|
|
Data: nil, // Nil data
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
})
|
|
|
|
t.Run("empty data map", func(t *testing.T) {
|
|
setupEventTestRobot(t, "robot_integ_event_emptydata", "team_integ_event")
|
|
|
|
m := manager.New()
|
|
err := m.Start()
|
|
require.NoError(t, err)
|
|
defer m.Stop()
|
|
|
|
ctx := types.NewContext(context.Background(), nil)
|
|
req := &types.EventRequest{
|
|
MemberID: "robot_integ_event_emptydata",
|
|
Source: "webhook",
|
|
EventType: "heartbeat",
|
|
Data: map[string]interface{}{}, // Empty map
|
|
}
|
|
|
|
result, err := m.HandleEvent(ctx, req)
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
assert.NotEmpty(t, result.ExecutionID)
|
|
})
|
|
}
|
|
|
|
// ==================== Test Data Setup Helpers ====================
|
|
|
|
// setupEventTestRobot creates a robot with event trigger enabled
|
|
func setupEventTestRobot(t *testing.T, memberID, teamID string) {
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
robotConfig := map[string]interface{}{
|
|
"identity": map[string]interface{}{
|
|
"role": "Event Test Robot",
|
|
"duties": []string{"Handle event triggers"},
|
|
},
|
|
"quota": map[string]interface{}{
|
|
"max": 5,
|
|
"queue": 20,
|
|
"priority": 5,
|
|
},
|
|
"triggers": map[string]interface{}{
|
|
"clock": map[string]interface{}{"enabled": false},
|
|
"intervene": map[string]interface{}{"enabled": false},
|
|
"event": map[string]interface{}{"enabled": true},
|
|
},
|
|
"events": []map[string]interface{}{
|
|
{
|
|
"type": "webhook",
|
|
"source": "/webhook/events",
|
|
},
|
|
{
|
|
"type": "database",
|
|
"source": "orders",
|
|
},
|
|
},
|
|
}
|
|
configJSON, _ := json.Marshal(robotConfig)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": memberID,
|
|
"team_id": teamID,
|
|
"member_type": "robot",
|
|
"display_name": "Event Test Robot " + memberID,
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert %s: %v", memberID, err)
|
|
}
|
|
}
|
|
|
|
// setupEventTestRobotPaused creates a paused robot
|
|
func setupEventTestRobotPaused(t *testing.T, memberID, teamID string) {
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
robotConfig := map[string]interface{}{
|
|
"identity": map[string]interface{}{"role": "Paused Event Robot"},
|
|
"triggers": map[string]interface{}{
|
|
"event": map[string]interface{}{"enabled": true},
|
|
},
|
|
}
|
|
configJSON, _ := json.Marshal(robotConfig)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": memberID,
|
|
"team_id": teamID,
|
|
"member_type": "robot",
|
|
"display_name": "Paused Event Robot " + memberID,
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "paused", // Paused
|
|
"robot_config": string(configJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert %s: %v", memberID, err)
|
|
}
|
|
}
|
|
|
|
// setupEventTestRobotDisabled creates a robot with event trigger disabled
|
|
func setupEventTestRobotDisabled(t *testing.T, memberID, teamID string) {
|
|
m := model.Select("__yao.member")
|
|
tableName := m.MetaData.Table.Name
|
|
qb := capsule.Query()
|
|
|
|
robotConfig := map[string]interface{}{
|
|
"identity": map[string]interface{}{"role": "Event Disabled Robot"},
|
|
"triggers": map[string]interface{}{
|
|
"event": map[string]interface{}{"enabled": false}, // Disabled
|
|
},
|
|
}
|
|
configJSON, _ := json.Marshal(robotConfig)
|
|
|
|
err := qb.Table(tableName).Insert([]map[string]interface{}{
|
|
{
|
|
"member_id": memberID,
|
|
"team_id": teamID,
|
|
"member_type": "robot",
|
|
"display_name": "Event Disabled Robot " + memberID,
|
|
"status": "active",
|
|
"role_id": "member",
|
|
"autonomous_mode": true,
|
|
"robot_status": "idle",
|
|
"robot_config": string(configJSON),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to insert %s: %v", memberID, err)
|
|
}
|
|
}
|