diff --git a/builder/rpc/csgbot_svc_client.go b/builder/rpc/csgbot_svc_client.go new file mode 100644 index 000000000..297f95f60 --- /dev/null +++ b/builder/rpc/csgbot_svc_client.go @@ -0,0 +1,246 @@ +package rpc + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "log/slog" + "net/http" + "strconv" + "time" + + "opencsg.com/csghub-server/common/errorx" + "opencsg.com/csghub-server/common/types" +) + +type CsgbotSvcClient interface { + DeleteWorkspaceFiles(ctx context.Context, userUUID string, username string, token string, agentName string) error + CreateKnowledgeBase(ctx context.Context, userUUID string, username string, token string, req *CreateKnowledgeBaseRequest) (*CreateKnowledgeBaseResponse, error) + DeleteKnowledgeBase(ctx context.Context, userUUID string, username string, token string, contentID string) error + UpdateKnowledgeBase(ctx context.Context, userUUID string, username string, token string, contentID string, req *types.UpdateAgentKnowledgeBaseRequest) error +} + +type CreateKnowledgeBaseRequest struct { + Name string `json:"name"` + Description string `json:"description"` +} + +type CreateKnowledgeBaseResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Data json.RawMessage `json:"data"` + IsComponent bool `json:"is_component"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Webhook bool `json:"webhook"` + Tags []string `json:"tags"` + Locked bool `json:"locked"` + McpEnabled bool `json:"mcp_enabled"` + AccessType string `json:"access_type"` + UserUUID string `json:"user_id"` // user uuid + FolderID string `json:"folder_id"` + Model string `json:"model"` +} + +type DeleteKnowledgeBaseRequest struct { + IDs []string `json:"ids"` +} + +type DeleteKnowledgeBaseResponse struct { + IDs []string `json:"ids"` + Total int `json:"total"` +} + +type CsgbotSvcHttpClientImpl struct { + hc *HttpClient +} + +func NewCsgbotSvcHttpClient(endpoint string, opts ...RequestOption) CsgbotSvcClient { + return &CsgbotSvcHttpClientImpl{ + hc: NewHttpClient(endpoint, opts...), + } +} + +// Delete workspace files for a code agent +// DELETE /api/v1/csgbot/codeAgent/{agent_name} +func (c *CsgbotSvcHttpClientImpl) DeleteWorkspaceFiles(ctx context.Context, userUUID string, username string, token string, agentName string) error { + rpcErrorCtx := map[string]any{ + "user_uuid": userUUID, + "service": "csgbot", + "api": "DELETE /api/v1/csgbot/codeAgent/{agent_name}", + } + + path := c.hc.endpoint + "/api/v1/csgbot/codeAgent/" + agentName + hreq, err := http.NewRequestWithContext(ctx, http.MethodDelete, path, nil) + if err != nil { + return errorx.InternalServerError(err, rpcErrorCtx) + } + hreq.Header.Set("Content-Type", "application/json") + hreq.Header.Set("user_uuid", userUUID) + hreq.Header.Set("user_name", username) + hreq.Header.Set("user_token", token) + + hresp, err := c.hc.Do(hreq) + if err != nil { + slog.ErrorContext(ctx, "failed to delete workspace files for code agent", "error", err, "rpc_error_ctx", rpcErrorCtx) + return errorx.RemoteSvcFail(errors.New("failed to delete workspace files for code agent"), rpcErrorCtx) + } + defer hresp.Body.Close() + + if hresp.StatusCode != http.StatusOK { + slog.ErrorContext(ctx, "failed to delete workspace files for code agent", "status_code", hresp.StatusCode, "rpc_error_ctx", rpcErrorCtx) + return errorx.RemoteSvcFail(errors.New("failed to delete workspace files for code agent"), rpcErrorCtx) + } + + return nil +} + +// Create knowledge base +// POST /api/v1/csgbot/langflow/flows/rag +func (c *CsgbotSvcHttpClientImpl) CreateKnowledgeBase(ctx context.Context, userUUID string, username string, token string, req *CreateKnowledgeBaseRequest) (*CreateKnowledgeBaseResponse, error) { + if req == nil { + return nil, errorx.BadRequest(errors.New("create knowledge base request is nil"), nil) + } + + rpcErrorCtx := map[string]any{ + "user_uuid": userUUID, + "service": "csgbot", + "api": "POST /api/v1/csgbot/langflow/flows/rag", + } + + jsonData, err := json.Marshal(req) + if err != nil { + return nil, errorx.InternalServerError(err, rpcErrorCtx) + } + buf := bytes.NewBuffer(jsonData) + path := c.hc.endpoint + "/api/v1/csgbot/langflow/flows/rag" + hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, path, buf) + if err != nil { + return nil, errorx.InternalServerError(err, rpcErrorCtx) + } + hreq.Header.Set("Content-Type", "application/json") + hreq.Header.Set("user_uuid", userUUID) + hreq.Header.Set("user_name", username) + hreq.Header.Set("user_token", token) + + hresp, err := c.hc.Do(hreq) + if err != nil { + slog.ErrorContext(ctx, "failed to create knowledge base in csgbot service", "error", err, "rpc_error_ctx", rpcErrorCtx) + return nil, errorx.RemoteSvcFail(errors.New("failed to create knowledge base in csgbot service"), rpcErrorCtx) + } + defer hresp.Body.Close() + if hresp.StatusCode != http.StatusOK { + slog.ErrorContext(ctx, "failed to create knowledge base in csgbot service", "status_code", hresp.StatusCode, "rpc_error_ctx", rpcErrorCtx) + return nil, errorx.RemoteSvcFail(errors.New("failed to create knowledge base in csgbot service"), rpcErrorCtx) + } + + body, err := io.ReadAll(hresp.Body) + if err != nil { + slog.ErrorContext(ctx, "failed to create knowledge base in csgbot service", "error", err, "rpc_error_ctx", rpcErrorCtx) + return nil, errorx.InternalServerError(err, rpcErrorCtx) + } + var resp CreateKnowledgeBaseResponse + err = json.Unmarshal(body, &resp) + if err != nil { + return nil, errorx.InternalServerError(err, rpcErrorCtx) + } + return &resp, nil +} + +// Delete knowledge base +// POST /api/v1/csgbot/langflow/flows/rag/delete +func (c *CsgbotSvcHttpClientImpl) DeleteKnowledgeBase(ctx context.Context, userUUID string, username string, token string, contentID string) error { + rpcErrorCtx := map[string]any{ + "user_uuid": userUUID, + "content_id": contentID, + "service": "csgbot", + "api": "POST /api/v1/csgbot/langflow/flows/rag/delete", + } + var resp DeleteKnowledgeBaseResponse + + req := DeleteKnowledgeBaseRequest{ + IDs: []string{contentID}, + } + jsonData, err := json.Marshal(req) + if err != nil { + return errorx.InternalServerError(err, rpcErrorCtx) + } + buf := bytes.NewBuffer(jsonData) + path := c.hc.endpoint + "/api/v1/csgbot/langflow/flows/rag/delete" + hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, path, buf) + if err != nil { + return errorx.InternalServerError(err, rpcErrorCtx) + } + + hreq.Header.Set("Content-Type", "application/json") + hreq.Header.Set("user_uuid", userUUID) + hreq.Header.Set("user_name", username) + hreq.Header.Set("user_token", token) + + hresp, err := c.hc.Do(hreq) + if err != nil { + slog.ErrorContext(ctx, "failed to delete knowledge base in csgbot service", "error", err, "rpc_error_ctx", rpcErrorCtx) + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service"), rpcErrorCtx) + } + defer hresp.Body.Close() + + if hresp.StatusCode != http.StatusOK { + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service, status code: "+strconv.Itoa(hresp.StatusCode)), rpcErrorCtx) + } + + body, err := io.ReadAll(hresp.Body) + if err != nil { + return errorx.InternalServerError(err, rpcErrorCtx) + } + if err := json.Unmarshal(body, &resp); err != nil { + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service, unmarshal response error: "+err.Error()), rpcErrorCtx) + } + + if resp.Total != 1 { + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service, total: "+strconv.Itoa(resp.Total)), rpcErrorCtx) + } + + if len(resp.IDs) == 0 { + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service, response IDs is empty"), rpcErrorCtx) + } + + if resp.IDs[0] != contentID { + return errorx.RemoteSvcFail(errors.New("failed to delete knowledge base in csgbot service, content ID mismatch: "+contentID+" != "+resp.IDs[0]), rpcErrorCtx) + } + return nil +} + +// Update knowledge base +// PUT /api/v1/csgbot/langflow/flows/rag/{content_id} +func (c *CsgbotSvcHttpClientImpl) UpdateKnowledgeBase(_ context.Context, _ string, _ string, _ string, _ string, _ *types.UpdateAgentKnowledgeBaseRequest) error { + return nil +} + +func NewCsgbotSvcHttpClientBuilder(endpoint string, opts ...RequestOption) CsgbotSvcClientBuilder { + return &CsgbotSvcHttpClientImpl{ + hc: NewHttpClient(endpoint, opts...), + } +} + +type CsgbotSvcClientBuilder interface { + WithRetry(attempts uint) CsgbotSvcClientBuilder + WithDelay(delay time.Duration) CsgbotSvcClientBuilder + Build() CsgbotSvcClient +} + +func (c *CsgbotSvcHttpClientImpl) WithRetry(attempts uint) CsgbotSvcClientBuilder { + c.hc = c.hc.WithRetry(attempts) + return c +} + +func (c *CsgbotSvcHttpClientImpl) WithDelay(delay time.Duration) CsgbotSvcClientBuilder { + c.hc = c.hc.WithDelay(delay) + return c +} + +func (c *CsgbotSvcHttpClientImpl) Build() CsgbotSvcClient { + return c +} diff --git a/builder/rpc/csgbot_svc_client_test.go b/builder/rpc/csgbot_svc_client_test.go new file mode 100644 index 000000000..abe633e79 --- /dev/null +++ b/builder/rpc/csgbot_svc_client_test.go @@ -0,0 +1,488 @@ +package rpc + +import ( + "context" + "encoding/json" + "errors" + "log/slog" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "opencsg.com/csghub-server/common/errorx" +) + +func setupTestCsgbotClient(server *httptest.Server) *CsgbotSvcHttpClientImpl { + return &CsgbotSvcHttpClientImpl{ + hc: &HttpClient{ + endpoint: server.URL, + hc: server.Client(), + logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)), + }, + } +} + +func TestDeleteWorkspaceFiles_Success(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-agent-name" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the request path + expectedPath := "/api/v1/csgbot/codeAgent/" + contentID + assert.Equal(t, expectedPath, r.URL.Path) + assert.Equal(t, http.MethodDelete, r.Method) + + // Verify headers + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, userUUID, r.Header.Get("user_uuid")) + assert.Equal(t, username, r.Header.Get("user_name")) + assert.Equal(t, token, r.Header.Get("user_token")) + + // Return success + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteWorkspaceFiles(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.NoError(t, err) +} + +func TestDeleteWorkspaceFiles_InternalServerError(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-agent-name" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteWorkspaceFiles(context.Background(), userUUID, username, token, contentID) + + // Verify result - should return RemoteSvcFail error + assert.Error(t, err) + assert.True(t, errors.Is(err, errorx.ErrRemoteServiceFail)) +} + +func TestNewCsgbotSvcHttpClient(t *testing.T) { + endpoint := "http://test-endpoint.com" + client := NewCsgbotSvcHttpClient(endpoint) + + // Verify the client is created + assert.NotNil(t, client) + + // Verify it implements the interface + var _ CsgbotSvcClient = client +} + +func TestCreateKnowledgeBase_Success(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + req := &CreateKnowledgeBaseRequest{ + Name: "Test KB", + Description: "Test description", + } + + expectedResponse := CreateKnowledgeBaseResponse{ + ID: "test-content-id", + Name: "Test KB", + Description: "", + IsComponent: false, + Webhook: false, + Tags: []string{"tag1"}, + Locked: false, + McpEnabled: false, + AccessType: "PRIVATE", + UserUUID: userUUID, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the request path + expectedPath := "/api/v1/csgbot/langflow/flows/rag" + assert.Equal(t, expectedPath, r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + // Verify headers + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, userUUID, r.Header.Get("user_uuid")) + assert.Equal(t, username, r.Header.Get("user_name")) + assert.Equal(t, token, r.Header.Get("user_token")) + + // Return success with response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "test-content-id", + "name": "Test KB", + "description": "", + "data": {}, + "is_component": false, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + "webhook": false, + "tags": ["tag1"], + "locked": false, + "mcp_enabled": false, + "access_type": "PRIVATE", + "user_id": "test-user-uuid", + "folder_id": "test-folder-id" + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + resp, err := client.CreateKnowledgeBase(context.Background(), userUUID, username, token, req) + + // Verify result + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, expectedResponse.ID, resp.ID) + assert.Equal(t, expectedResponse.Name, resp.Name) + assert.Equal(t, expectedResponse.Description, resp.Description) + assert.Equal(t, expectedResponse.UserUUID, resp.UserUUID) +} + +func TestCreateKnowledgeBase_DescriptionIsEmpty(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + req := &CreateKnowledgeBaseRequest{ + Name: "Test KB", + Description: "", + } + + expectedResponse := CreateKnowledgeBaseResponse{ + ID: "test-content-id", + Name: "Test KB", + Description: "", + IsComponent: false, + Webhook: false, + Tags: []string{"tag1"}, + Locked: false, + McpEnabled: false, + AccessType: "PRIVATE", + UserUUID: userUUID, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the request path + expectedPath := "/api/v1/csgbot/langflow/flows/rag" + assert.Equal(t, expectedPath, r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + // Verify headers + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, userUUID, r.Header.Get("user_uuid")) + assert.Equal(t, username, r.Header.Get("user_name")) + assert.Equal(t, token, r.Header.Get("user_token")) + + // Return success with response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "id": "test-content-id", + "name": "Test KB", + "description": "", + "data": {}, + "is_component": false, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + "webhook": false, + "tags": ["tag1"], + "locked": false, + "mcp_enabled": false, + "access_type": "PRIVATE", + "user_id": "test-user-uuid", + "folder_id": "test-folder-id" + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + resp, err := client.CreateKnowledgeBase(context.Background(), userUUID, username, token, req) + + // Verify result + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, expectedResponse.ID, resp.ID) + assert.Equal(t, expectedResponse.Name, resp.Name) + assert.Equal(t, expectedResponse.Description, resp.Description) + assert.Equal(t, expectedResponse.UserUUID, resp.UserUUID) +} + +func TestCreateKnowledgeBase_NilRequest(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + resp, err := client.CreateKnowledgeBase(context.Background(), userUUID, username, token, nil) + + // Verify result + assert.Error(t, err) + assert.Nil(t, resp) + assert.True(t, errors.Is(err, errorx.ErrBadRequest)) +} + +func TestCreateKnowledgeBase_Non200Status(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + req := &CreateKnowledgeBaseRequest{ + Name: "Test KB", + Description: "", + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + resp, err := client.CreateKnowledgeBase(context.Background(), userUUID, username, token, req) + + // Verify result + assert.Error(t, err) + assert.Nil(t, resp) + assert.True(t, errors.Is(err, errorx.ErrRemoteServiceFail)) +} + +func TestCreateKnowledgeBase_ReadBodyError(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + req := &CreateKnowledgeBaseRequest{ + Name: "Test KB", + Description: "", + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "1") + w.WriteHeader(http.StatusOK) + // Close the connection immediately to cause read error + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + resp, err := client.CreateKnowledgeBase(context.Background(), userUUID, username, token, req) + + // Verify result + assert.Error(t, err) + assert.Nil(t, resp) +} + +func TestDeleteKnowledgeBase_Success(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify the request path + expectedPath := "/api/v1/csgbot/langflow/flows/rag/delete" + assert.Equal(t, expectedPath, r.URL.Path) + assert.Equal(t, http.MethodPost, r.Method) + + // Verify headers + assert.Equal(t, "application/json", r.Header.Get("Content-Type")) + assert.Equal(t, userUUID, r.Header.Get("user_uuid")) + assert.Equal(t, username, r.Header.Get("user_name")) + assert.Equal(t, token, r.Header.Get("user_token")) + + // Verify request body + var reqBody DeleteKnowledgeBaseRequest + err := json.NewDecoder(r.Body).Decode(&reqBody) + assert.NoError(t, err) + assert.Equal(t, []string{contentID}, reqBody.IDs) + + // Return success with response + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "ids": ["test-content-id"], + "total": 1 + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.NoError(t, err) +} + +func TestDeleteKnowledgeBase_Non200Status(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) + assert.True(t, errors.Is(err, errorx.ErrRemoteServiceFail)) +} + +func TestDeleteKnowledgeBase_ReadBodyError(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Length", "1") + w.WriteHeader(http.StatusOK) + // Close the connection immediately to cause read error + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) +} + +func TestDeleteKnowledgeBase_UnmarshalError(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`invalid json`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) + assert.Contains(t, err.Error(), "unmarshal response error") +} + +func TestDeleteKnowledgeBase_TotalMismatch(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "ids": ["test-content-id"], + "total": 2 + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) + assert.Contains(t, err.Error(), "total: 2") +} + +func TestDeleteKnowledgeBase_EmptyIDs(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "ids": [], + "total": 1 + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) + assert.Contains(t, err.Error(), "response IDs is empty") +} + +func TestDeleteKnowledgeBase_ContentIDMismatch(t *testing.T) { + userUUID := "test-user-uuid" + username := "test-username" + token := "test-token" + contentID := "test-content-id" + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{ + "ids": ["different-content-id"], + "total": 1 + }`)) + })) + defer server.Close() + + client := setupTestCsgbotClient(server) + + // Execute the test + err := client.DeleteKnowledgeBase(context.Background(), userUUID, username, token, contentID) + + // Verify result + assert.Error(t, err) + assert.Contains(t, err.Error(), "content ID mismatch") +}