package api_test // Integration tests for the Robot Agent API // These tests verify the complete API functionality with real database operations. // // Test Structure: // - api_test.go: Core API integration tests (this file) // - lifecycle_test.go: Start/Stop lifecycle tests // - robot_test.go: Robot query tests // - trigger_test.go: Trigger tests // - execution_test.go: Execution query/control tests // // Test Data: // All tests use real database records in __yao.member and agent_execution tables // Test robot IDs are prefixed with "robot_api_" for easy cleanup // Test execution IDs are prefixed with "exec_api_" for easy cleanup 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/api" "github.com/yaoapp/yao/agent/robot/store" "github.com/yaoapp/yao/agent/robot/types" "github.com/yaoapp/yao/agent/testutils" ) // ==================== Full Lifecycle Integration Tests ==================== // TestAPIFullLifecycle tests the complete API workflow: // Start → Create Robot → Query Robot → Trigger → Query Execution → Stop func TestAPIFullLifecycle(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) // Cleanup test data cleanupAPITestRobots(t) cleanupAPITestExecutions(t) defer cleanupAPITestRobots(t) defer cleanupAPITestExecutions(t) t.Run("complete workflow", func(t *testing.T) { // 1. Setup: Create test robot in database setupAPITestRobot(t, "robot_api_lifecycle_001", "team_api_001") // 2. Start the API system err := api.Start() require.NoError(t, err) assert.True(t, api.IsRunning()) defer api.Stop() // 3. Query the robot via API ctx := types.NewContext(context.Background(), nil) robot, err := api.GetRobot(ctx, "robot_api_lifecycle_001") require.NoError(t, err) require.NotNil(t, robot) assert.Equal(t, "robot_api_lifecycle_001", robot.MemberID) assert.Equal(t, "team_api_001", robot.TeamID) assert.Equal(t, "API Test Robot robot_api_lifecycle_001", robot.DisplayName) // 4. Get robot status status, err := api.GetRobotStatus(ctx, "robot_api_lifecycle_001") require.NoError(t, err) require.NotNil(t, status) assert.Equal(t, "robot_api_lifecycle_001", status.MemberID) assert.Equal(t, types.RobotIdle, status.Status) assert.Equal(t, 0, status.Running) assert.Equal(t, 5, status.MaxRunning) // 5. List robots listResult, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_001", Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, listResult) assert.GreaterOrEqual(t, listResult.Total, 1) // Find our robot in the list found := false for _, r := range listResult.Data { if r.MemberID == "robot_api_lifecycle_001" { found = true break } } assert.True(t, found, "Robot should be in list") // 6. Trigger manual execution triggerResult, err := api.TriggerManual(ctx, "robot_api_lifecycle_001", types.TriggerClock, nil) require.NoError(t, err) require.NotNil(t, triggerResult) assert.True(t, triggerResult.Accepted) assert.NotEmpty(t, triggerResult.ExecutionID) // 7. Wait for execution to complete time.Sleep(500 * time.Millisecond) // 8. Stop the system err = api.Stop() require.NoError(t, err) assert.False(t, api.IsRunning()) }) } // TestAPIRobotQueryWithData tests robot query APIs with real data func TestAPIRobotQueryWithData(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) cleanupAPITestRobots(t) defer cleanupAPITestRobots(t) // Setup: Create multiple robots setupAPITestRobot(t, "robot_api_query_001", "team_api_query") setupAPITestRobot(t, "robot_api_query_002", "team_api_query") setupAPITestRobot(t, "robot_api_query_003", "team_api_other") ctx := types.NewContext(context.Background(), nil) t.Run("GetRobot returns correct robot", func(t *testing.T) { robot, err := api.GetRobot(ctx, "robot_api_query_001") require.NoError(t, err) require.NotNil(t, robot) assert.Equal(t, "robot_api_query_001", robot.MemberID) assert.Equal(t, "team_api_query", robot.TeamID) assert.Equal(t, "API Test Robot robot_api_query_001", robot.DisplayName) assert.True(t, robot.AutonomousMode) assert.Equal(t, types.RobotIdle, robot.Status) }) t.Run("ListAllRobots filters by team", func(t *testing.T) { result, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_query", Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have at least 2 robots from team_api_query // (might have more if other tests created robots in this team) assert.GreaterOrEqual(t, result.Total, 2) assert.GreaterOrEqual(t, len(result.Data), 2) // Verify all returned robots are from the correct team for _, robot := range result.Data { assert.Equal(t, "team_api_query", robot.TeamID) } }) t.Run("ListAllRobots pagination works", func(t *testing.T) { // Page 1 with size 1 result1, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_query", Page: 1, PageSize: 1, }) require.NoError(t, err) require.GreaterOrEqual(t, len(result1.Data), 1, "Should have at least 1 robot on page 1") // Page 2 with size 1 result2, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_query", Page: 2, PageSize: 1, }) require.NoError(t, err) require.GreaterOrEqual(t, len(result2.Data), 1, "Should have at least 1 robot on page 2") // Should be different robots assert.NotEqual(t, result1.Data[0].MemberID, result2.Data[0].MemberID) }) t.Run("ListAllRobots filters by keywords", func(t *testing.T) { result, err := api.ListAllRobots(ctx, &api.ListQuery{ Keywords: "robot_api_query_001", Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should find at least 1 robot matching keywords assert.GreaterOrEqual(t, result.Total, 1) for _, robot := range result.Data { assert.Contains(t, robot.DisplayName, "robot_api_query_001") } }) } // TestListAllRobotsAutonomousModeFilter tests the autonomous_mode filter func TestListAllRobotsAutonomousModeFilter(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) cleanupAPITestRobots(t) defer cleanupAPITestRobots(t) // Setup: Create robots with different autonomous_mode settings setupAPITestRobotWithMode(t, "robot_api_auto_001", "team_api_mode", true) // autonomous setupAPITestRobotWithMode(t, "robot_api_auto_002", "team_api_mode", true) // autonomous setupAPITestRobotWithMode(t, "robot_api_demand_001", "team_api_mode", false) // on-demand ctx := types.NewContext(context.Background(), nil) t.Run("ListAllRobots returns all robots when autonomous_mode is nil", func(t *testing.T) { result, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_mode", Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have all 3 robots assert.Equal(t, 3, result.Total) }) t.Run("ListAllRobots filters by autonomous_mode=true", func(t *testing.T) { autonomousMode := true result, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_mode", AutonomousMode: &autonomousMode, Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have only 2 autonomous robots assert.Equal(t, 2, result.Total) for _, robot := range result.Data { assert.True(t, robot.AutonomousMode, "All returned robots should be autonomous") } }) t.Run("ListAllRobots filters by autonomous_mode=false", func(t *testing.T) { autonomousMode := false result, err := api.ListAllRobots(ctx, &api.ListQuery{ TeamID: "team_api_mode", AutonomousMode: &autonomousMode, Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have only 1 on-demand robot assert.Equal(t, 1, result.Total) for _, robot := range result.Data { assert.False(t, robot.AutonomousMode, "All returned robots should be on-demand") } }) } // TestAPIExecutionQueryWithData tests execution query APIs with real data func TestAPIExecutionQueryWithData(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) cleanupAPITestExecutions(t) defer cleanupAPITestExecutions(t) // Setup: Create test executions setupAPITestExecution(t, "exec_api_query_001", "member_api_exec", types.TriggerClock, types.ExecCompleted) setupAPITestExecution(t, "exec_api_query_002", "member_api_exec", types.TriggerHuman, types.ExecRunning) setupAPITestExecution(t, "exec_api_query_003", "member_api_exec", types.TriggerClock, types.ExecFailed) setupAPITestExecution(t, "exec_api_query_004", "member_api_other", types.TriggerEvent, types.ExecCompleted) ctx := types.NewContext(context.Background(), nil) t.Run("GetExecution returns correct execution", func(t *testing.T) { exec, err := api.GetExecution(ctx, "exec_api_query_001") require.NoError(t, err) require.NotNil(t, exec) assert.Equal(t, "exec_api_query_001", exec.ID) assert.Equal(t, "member_api_exec", exec.MemberID) assert.Equal(t, types.TriggerClock, exec.TriggerType) assert.Equal(t, types.ExecCompleted, exec.Status) }) t.Run("ListExecutions filters by member", func(t *testing.T) { result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{ Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have 3 executions for member_api_exec assert.Equal(t, 3, result.Total) assert.Len(t, result.Data, 3) // Verify all returned executions are for the correct member for _, exec := range result.Data { assert.Equal(t, "member_api_exec", exec.MemberID) } }) t.Run("ListExecutions filters by status", func(t *testing.T) { result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{ Status: types.ExecCompleted, Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have only completed executions assert.Equal(t, 1, result.Total) for _, exec := range result.Data { assert.Equal(t, types.ExecCompleted, exec.Status) } }) t.Run("ListExecutions filters by trigger type", func(t *testing.T) { result, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{ Trigger: types.TriggerClock, Page: 1, PageSize: 10, }) require.NoError(t, err) require.NotNil(t, result) // Should have only clock trigger executions assert.Equal(t, 2, result.Total) for _, exec := range result.Data { assert.Equal(t, types.TriggerClock, exec.TriggerType) } }) t.Run("ListExecutions pagination works", func(t *testing.T) { // Page 1 with size 2 result1, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{ Page: 1, PageSize: 2, }) require.NoError(t, err) assert.GreaterOrEqual(t, len(result1.Data), 2, "Should have at least 2 executions on page 1") // Page 2 with size 2 result2, err := api.ListExecutions(ctx, "member_api_exec", &api.ExecutionQuery{ Page: 2, PageSize: 2, }) require.NoError(t, err) assert.GreaterOrEqual(t, len(result2.Data), 1, "Should have at least 1 execution on page 2") }) } // TestAPITriggerWithData tests trigger APIs with real robots func TestAPITriggerWithData(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test") } testutils.Prepare(t) defer testutils.Clean(t) cleanupAPITestRobots(t) defer cleanupAPITestRobots(t) // Setup: Create test robot setupAPITestRobot(t, "robot_api_trigger_001", "team_api_trigger") // Start manager err := api.Start() require.NoError(t, err) defer api.Stop() ctx := types.NewContext(context.Background(), nil) t.Run("TriggerManual accepts valid robot", func(t *testing.T) { result, err := api.TriggerManual(ctx, "robot_api_trigger_001", types.TriggerClock, nil) require.NoError(t, err) require.NotNil(t, result) assert.True(t, result.Accepted) assert.NotEmpty(t, result.ExecutionID) assert.Contains(t, result.Message, "submitted") }) t.Run("Trigger with human type", func(t *testing.T) { result, err := api.Trigger(ctx, "robot_api_trigger_001", &api.TriggerRequest{ Type: types.TriggerHuman, Action: types.ActionTaskAdd, }) require.NoError(t, err) require.NotNil(t, result) // Result should be returned (accepted or not depends on robot state) // The important thing is that the API doesn't error t.Logf("Trigger result: accepted=%v, message=%s", result.Accepted, result.Message) }) t.Run("Trigger with event type", func(t *testing.T) { result, err := api.Trigger(ctx, "robot_api_trigger_001", &api.TriggerRequest{ Type: types.TriggerEvent, Source: types.EventWebhook, EventType: "test.event", Data: map[string]interface{}{"key": "value"}, }) require.NoError(t, err) require.NotNil(t, result) // Should be accepted (robot exists and has event enabled) assert.True(t, result.Accepted) }) t.Run("Trigger rejects non-existent robot", func(t *testing.T) { result, err := api.Trigger(ctx, "robot_api_nonexistent", &api.TriggerRequest{ Type: types.TriggerHuman, }) // Should not return error, but result should show not accepted assert.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.Accepted) }) } // ==================== Helper Functions ==================== // setupAPITestRobotWithMode creates a test robot with specific autonomous_mode setting func setupAPITestRobotWithMode(t *testing.T, memberID, teamID string, autonomousMode bool) { m := model.Select("__yao.member") tableName := m.MetaData.Table.Name qb := capsule.Query() robotConfig := map[string]interface{}{ "identity": map[string]interface{}{ "role": "API Test Robot", "duties": []string{"Testing API functions"}, }, "quota": map[string]interface{}{ "max": 5, "queue": 20, "priority": 5, }, } configJSON, _ := json.Marshal(robotConfig) err := qb.Table(tableName).Insert([]map[string]interface{}{ { "member_id": memberID, "team_id": teamID, "member_type": "robot", "display_name": "API Test Robot " + memberID, "system_prompt": "You are an API test robot.", "status": "active", "role_id": "member", "autonomous_mode": autonomousMode, "robot_status": "idle", "robot_config": string(configJSON), }, }) if err != nil { t.Fatalf("Failed to insert robot %s: %v", memberID, err) } } // setupAPITestRobot creates a test robot in the database func setupAPITestRobot(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": "API Test Robot", "duties": []string{"Testing API functions"}, }, "quota": map[string]interface{}{ "max": 5, "queue": 20, "priority": 5, }, "triggers": map[string]interface{}{ "clock": map[string]interface{}{"enabled": true}, "intervene": map[string]interface{}{"enabled": true}, "event": map[string]interface{}{"enabled": true}, }, "clock": map[string]interface{}{ "mode": "interval", "every": "1h", "timeout": "30m", }, } configJSON, _ := json.Marshal(robotConfig) err := qb.Table(tableName).Insert([]map[string]interface{}{ { "member_id": memberID, "team_id": teamID, "member_type": "robot", "display_name": "API Test Robot " + memberID, "system_prompt": "You are an API test robot.", "status": "active", "role_id": "member", "autonomous_mode": true, "robot_status": "idle", "robot_config": string(configJSON), }, }) if err != nil { t.Fatalf("Failed to insert robot %s: %v", memberID, err) } } // setupAPITestExecution creates a test execution in the database func setupAPITestExecution(t *testing.T, execID, memberID string, triggerType types.TriggerType, status types.ExecStatus) { s := store.NewExecutionStore() ctx := context.Background() startTime := time.Now().Add(-1 * time.Hour) record := &store.ExecutionRecord{ ExecutionID: execID, MemberID: memberID, TeamID: "team_api_exec", TriggerType: triggerType, Status: status, Phase: types.PhaseDelivery, StartTime: &startTime, } if status == types.ExecCompleted || status == types.ExecFailed { endTime := time.Now() record.EndTime = &endTime } err := s.Save(ctx, record) if err != nil { t.Fatalf("Failed to insert execution %s: %v", execID, err) } } // cleanupAPITestRobots removes all API test robots func cleanupAPITestRobots(t *testing.T) { m := model.Select("__yao.member") if m == nil { return } tableName := m.MetaData.Table.Name qb := capsule.Query() // Delete all robots with member_id starting with "robot_api_" or "api_robot_" _, err := qb.Table(tableName).Where("member_id", "like", "robot_api_%").Delete() if err != nil { t.Logf("Warning: cleanup robots error: %v", err) } // Also delete "api_robot_" prefixed robots (new tests) _, err = qb.Table(tableName).Where("member_id", "like", "api_robot_%").Delete() if err != nil { t.Logf("Warning: cleanup robots error: %v", err) } } // cleanupAPITestExecutions removes all API test executions func cleanupAPITestExecutions(t *testing.T) { m := model.Select("__yao.agent.execution") if m == nil { t.Logf("Warning: model __yao.agent.execution not found, skipping cleanup") return } tableName := m.MetaData.Table.Name qb := capsule.Query() // Delete all executions with execution_id starting with "exec_api_" _, err := qb.Table(tableName).Where("execution_id", "like", "exec_api_%").Delete() if err != nil { t.Logf("Warning: cleanup executions error: %v", err) } // Also delete executions for API test members _, err = qb.Table(tableName).Where("member_id", "like", "member_api_%").Delete() if err != nil { t.Logf("Warning: cleanup executions error: %v", err) } }