yao/agent/robot/manager/integration_human_test.go
Max 00851c442e Enhance Integration Tests with Cache Verification
- 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.
2026-01-15 17:39:03 +08:00

554 lines
15 KiB
Go

package manager_test
// Integration tests for Human intervention triggers
// Tests Manager.Intervene() with various actions 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"
agentcontext "github.com/yaoapp/yao/agent/context"
"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"
)
// ==================== Human Intervention Tests ====================
// TestIntegrationHumanIntervention tests human intervention trigger flow
func TestIntegrationHumanIntervention(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("task.add action success", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_add", "team_integ_human")
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_human_add")
require.NotNil(t, robot, "Robot should be loaded into cache")
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
TeamID: "team_integ_human",
MemberID: "robot_integ_human_add",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Add a new task: analyze sales data"},
},
}
result, err := m.Intervene(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, "task.add")
// Wait for execution
time.Sleep(500 * time.Millisecond)
// Verify execution completed
assert.GreaterOrEqual(t, m.Executor().ExecCount(), 1)
})
t.Run("goal.adjust action success", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_goal", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
TeamID: "team_integ_human",
MemberID: "robot_integ_human_goal",
Action: types.ActionGoalAdjust,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Focus on high-priority customers only"},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
})
t.Run("instruct action success", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_instruct", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
TeamID: "team_integ_human",
MemberID: "robot_integ_human_instruct",
Action: types.ActionInstruct,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Generate a weekly report"},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
})
}
// TestIntegrationHumanInterventionErrors tests error cases for human intervention
func TestIntegrationHumanInterventionErrors(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.InterveneRequest{
MemberID: "robot_nonexistent",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Test"},
},
}
_, err = m.Intervene(ctx, req)
assert.Error(t, err)
assert.Equal(t, types.ErrRobotNotFound, err)
})
t.Run("robot paused", func(t *testing.T) {
setupInterveneTestRobotPaused(t, "robot_integ_human_paused", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_paused",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Test"},
},
}
_, err = m.Intervene(ctx, req)
assert.Error(t, err)
assert.Equal(t, types.ErrRobotPaused, err)
})
t.Run("intervene trigger disabled", func(t *testing.T) {
setupInterveneTestRobotDisabled(t, "robot_integ_human_disabled", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_disabled",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Test"},
},
}
_, err = m.Intervene(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.InterveneRequest{
MemberID: "", // Empty
Action: types.ActionTaskAdd,
}
_, err = m.Intervene(ctx, req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "member_id")
})
t.Run("invalid request - empty action", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_noaction", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_noaction",
Action: "", // Empty action
}
_, err = m.Intervene(ctx, req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "action")
})
t.Run("manager not started", func(t *testing.T) {
m := manager.New()
// Don't start
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_test",
Action: types.ActionTaskAdd,
}
_, err := m.Intervene(ctx, req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not started")
})
}
// TestIntegrationHumanInterventionMultimodal tests multimodal input support
func TestIntegrationHumanInterventionMultimodal(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("text message", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_text", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_text",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{
Role: agentcontext.RoleUser,
Content: "Analyze the quarterly sales report",
},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
})
t.Run("message with image reference", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_image", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_image",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{
Role: agentcontext.RoleUser,
Content: []interface{}{
map[string]interface{}{
"type": "text",
"text": "Analyze this chart",
},
map[string]interface{}{
"type": "image_url",
"image_url": map[string]interface{}{
"url": "https://example.com/chart.png",
},
},
},
},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
})
t.Run("multiple messages", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_multi", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_multi",
Action: types.ActionTaskAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "First, check the sales data"},
{Role: agentcontext.RoleUser, Content: "Then, prepare a summary report"},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
})
}
// TestIntegrationHumanInterventionAllActions tests all intervention actions
func TestIntegrationHumanInterventionAllActions(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test")
}
testutils.Prepare(t)
defer testutils.Clean(t)
cleanupIntegrationRobots(t)
defer cleanupIntegrationRobots(t)
// Test all defined actions
actions := []types.InterventionAction{
types.ActionTaskAdd,
types.ActionTaskCancel,
types.ActionTaskUpdate,
types.ActionGoalAdjust,
types.ActionGoalAdd,
types.ActionGoalComplete,
types.ActionGoalCancel,
types.ActionInstruct,
// Note: plan.add, plan.remove, plan.update are handled differently
}
for _, action := range actions {
t.Run(string(action), func(t *testing.T) {
memberID := "robot_integ_action_" + string(action)
setupInterveneTestRobot(t, memberID, "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
req := &types.InterveneRequest{
MemberID: memberID,
Action: action,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Test action: " + string(action)},
},
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err, "Action %s should succeed", action)
assert.NotNil(t, result)
assert.NotEmpty(t, result.ExecutionID)
assert.Equal(t, types.ExecPending, result.Status)
})
}
}
// TestIntegrationHumanInterventionPlanAdd tests plan.add action (deferred execution)
func TestIntegrationHumanInterventionPlanAdd(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("plan.add with future time", func(t *testing.T) {
setupInterveneTestRobot(t, "robot_integ_human_plan", "team_integ_human")
m := manager.New()
err := m.Start()
require.NoError(t, err)
defer m.Stop()
ctx := types.NewContext(context.Background(), nil)
planTime := time.Now().Add(1 * time.Hour)
req := &types.InterveneRequest{
MemberID: "robot_integ_human_plan",
Action: types.ActionPlanAdd,
Messages: []agentcontext.Message{
{Role: agentcontext.RoleUser, Content: "Send weekly report"},
},
PlanTime: &planTime,
}
result, err := m.Intervene(ctx, req)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, types.ExecPending, result.Status)
assert.Contains(t, result.Message, "Planned")
// Note: Plan queue not implemented yet, so execution is deferred
})
}
// ==================== Test Data Setup Helpers ====================
// setupInterveneTestRobot creates a robot with intervene trigger enabled
func setupInterveneTestRobot(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": "Intervene Test Robot",
"duties": []string{"Handle human interventions"},
},
"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": true},
"event": map[string]interface{}{"enabled": false},
},
}
configJSON, _ := json.Marshal(robotConfig)
err := qb.Table(tableName).Insert([]map[string]interface{}{
{
"member_id": memberID,
"team_id": teamID,
"member_type": "robot",
"display_name": "Intervene 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)
}
}
// setupInterveneTestRobotPaused creates a paused robot
func setupInterveneTestRobotPaused(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 Robot"},
"triggers": map[string]interface{}{
"intervene": 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 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)
}
}
// setupInterveneTestRobotDisabled creates a robot with intervene trigger disabled
func setupInterveneTestRobotDisabled(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": "Intervene Disabled Robot"},
"triggers": map[string]interface{}{
"intervene": 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": "Intervene 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)
}
}