diff --git a/openapi/agent/agent.go b/openapi/agent/agent.go index defae22f..0c27c5dd 100644 --- a/openapi/agent/agent.go +++ b/openapi/agent/agent.go @@ -20,7 +20,7 @@ func Attach(group *gin.RouterGroup, oauth types.OAuth) { group.GET("/assistants", ListAssistants) // GET /assistants - List assistants group.POST("/assistants", n.HandleAssistantSave) // POST /assistants - Create/Update assistant group.GET("/assistants/tags", ListAssistantTags) // GET /assistants/tags - Get all assistant tags with permission filtering - group.GET("/assistants/:id", n.HandleAssistantDetail) // GET /assistants/:id - Get assistant details + group.GET("/assistants/:id", GetAssistant) // GET /assistants/:id - Get assistant details with permission verification group.DELETE("/assistants/:id", n.HandleAssistantDelete) // DELETE /assistants/:id - Delete assistant // Assistant Actions diff --git a/openapi/agent/assistant.go b/openapi/agent/assistant.go index 7e5b5f3b..122681e0 100644 --- a/openapi/agent/assistant.go +++ b/openapi/agent/assistant.go @@ -7,7 +7,9 @@ import ( "github.com/gin-gonic/gin" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/agent" + agenttypes "github.com/yaoapp/yao/agent/store/types" "github.com/yaoapp/yao/openapi/oauth/authorized" + "github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/openapi/response" ) @@ -160,6 +162,87 @@ func ListAssistants(c *gin.Context) { response.RespondWithSuccess(c, response.StatusOK, result) } +// GetAssistant retrieves a single assistant by ID with permission verification +func GetAssistant(c *gin.Context) { + + // Get authorized information + authInfo := authorized.GetInfo(c) + + // Get Agent instance from global variable + agentInstance := agent.GetAgent() + if agentInstance == nil || agentInstance.Store == nil { + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Agent store not initialized", + } + response.RespondWithError(c, response.StatusInternalServerError, errorResp) + return + } + + // Get assistant ID from URL parameter + assistantID := c.Param("id") + if assistantID == "" { + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "assistant_id is required", + } + response.RespondWithError(c, response.StatusBadRequest, errorResp) + return + } + + // Parse locale (optional - if not provided, returns raw data without i18n translation) + // This is useful for form editing scenarios where you need the original values + var assistant *agenttypes.AssistantModel + var err error + + if loc := c.Query("locale"); loc != "" { + // If locale is specified, get assistant with translation + locale := strings.ToLower(strings.TrimSpace(loc)) + assistant, err = agentInstance.Store.GetAssistant(assistantID, locale) + } else { + // If no locale specified, get raw data without translation + assistant, err = agentInstance.Store.GetAssistant(assistantID) + } + if err != nil { + log.Error("Failed to get assistant %s: %v", assistantID, err) + errorResp := &response.ErrorResponse{ + Code: response.ErrInvalidRequest.Code, + ErrorDescription: "Assistant not found: " + err.Error(), + } + response.RespondWithError(c, response.StatusNotFound, errorResp) + return + } + + // Check permission + hasPermission, err := checkAssistantPermission(authInfo, assistant) + if err != nil { + log.Error("Failed to check permission for assistant %s: %v", assistantID, err) + errorResp := &response.ErrorResponse{ + Code: response.ErrServerError.Code, + ErrorDescription: "Failed to check permission: " + err.Error(), + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + if !hasPermission { + errorResp := &response.ErrorResponse{ + Code: response.ErrAccessDenied.Code, + ErrorDescription: "Forbidden: No permission to access this assistant", + } + response.RespondWithError(c, response.StatusForbidden, errorResp) + return + } + + // Filter sensitive fields for built-in assistants + FilterBuiltInAssistant(assistant) + + // Return the result with standard response format + response.RespondWithSuccess(c, response.StatusOK, map[string]interface{}{ + "data": assistant, + }) +} + // ListAssistantTags lists assistant tags with permission-based filtering func ListAssistantTags(c *gin.Context) { @@ -233,3 +316,53 @@ func ListAssistantTags(c *gin.Context) { "data": tags, }) } + +// checkAssistantPermission checks if the user has permission to access the assistant +// Similar logic to checkFilePermission in openapi/file/file.go +func checkAssistantPermission(authInfo *types.AuthorizedInfo, assistant *agenttypes.AssistantModel) (bool, error) { + // No auth info, allow access + if authInfo == nil { + return true, nil + } + + // No constraints, allow access + if !authInfo.Constraints.TeamOnly && !authInfo.Constraints.OwnerOnly { + return true, nil + } + + // If assistant is public, allow access + if assistant.Public { + return true, nil + } + + // Combined Team and Owner permission validation + if authInfo.Constraints.TeamOnly && authInfo.Constraints.OwnerOnly { + if assistant.YaoCreatedBy == authInfo.UserID && assistant.YaoTeamID == authInfo.TeamID { + return true, nil + } + return false, nil + } + + // Team only permission validation + if authInfo.Constraints.TeamOnly { + // Check if assistant belongs to the same team + if assistant.YaoTeamID == authInfo.TeamID { + // Allow if user created it or if it's shared with team + if assistant.YaoCreatedBy == authInfo.UserID || assistant.Share == "team" { + return true, nil + } + } + return false, nil + } + + // Owner only permission validation + if authInfo.Constraints.OwnerOnly { + // Check if user created the assistant and team_id is empty (not a team resource) + if assistant.YaoCreatedBy == authInfo.UserID && assistant.YaoTeamID == "" { + return true, nil + } + return false, nil + } + + return false, nil +} diff --git a/openapi/agent/filter.go b/openapi/agent/filter.go index c858e44b..f946cd3e 100644 --- a/openapi/agent/filter.go +++ b/openapi/agent/filter.go @@ -125,7 +125,7 @@ func AuthQueryFilter(c *gin.Context, authInfo *types.AuthorizedInfo) func(query. return nil } -// FilterBuiltInFields filters sensitive fields for built-in assistants +// FilterBuiltInFields filters sensitive fields for built-in assistants in a list // For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) { if assistants == nil { @@ -133,14 +133,25 @@ func FilterBuiltInFields(assistants []*agenttypes.AssistantModel) { } for _, assistant := range assistants { - if assistant != nil && assistant.BuiltIn { - // Clear code-level sensitive fields for built-in assistants - assistant.Prompts = nil - assistant.Workflow = nil - assistant.Tools = nil - assistant.KB = nil - assistant.MCP = nil - assistant.Options = nil - } + FilterBuiltInAssistant(assistant) + } +} + +// FilterBuiltInAssistant filters sensitive fields for a single built-in assistant +// For built-in assistants, code-level fields (prompts, workflow, tools, kb, mcp, options) should be cleared +// This function can be used for both single assistant and list of assistants +func FilterBuiltInAssistant(assistant *agenttypes.AssistantModel) { + if assistant == nil { + return + } + + if assistant.BuiltIn { + // Clear code-level sensitive fields for built-in assistants + assistant.Prompts = nil + assistant.Workflow = nil + assistant.Tools = nil + assistant.KB = nil + assistant.MCP = nil + assistant.Options = nil } } diff --git a/openapi/tests/agent/assistant_test.go b/openapi/tests/agent/assistant_test.go index d998059d..1f908449 100644 --- a/openapi/tests/agent/assistant_test.go +++ b/openapi/tests/agent/assistant_test.go @@ -920,6 +920,533 @@ func TestListAssistantTags(t *testing.T) { }) } +// TestGetAssistant tests the get assistant detail endpoint +func TestGetAssistant(t *testing.T) { + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + // Get base URL from server config + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Register test client and get token + client := testutils.RegisterTestClient(t, "Agent Detail 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") + + t.Run("GetAssistantSuccess", func(t *testing.T) { + // First, get a list of assistants to find a valid assistant_id + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + assert.NotNil(t, listResp) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + // Get the first assistant's ID + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + t.Logf("Testing with assistant ID: %s", assistantID) + + // Now test getting the specific assistant + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Expect successful response + assert.Equal(t, http.StatusOK, resp.StatusCode, "Should successfully retrieve assistant") + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + // Response should have data field with assistant details + detailData, hasDetailData := response["data"].(map[string]interface{}) + assert.True(t, hasDetailData, "Response should have 'data' field") + assert.NotNil(t, detailData, "Assistant data should not be nil") + + // Verify assistant_id matches + returnedID, hasID := detailData["assistant_id"].(string) + assert.True(t, hasID, "Assistant should have assistant_id field") + assert.Equal(t, assistantID, returnedID, "Returned assistant_id should match requested ID") + + t.Logf("Successfully retrieved assistant: %s", returnedID) + }) + + t.Run("GetAssistantWithoutLocale", func(t *testing.T) { + // Get a valid assistant ID first + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + // Test without locale parameter - should return raw data (useful for form editing) + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + _, hasDetailData := response["data"].(map[string]interface{}) + assert.True(t, hasDetailData, "Response should have 'data' field") + + t.Logf("Successfully retrieved assistant without locale (raw data for editing)") + }) + + t.Run("GetAssistantWithLocale", func(t *testing.T) { + // Get a valid assistant ID first + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + // Test with locale parameter - should return translated data + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID+"?locale=zh-cn", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + t.Logf("Successfully retrieved assistant with zh-cn locale (translated)") + }) + + t.Run("GetAssistantBuiltInFieldsFiltered", func(t *testing.T) { + // Get a built-in assistant + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?built_in=true&pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No built-in assistants available for testing") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + // Get the built-in assistant detail + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + detailData, hasDetailData := response["data"].(map[string]interface{}) + assert.True(t, hasDetailData, "Response should have 'data' field") + + // Verify built-in flag + builtIn, hasBuiltIn := detailData["built_in"].(bool) + if hasBuiltIn && builtIn { + // Check that sensitive fields are filtered + prompts := detailData["prompts"] + workflow := detailData["workflow"] + tools := detailData["tools"] + kb := detailData["kb"] + mcp := detailData["mcp"] + options := detailData["options"] + + // These should be nil or absent for built-in assistants + assert.Nil(t, prompts, "Built-in assistant should not expose prompts") + assert.Nil(t, workflow, "Built-in assistant should not expose workflow") + assert.Nil(t, tools, "Built-in assistant should not expose tools") + assert.Nil(t, kb, "Built-in assistant should not expose kb") + assert.Nil(t, mcp, "Built-in assistant should not expose mcp") + assert.Nil(t, options, "Built-in assistant should not expose options") + + t.Logf("Built-in assistant sensitive fields correctly filtered") + } + }) + + t.Run("GetAssistantNotFound", func(t *testing.T) { + // Test with non-existent assistant ID + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/non-existent-id-12345", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Should return 404 Not Found + assert.Equal(t, http.StatusNotFound, resp.StatusCode, "Should return 404 for non-existent assistant") + + var errorResponse map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&errorResponse) + assert.NoError(t, err) + + assert.Contains(t, errorResponse, "error", "Error response should have 'error' field") + t.Logf("Correctly returned 404 for non-existent assistant") + }) + + t.Run("GetAssistantUnauthorized", func(t *testing.T) { + // Test without authentication + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/some-id", nil) + assert.NoError(t, err) + // No Authorization header + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Should return 401 without authentication") + t.Logf("Correctly rejected unauthorized request") + }) + + t.Run("GetAssistantEmptyID", func(t *testing.T) { + // Test with empty assistant ID (should be caught by router) + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // This should either redirect to list endpoint or return 404 + // Depends on router configuration + assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusMovedPermanently || resp.StatusCode == http.StatusOK) + t.Logf("Handled empty assistant ID (status: %d)", resp.StatusCode) + }) +} + +// TestGetAssistantPermissionFiltering tests that permission-based filtering works for single assistant retrieval +func TestGetAssistantPermissionFiltering(t *testing.T) { + serverURL := testutils.Prepare(t) + defer testutils.Clean() + + baseURL := "" + if openapi.Server != nil && openapi.Server.Config != nil { + baseURL = openapi.Server.Config.BaseURL + } + + // Create test client + client := testutils.RegisterTestClient(t, "Agent Detail Permission Test Client", []string{"https://localhost/callback"}) + defer testutils.CleanupTestClient(t, client.ClientID) + + // User 1 token + token1 := testutils.ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + t.Run("UserCanAccessPublicAssistant", func(t *testing.T) { + // Get a public assistant (if available) + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=10", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + // Find a public assistant + var publicAssistantID string + for _, item := range data { + assistant, ok := item.(map[string]interface{}) + if !ok { + continue + } + + isPublic, hasPublic := assistant["public"].(bool) + assistantID, hasID := assistant["assistant_id"].(string) + + if hasPublic && isPublic && hasID && assistantID != "" { + publicAssistantID = assistantID + break + } + } + + if publicAssistantID == "" { + t.Skip("No public assistants available for testing") + return + } + + // Test accessing public assistant + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+publicAssistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode, "User should be able to access public assistant") + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + t.Logf("User successfully accessed public assistant: %s", publicAssistantID) + }) + + t.Run("UserCanAccessOwnAssistant", func(t *testing.T) { + // This test assumes user has their own assistants + // In a real scenario, you would create a test assistant first + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + // Test accessing own assistant + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+token1.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Should be able to access (either own assistant or permission allows) + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden) + + if resp.StatusCode == http.StatusOK { + t.Logf("User successfully accessed assistant: %s", assistantID) + } else { + t.Logf("User does not have permission to access assistant: %s", assistantID) + } + }) +} + +// TestGetAssistantResponseStructure tests that the response structure matches expectations +func TestGetAssistantResponseStructure(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, "Agent Detail Response 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") + + t.Run("ResponseHasCorrectStructure", func(t *testing.T) { + // Get a valid assistant ID first + listReq, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + assert.NoError(t, err) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + assert.NoError(t, err) + defer listResp.Body.Close() + + var listResponse map[string]interface{} + err = json.NewDecoder(listResp.Body).Decode(&listResponse) + assert.NoError(t, err) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + t.Skip("No assistants available for testing") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + t.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + t.Skip("Assistant ID not found") + return + } + + // Get assistant detail + req, err := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + // Verify response structure + assert.Contains(t, response, "data", "Response should have 'data' field") + + // Verify data is an object (not an array) + detailData, ok := response["data"].(map[string]interface{}) + assert.True(t, ok, "Data field should be an object") + assert.NotNil(t, detailData, "Data should not be nil") + + // Verify essential assistant fields + assert.Contains(t, detailData, "assistant_id", "Assistant should have assistant_id") + assert.Contains(t, detailData, "name", "Assistant should have name") + assert.Contains(t, detailData, "type", "Assistant should have type") + + t.Logf("Response structure is correct for assistant: %s", assistantID) + }) +} + // BenchmarkListAssistants benchmarks the list assistants endpoint func BenchmarkListAssistants(b *testing.B) { // Convert testing.B to testing.T for Prepare/Clean @@ -951,3 +1478,66 @@ func BenchmarkListAssistants(b *testing.B) { resp.Body.Close() } } + +// BenchmarkGetAssistant benchmarks the get assistant detail endpoint +func BenchmarkGetAssistant(b *testing.B) { + // Convert testing.B to testing.T for Prepare/Clean + 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, "Agent Detail Benchmark 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") + + // Get a valid assistant ID for benchmarking + listReq, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants?pagesize=1", nil) + listReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + listResp, err := http.DefaultClient.Do(listReq) + if err != nil { + b.Fatalf("Failed to get assistant list: %v", err) + } + defer listResp.Body.Close() + + var listResponse map[string]interface{} + json.NewDecoder(listResp.Body).Decode(&listResponse) + + data, hasData := listResponse["data"].([]interface{}) + if !hasData || len(data) == 0 { + b.Skip("No assistants available for benchmarking") + return + } + + assistant, ok := data[0].(map[string]interface{}) + if !ok { + b.Skip("Invalid assistant data format") + return + } + + assistantID, ok := assistant["assistant_id"].(string) + if !ok || assistantID == "" { + b.Skip("Assistant ID not found") + return + } + + // Reset timer after setup + b.ResetTimer() + + // Run benchmark + for i := 0; i < b.N; i++ { + req, _ := http.NewRequest("GET", serverURL+baseURL+"/agent/assistants/"+assistantID, nil) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + b.Fatalf("Request failed: %v", err) + } + resp.Body.Close() + } +}