feat(captain): align custom tool limits

This commit is contained in:
2026-06-07 16:06:19 +08:00
parent 0d8de3be0b
commit dd7bcf4118
6 changed files with 166 additions and 13 deletions
@@ -5,7 +5,9 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
"github.com/gin-gonic/gin"
@@ -163,6 +165,87 @@ func (s *CaptainCustomToolCRUDTestSuite) TestCreate_默认GET方法() {
assert.Equal(s.T(), "GET", resp["http_method"]) // default HTTP method
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_省略Slug时使用ChatwootCustom前缀和下划线() {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Lookup Order",
"endpoint_url": "https://example.com/orders",
},
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), "custom_lookup_order", resp["slug"])
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_重复标题Slug追加随机后缀() {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Lookup Order",
"endpoint_url": "https://example.com/orders",
},
}
w1 := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
s.Require().Equal(http.StatusOK, w1.Code)
w2 := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
s.Require().Equal(http.StatusOK, w2.Code)
var first, second map[string]interface{}
s.Require().NoError(json.Unmarshal(w1.Body.Bytes(), &first))
s.Require().NoError(json.Unmarshal(w2.Body.Bytes(), &second))
assert.Equal(s.T(), "custom_lookup_order", first["slug"])
assert.NotEqual(s.T(), first["slug"], second["slug"])
assert.Regexp(s.T(), regexp.MustCompile(`^custom_lookup_order_[a-z0-9]{6}$`), second["slug"])
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_生成Slug最长64字符() {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": strings.Repeat("Very Long Title ", 10),
"endpoint_url": "https://example.com/long",
},
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
slug := resp["slug"].(string)
assert.LessOrEqual(s.T(), len(slug), 64)
assert.True(s.T(), strings.HasPrefix(slug, "custom_"))
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_超过每账户15个工具返回422() {
for i := 0; i < 15; i++ {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Limit Tool " + strconv.Itoa(i),
"slug": "limit-tool-" + strconv.Itoa(i),
"endpoint_url": "https://example.com/limit" + strconv.Itoa(i),
},
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
s.Require().Equal(http.StatusOK, w.Code)
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "One Too Many",
"endpoint_url": "https://example.com/too-many",
},
})
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), "You can create a maximum of 15 custom tools per account", resp["error"])
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_无效JSON返回400() {
// ShouldBindJSON 在 JSON 解析失败时返回 400
w := httptest.NewRecorder()
@@ -1,6 +1,7 @@
package v1
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
@@ -42,6 +43,10 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) {
tool, err := h.svc.Create(c.Request.Context(), accountID, &req)
if err != nil {
applogger.L().Errorf("Create captain custom tool: %v", err)
if errors.Is(err, service.ErrCaptainCustomToolLimitExceeded) {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": service.ErrCaptainCustomToolLimitExceeded.Error()})
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create custom tool")
return
}
@@ -195,7 +195,7 @@ func TestCaptainCustomToolHandler_ChatwootToolPayloadsAndScope(t *testing.T) {
var created map[string]any
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created))
assert.NotContains(t, created, "success")
assert.Equal(t, "lookup-order", created["slug"])
assert.Equal(t, "custom_lookup_order", created["slug"])
assert.Equal(t, "POST", created["http_method"])
toolID := uint(created["id"].(float64))