- Added `Name` and `CurrentTaskName` fields to the `Execution` struct for improved UI display during execution phases. - Implemented localization support for UI messages, allowing dynamic updates based on the execution context and user locale. - Updated the executor to manage these fields at various phases, ensuring accurate representation of execution status. - Enhanced OpenAPI documentation to reflect the new fields and their usage in execution responses. - Added unit tests to validate the functionality of UI fields and localization handling.
502 lines
16 KiB
Go
502 lines
16 KiB
Go
package openapi_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/openapi"
|
|
"github.com/yaoapp/yao/openapi/tests/testutils"
|
|
)
|
|
|
|
// TestTriggerRobot tests the robot trigger endpoint
|
|
// POST /v1/agent/robots/:id/trigger
|
|
func TestTriggerRobot(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping trigger tests in short mode (requires AI/manager)")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Trigger Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test robot
|
|
robotID := fmt.Sprintf("test_trigger_%d", time.Now().UnixNano())
|
|
createRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Trigger Test Robot")
|
|
defer deleteRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("TriggerRobotBasic", func(t *testing.T) {
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Hello, please help me with a task",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// If manager is not started, we get a 500 error (expected in test environment)
|
|
// In production with manager running, response should contain "accepted" field
|
|
if resp.StatusCode == http.StatusInternalServerError {
|
|
// Expected when robot manager is not started
|
|
assert.Contains(t, response, "error_description")
|
|
t.Logf("Trigger response (manager not started): status=%d, error=%v", resp.StatusCode, response["error_description"])
|
|
} else {
|
|
// Manager is running - verify accepted field
|
|
assert.Contains(t, response, "accepted")
|
|
t.Logf("Trigger response: status=%d, accepted=%v", resp.StatusCode, response["accepted"])
|
|
}
|
|
})
|
|
|
|
t.Run("TriggerRobotWithAction", func(t *testing.T) {
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
"action": "task.add",
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Add a new task: Review quarterly report",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
var response map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&response)
|
|
|
|
// If manager is not started, we get a 500 error (expected in test environment)
|
|
if resp.StatusCode == http.StatusInternalServerError {
|
|
assert.Contains(t, response, "error_description")
|
|
t.Logf("Trigger with action response (manager not started): status=%d", resp.StatusCode)
|
|
} else {
|
|
assert.Contains(t, response, "accepted")
|
|
t.Logf("Trigger with action response: status=%d", resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("TriggerRobotWithLocale", func(t *testing.T) {
|
|
// Test the new locale parameter for i18n support
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
"locale": "zh", // Chinese locale
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "请帮我分析销售数据",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
var response map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&response)
|
|
|
|
// If manager is not started, we get a 500 error (expected in test environment)
|
|
if resp.StatusCode == http.StatusInternalServerError {
|
|
assert.Contains(t, response, "error_description")
|
|
t.Logf("Trigger with locale response (manager not started): status=%d", resp.StatusCode)
|
|
} else {
|
|
assert.Contains(t, response, "accepted")
|
|
t.Logf("Trigger with locale response: status=%d, accepted=%v", resp.StatusCode, response["accepted"])
|
|
}
|
|
})
|
|
|
|
t.Run("TriggerRobotNotFound", func(t *testing.T) {
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("TriggerRobotUnauthorized", func(t *testing.T) {
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// No Authorization header
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("TriggerRobotInvalidBody", func(t *testing.T) {
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer([]byte("invalid json")))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestInterveneRobot tests the robot intervention endpoint
|
|
// POST /v1/agent/robots/:id/intervene
|
|
func TestInterveneRobot(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping intervene tests in short mode (requires AI/manager)")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client and get token
|
|
client := testutils.RegisterTestClient(t, "Intervene Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
tokenInfo := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// Create a test robot
|
|
robotID := fmt.Sprintf("test_intervene_%d", time.Now().UnixNano())
|
|
createRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Intervene Test Robot")
|
|
defer deleteRobotForTrigger(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("InterveneRobotBasic", func(t *testing.T) {
|
|
interveneData := map[string]interface{}{
|
|
"action": "task.add",
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Please add a high priority task",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(interveneData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
defer resp.Body.Close()
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
// If manager is not started, we get a 500 error (expected in test environment)
|
|
if resp.StatusCode == http.StatusInternalServerError {
|
|
assert.Contains(t, response, "error_description")
|
|
t.Logf("Intervene response (manager not started): status=%d, error=%v", resp.StatusCode, response["error_description"])
|
|
} else {
|
|
assert.Contains(t, response, "accepted")
|
|
t.Logf("Intervene response: status=%d, accepted=%v", resp.StatusCode, response["accepted"])
|
|
}
|
|
})
|
|
|
|
t.Run("InterveneRobotMissingAction", func(t *testing.T) {
|
|
interveneData := map[string]interface{}{
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Some message",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(interveneData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Action is required
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("InterveneRobotNotFound", func(t *testing.T) {
|
|
interveneData := map[string]interface{}{
|
|
"action": "task.add",
|
|
}
|
|
|
|
body, _ := json.Marshal(interveneData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/intervene", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("InterveneRobotUnauthorized", func(t *testing.T) {
|
|
interveneData := map[string]interface{}{
|
|
"action": "task.add",
|
|
}
|
|
|
|
body, _ := json.Marshal(interveneData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
// No Authorization header
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("InterveneRobotInvalidBody", func(t *testing.T) {
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer([]byte("invalid json")))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// TestTriggerPermissions tests trigger permission inheritance from robot
|
|
func TestTriggerPermissions(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping trigger permission tests in short mode")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
// Register test client
|
|
client := testutils.RegisterTestClient(t, "Trigger Permission Test Client", []string{"https://localhost/callback"})
|
|
defer testutils.CleanupTestClient(t, client.ClientID)
|
|
|
|
// Create User 1
|
|
token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
user1ID := token1.UserID
|
|
|
|
// Create User 2
|
|
token2 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile")
|
|
|
|
// User 1 creates a robot
|
|
robotID := fmt.Sprintf("test_trig_perm_%d", time.Now().UnixNano())
|
|
createRobotWithTeamForTrigger(t, serverURL, baseURL, token1.AccessToken, robotID, "Trigger Perm Robot", user1ID)
|
|
defer deleteRobotForTrigger(t, serverURL, baseURL, token1.AccessToken, robotID)
|
|
|
|
t.Run("OwnerCanTrigger", func(t *testing.T) {
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Owner triggering robot",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// Owner should be able to trigger (may fail at manager level with 500, but not 403 permission denied)
|
|
// 500 = manager not started (acceptable), 403 = permission denied (not acceptable)
|
|
assert.NotEqual(t, http.StatusForbidden, resp.StatusCode, "Owner should have permission to trigger")
|
|
t.Logf("Owner trigger attempt status: %d", resp.StatusCode)
|
|
})
|
|
|
|
t.Run("OwnerCanIntervene", func(t *testing.T) {
|
|
interveneData := map[string]interface{}{
|
|
"action": "task.add",
|
|
"messages": []map[string]interface{}{
|
|
{
|
|
"role": "user",
|
|
"content": "Owner intervention",
|
|
},
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(interveneData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/intervene", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token1.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
// 500 = manager not started (acceptable), 403 = permission denied (not acceptable)
|
|
assert.NotEqual(t, http.StatusForbidden, resp.StatusCode, "Owner should have permission to intervene")
|
|
t.Logf("Owner intervene attempt status: %d", resp.StatusCode)
|
|
})
|
|
|
|
t.Run("OtherUserTriggerAccess", func(t *testing.T) {
|
|
// User 2 attempts to trigger User 1's robot
|
|
// With system:root scope this might succeed
|
|
triggerData := map[string]interface{}{
|
|
"trigger_type": "human",
|
|
}
|
|
|
|
body, _ := json.Marshal(triggerData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/trigger", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token2.AccessToken)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
defer resp.Body.Close()
|
|
|
|
t.Logf("User 2 trigger attempt status: %d (with system:root scope)", resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
// ==================== Helper Functions ====================
|
|
|
|
func createRobotForTrigger(t *testing.T, serverURL, baseURL, token, robotID, displayName string) {
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_001",
|
|
"display_name": displayName,
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
}
|
|
|
|
func createRobotWithTeamForTrigger(t *testing.T, serverURL, baseURL, token, robotID, displayName, teamID string) {
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": teamID,
|
|
"display_name": displayName,
|
|
}
|
|
|
|
body, _ := json.Marshal(createData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots", bytes.NewBuffer(body))
|
|
require.NoError(t, err)
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
resp.Body.Close()
|
|
}
|
|
|
|
func deleteRobotForTrigger(t *testing.T, serverURL, baseURL, token, robotID string) {
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, _ := http.DefaultClient.Do(req)
|
|
if resp != nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|