- Implement TestEnsureChatMetadata to verify that metadata, including robot_id, is correctly persisted in chat records. - Update EnsureChat method to store metadata from the context when creating chat records. - Introduce TestExecutorGoalsInjection to validate that pre-confirmed goals are injected into executions from TriggerInput.Data. - Enhance executor logic to handle goal injection and persistence, ensuring accurate execution titles. - Modify chat filtering to support chat_id_prefix for improved chat retrieval based on robot identifiers.
317 lines
12 KiB
Go
317 lines
12 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"
|
|
)
|
|
|
|
// TestRobotHostID tests GET /v1/agent/robots/:id/host
|
|
// Returns the host assistant ID for a given robot.
|
|
func TestRobotHostID(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
client := testutils.RegisterTestClient(t, "Host ID 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 with a minimal config
|
|
robotID := fmt.Sprintf("test_host_id_%d", time.Now().UnixNano())
|
|
createTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Host ID Test Robot")
|
|
defer deleteTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("GetHostIDSuccess", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/host", nil)
|
|
require.NoError(t, err)
|
|
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.StatusOK, resp.StatusCode)
|
|
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
assert.Contains(t, response, "assistant_id", "Response should contain assistant_id")
|
|
assert.Contains(t, response, "robot_id", "Response should contain robot_id")
|
|
assert.Equal(t, robotID, response["robot_id"])
|
|
// assistant_id is either configured or falls back to "__yao.host"
|
|
assistantID, ok := response["assistant_id"].(string)
|
|
assert.True(t, ok, "assistant_id should be a string")
|
|
assert.NotEmpty(t, assistantID, "assistant_id should not be empty")
|
|
t.Logf("✓ Host ID: robot=%s, assistant=%s", robotID, assistantID)
|
|
})
|
|
|
|
t.Run("GetHostIDNotFound", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/non_existent_robot/host", nil)
|
|
require.NoError(t, err)
|
|
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.StatusInternalServerError, resp.StatusCode)
|
|
t.Logf("✓ Non-existent robot returns error for /host")
|
|
})
|
|
|
|
t.Run("GetHostIDUnauthorized", func(t *testing.T) {
|
|
req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/robots/"+robotID+"/host", nil)
|
|
require.NoError(t, err)
|
|
// 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.Logf("✓ Unauthorized request rejected")
|
|
})
|
|
}
|
|
|
|
// TestRobotExecute tests POST /v1/agent/robots/:id/execute
|
|
// Called by CUI after Host Agent confirms goals via robot.execute Action.
|
|
func TestRobotExecute(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping execute tests in short mode (requires manager)")
|
|
}
|
|
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
client := testutils.RegisterTestClient(t, "Execute 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")
|
|
|
|
robotID := fmt.Sprintf("test_execute_%d", time.Now().UnixNano())
|
|
createTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Execute Test Robot")
|
|
defer deleteTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("ExecuteWithGoals", func(t *testing.T) {
|
|
execData := map[string]interface{}{
|
|
"goals": "Create a mecha image with sci-fi style",
|
|
"chat_id": fmt.Sprintf("robot_%s_%d", robotID, time.Now().UnixMilli()),
|
|
"context": map[string]interface{}{
|
|
"style": "sci-fi",
|
|
"subject": "mecha",
|
|
},
|
|
}
|
|
|
|
body, _ := json.Marshal(execData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/execute", 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()
|
|
|
|
// Either 200 (accepted) or 400 (trigger disabled / manager not running)
|
|
// Both are valid — we test the API contract
|
|
var response map[string]interface{}
|
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
|
require.NoError(t, err)
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
assert.Contains(t, response, "execution_id", "Should return execution_id")
|
|
assert.Equal(t, "started", response["status"])
|
|
t.Logf("✓ Execute accepted: execution_id=%v", response["execution_id"])
|
|
} else {
|
|
// Manager not running or trigger disabled is acceptable in test env
|
|
t.Logf("Execute returned %d: %v (manager may not be running)", resp.StatusCode, response)
|
|
}
|
|
})
|
|
|
|
t.Run("ExecuteMissingGoals", func(t *testing.T) {
|
|
// goals is required — omitting it should fail with 400
|
|
execData := map[string]interface{}{
|
|
"chat_id": fmt.Sprintf("robot_%s_%d", robotID, time.Now().UnixMilli()),
|
|
}
|
|
|
|
body, _ := json.Marshal(execData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/execute", 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.StatusBadRequest, resp.StatusCode)
|
|
t.Logf("✓ Missing goals returns 400")
|
|
})
|
|
|
|
t.Run("ExecuteNotFound", func(t *testing.T) {
|
|
execData := map[string]interface{}{
|
|
"goals": "Some goal",
|
|
}
|
|
body, _ := json.Marshal(execData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot_xyz/execute", 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()
|
|
|
|
// 404 (robot not found), 400 (trigger disabled) or 500 (manager not running) — all acceptable
|
|
assert.True(t,
|
|
resp.StatusCode == http.StatusNotFound ||
|
|
resp.StatusCode == http.StatusBadRequest ||
|
|
resp.StatusCode == http.StatusInternalServerError,
|
|
"Non-existent robot should return 4xx or 500, got %d", resp.StatusCode)
|
|
t.Logf("✓ Non-existent robot execute returns %d", resp.StatusCode)
|
|
})
|
|
|
|
t.Run("ExecuteUnauthorized", func(t *testing.T) {
|
|
execData := map[string]interface{}{
|
|
"goals": "Some goal",
|
|
}
|
|
body, _ := json.Marshal(execData)
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/execute", 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.Logf("✓ Unauthorized execute returns 401")
|
|
})
|
|
}
|
|
|
|
// TestRobotCompletionsMirrorAPI tests POST /v1/agent/robots/:id/completions
|
|
// Mirror API that resolves host assistant and delegates to standard chat completions.
|
|
func TestRobotCompletionsMirrorAPI(t *testing.T) {
|
|
serverURL := testutils.Prepare(t)
|
|
defer testutils.Clean()
|
|
|
|
baseURL := ""
|
|
if openapi.Server != nil && openapi.Server.Config != nil {
|
|
baseURL = openapi.Server.Config.BaseURL
|
|
}
|
|
|
|
client := testutils.RegisterTestClient(t, "Completions Mirror 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")
|
|
|
|
robotID := fmt.Sprintf("test_completions_%d", time.Now().UnixNano())
|
|
createTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID, "Completions Mirror Test Robot")
|
|
defer deleteTestRobotForHost(t, serverURL, baseURL, tokenInfo.AccessToken, robotID)
|
|
|
|
t.Run("RejectsEmptyBody", func(t *testing.T) {
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/completions",
|
|
bytes.NewBuffer([]byte("{}")))
|
|
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()
|
|
|
|
// Should be handled by the chat completion handler
|
|
// 4xx or 5xx is acceptable — we're verifying the route resolves
|
|
assert.True(t, resp.StatusCode >= 400, "Empty completions request should fail, got %d", resp.StatusCode)
|
|
t.Logf("✓ Empty completions body handled: %d", resp.StatusCode)
|
|
})
|
|
|
|
t.Run("RejectsNonExistentRobot", func(t *testing.T) {
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"chat_id": "test_chat_id",
|
|
"assistant_id": "some_assistant",
|
|
"messages": []map[string]interface{}{
|
|
{"role": "user", "content": "hello"},
|
|
},
|
|
})
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/non_existent_robot/completions",
|
|
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.True(t, resp.StatusCode >= 400, "Non-existent robot should return error, got %d", resp.StatusCode)
|
|
t.Logf("✓ Non-existent robot completions handled: %d", resp.StatusCode)
|
|
})
|
|
|
|
t.Run("UnauthorizedReturns401", func(t *testing.T) {
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"chat_id": "test_chat",
|
|
})
|
|
req, err := http.NewRequest("POST", serverURL+baseURL+"/agent/robots/"+robotID+"/completions",
|
|
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.Logf("✓ Unauthorized completions returns 401")
|
|
})
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
func createTestRobotForHost(t *testing.T, serverURL, baseURL, token, robotID, name string) {
|
|
t.Helper()
|
|
createData := map[string]interface{}{
|
|
"member_id": robotID,
|
|
"team_id": "test_team_host_001",
|
|
"display_name": name,
|
|
"bio": "A test robot for host API testing",
|
|
}
|
|
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 deleteTestRobotForHost(t *testing.T, serverURL, baseURL, token, robotID string) {
|
|
t.Helper()
|
|
req, _ := http.NewRequest("DELETE", serverURL+baseURL+"/agent/robots/"+robotID, nil)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err == nil {
|
|
resp.Body.Close()
|
|
}
|
|
}
|