Add Go service/handler tests for API contract parity (#16905)

This commit is contained in:
euvre
2026-07-20 20:02:41 +08:00
committed by GitHub
parent 2f7c2eb53c
commit f45f03a016
46 changed files with 1553 additions and 214 deletions

View File

@@ -402,11 +402,11 @@ func (h *DocumentHandler) DeleteDocuments(c *gin.Context) {
ids = *req.IDs
}
if len(ids) > 0 && req.DeleteAll {
common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "should not provide both ids and delete_all")
common.ResponseWithCodeData(c, common.CodeDataError, nil, "should not provide both ids and delete_all")
return
}
if len(ids) == 0 && !req.DeleteAll {
common.ResponseWithCodeData(c, common.CodeArgumentError, nil, "should either provide doc ids or set delete_all(true)")
common.ResponseWithCodeData(c, common.CodeDataError, nil, "should either provide doc ids or set delete_all(true)")
return
}
@@ -498,7 +498,7 @@ func (h *DocumentHandler) ListDocuments(c *gin.Context) {
userID := c.GetString("user_id")
if !h.datasetService.Accessible(datasetID, userID) {
common.ResponseWithCodeData(c, common.CodeAuthenticationError, nil, "No authorization to access the dataset.")
common.ResponseWithCodeData(c, common.CodeDataError, nil, fmt.Sprintf("You don't own the dataset %s.", datasetID))
return
}
@@ -530,7 +530,9 @@ func (h *DocumentHandler) ListDocuments(c *gin.Context) {
return
}
// Use kbID to filter documents
// Use kbID to filter documents.
// Note: create_time_from / create_time_to are applied post-query (not at DB level)
// so that total reflects the unfiltered count, matching the Python API contract.
documents, total, err := h.documentService.ListDocumentsByDatasetIDWithOptions(opts, page, pageSize)
if err != nil {
common.ResponseWithCodeData(c, 1, map[string]interface{}{"total": 0, "docs": []interface{}{}}, "failed to get documents")
@@ -539,6 +541,12 @@ func (h *DocumentHandler) ListDocuments(c *gin.Context) {
docs := make([]map[string]interface{}, 0, len(documents))
for _, doc := range documents {
if opts.CreateTimeFrom > 0 && doc.CreateTime != nil && *doc.CreateTime < opts.CreateTimeFrom {
continue
}
if opts.CreateTimeTo > 0 && doc.CreateTime != nil && *doc.CreateTime > opts.CreateTimeTo {
continue
}
metaFields, err := h.documentService.GetDocumentMetadataByID(doc.ID)
if err != nil {
metaFields = make(map[string]interface{})
@@ -1387,31 +1395,38 @@ func (h *DocumentHandler) ListIngestionTasks(c *gin.Context) {
}
type StartParseDocumentsRequest struct {
DatasetID string `json:"dataset_id"`
Documents []string `json:"documents" binding:"required"`
DocumentIDs []string `json:"document_ids" binding:"required"`
}
func (h *DocumentHandler) StartIngestionTask(c *gin.Context) {
datasetID := c.Param("dataset_id")
var req StartParseDocumentsRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
common.ResponseWithCodeData(c, common.CodeDataError, nil, "`document_ids` is required")
return
}
userID := c.GetString("user_id")
if !h.datasetService.Accessible(req.DatasetID, userID) {
common.ResponseWithCodeData(c, common.CodeAuthenticationError, nil, "No authorization to access the dataset.")
if !h.datasetService.Accessible(datasetID, userID) {
common.ResponseWithCodeData(c, common.CodeDataError, nil, fmt.Sprintf("You don't own the dataset %s.", datasetID))
return
}
parseResult, err := h.documentService.IngestDocuments(req.DatasetID, userID, req.Documents)
parseResult, err := h.documentService.IngestDocuments(datasetID, userID, req.DocumentIDs)
if err != nil {
common.ResponseWithCodeData(c, common.CodeExceptionError, nil, err.Error())
return
}
common.SuccessWithData(c, parseResult, "success")
successCount := 0
for _, r := range parseResult {
if strings.HasPrefix(r.Result, "task_id:") {
successCount++
}
}
common.SuccessWithData(c, map[string]interface{}{"success_count": successCount}, "success")
}
type StopIngestionsRequest struct {
@@ -1510,7 +1525,7 @@ func (h *DocumentHandler) StopParseDocuments(c *gin.Context) {
userID := c.GetString("user_id")
if !h.datasetService.Accessible(datasetID, userID) {
common.ResponseWithCodeData(c, common.CodeAuthenticationError, nil, "You don't own the dataset.")
common.ResponseWithCodeData(c, common.CodeDataError, nil, fmt.Sprintf("You don't own the dataset %s.", datasetID))
return
}
@@ -1684,19 +1699,32 @@ func (h *DocumentHandler) handleBatchUpdateDocumentMetadatas(c *gin.Context) {
return
}
var req documentMetadataBatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, err.Error())
var rawBody map[string]interface{}
if err := c.ShouldBindJSON(&rawBody); err != nil {
common.ResponseWithCodeData(c, common.CodeDataError, nil, "Invalid request payload: expected object, got "+inferJSONType(err))
return
}
if req.Selector == nil {
req.Selector = &document.DocumentMetadataSelector{}
selector, errMsg := parseMetadataSelector(rawBody["selector"])
if errMsg != "" {
common.ResponseWithCodeData(c, common.CodeDataError, nil, errMsg)
return
}
if req.Updates == nil {
req.Updates = []document.DocumentMetadataUpdate{}
updates, errMsg := parseMetadataUpdates(rawBody["updates"])
if errMsg != "" {
common.ResponseWithCodeData(c, common.CodeDataError, nil, errMsg)
return
}
if req.Deletes == nil {
req.Deletes = []document.DocumentMetadataDelete{}
deletes, errMsg := parseMetadataDeletes(rawBody["deletes"])
if errMsg != "" {
common.ResponseWithCodeData(c, common.CodeDataError, nil, errMsg)
return
}
req := documentMetadataBatchRequest{
Selector: selector,
Updates: updates,
Deletes: deletes,
}
resp, code, err := h.documentService.BatchUpdateDocumentMetadatas(datasetID, req.Selector, req.Updates, req.Deletes)
@@ -1706,3 +1734,95 @@ func (h *DocumentHandler) handleBatchUpdateDocumentMetadatas(c *gin.Context) {
}
common.SuccessWithData(c, resp, "success")
}
func inferJSONType(err error) string {
s := err.Error()
if strings.Contains(s, "array") {
return "array"
}
if strings.Contains(s, "number") {
return "number"
}
if strings.Contains(s, "string") {
return "string"
}
if strings.Contains(s, "bool") {
return "bool"
}
return "unknown"
}
func parseMetadataSelector(raw interface{}) (*document.DocumentMetadataSelector, string) {
if raw == nil {
return &document.DocumentMetadataSelector{}, ""
}
m, ok := raw.(map[string]interface{})
if !ok {
return nil, "selector must be an object."
}
selector := &document.DocumentMetadataSelector{}
if v, ok := m["document_ids"]; ok && v != nil {
ids, ok := v.([]interface{})
if !ok {
return nil, "document_ids must be a list."
}
for _, id := range ids {
selector.DocumentIDs = append(selector.DocumentIDs, id.(string))
}
}
if v, ok := m["metadata_condition"]; ok && v != nil {
mc, ok := v.(map[string]interface{})
if !ok {
return nil, "metadata_condition must be an object."
}
selector.MetadataCondition = mc
}
return selector, ""
}
func parseMetadataUpdates(raw interface{}) ([]document.DocumentMetadataUpdate, string) {
if raw == nil {
return []document.DocumentMetadataUpdate{}, ""
}
arr, ok := raw.([]interface{})
if !ok {
return nil, "updates and deletes must be lists."
}
updates := make([]document.DocumentMetadataUpdate, 0, len(arr))
for _, item := range arr {
m, ok := item.(map[string]interface{})
if !ok {
return nil, "Each update requires key and value."
}
key, _ := m["key"].(string)
if key == "" {
return nil, "Each update requires key and value."
}
value := m["value"]
updates = append(updates, document.DocumentMetadataUpdate{Key: key, Value: value})
}
return updates, ""
}
func parseMetadataDeletes(raw interface{}) ([]document.DocumentMetadataDelete, string) {
if raw == nil {
return []document.DocumentMetadataDelete{}, ""
}
arr, ok := raw.([]interface{})
if !ok {
return nil, "updates and deletes must be lists."
}
deletes := make([]document.DocumentMetadataDelete, 0, len(arr))
for _, item := range arr {
m, ok := item.(map[string]interface{})
if !ok {
return nil, "Each delete requires key."
}
key, _ := m["key"].(string)
if key == "" {
return nil, "Each delete requires key."
}
deletes = append(deletes, document.DocumentMetadataDelete{Key: key})
}
return deletes, ""
}