Files
ai-agent/internal/handlers/dashboard/request_parameter_contract_test.go
t 18c9354095 refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
2026-08-28 22:23:13 +08:00

135 lines
3.4 KiB
Go

package dashboard
import (
"go/ast"
"go/parser"
"go/token"
"reflect"
"strconv"
"strings"
"testing"
)
func TestHandlerRequestParametersUseSnakeCase(t *testing.T) {
for _, dir := range []string{".", "../api", "../../middleware", "../../pkg/httpx"} {
fset := token.NewFileSet()
packages, err := parser.ParseDir(fset, dir, nil, 0)
if err != nil {
t.Fatalf("parse handler directory %s: %v", dir, err)
}
for _, pkg := range packages {
for filename, file := range pkg.Files {
ast.Inspect(file, func(node ast.Node) bool {
switch value := node.(type) {
case *ast.CallExpr:
checkHandlerParameterCall(t, filename, value)
case *ast.CompositeLit:
checkQueryFilterParameter(t, filename, value)
case *ast.Field:
checkHandlerRequestTag(t, filename, value)
}
return true
})
}
}
}
}
func checkHandlerParameterCall(t *testing.T, filename string, call *ast.CallExpr) {
if identifier, ok := call.Fun.(*ast.Ident); ok {
if identifier.Name == "requestExternalValue" && len(call.Args) > 2 {
checkSnakeCaseStringLiteral(t, filename, call.Args[2])
}
return
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return
}
identifier, ok := selector.X.(*ast.Ident)
if !ok {
return
}
argIndex := -1
if identifier.Name == "params" {
switch selector.Sel.Name {
case "Get", "GetInt", "GetInt64", "GetBool", "GetTime", "GetInt64Arr", "FormValue":
argIndex = 1
}
} else if identifier.Name == "ctx" {
switch selector.Sel.Name {
case "Query", "DefaultQuery", "GetQuery", "QueryArray", "PostForm", "PostFormArray":
argIndex = 0
}
}
if argIndex < 0 || len(call.Args) <= argIndex {
return
}
checkSnakeCaseStringLiteral(t, filename, call.Args[argIndex])
}
func checkQueryFilterParameter(t *testing.T, filename string, literal *ast.CompositeLit) {
selector, ok := literal.Type.(*ast.SelectorExpr)
if !ok || selector.Sel.Name != "QueryFilter" {
return
}
identifier, ok := selector.X.(*ast.Ident)
if !ok || identifier.Name != "params" {
return
}
for _, element := range literal.Elts {
pair, ok := element.(*ast.KeyValueExpr)
if !ok {
continue
}
key, ok := pair.Key.(*ast.Ident)
if ok && key.Name == "ParamName" {
checkSnakeCaseStringLiteral(t, filename, pair.Value)
}
}
}
func checkHandlerRequestTag(t *testing.T, filename string, field *ast.Field) {
if field.Tag == nil {
return
}
rawTag, err := strconv.Unquote(field.Tag.Value)
if err != nil {
t.Errorf("%s: invalid struct tag %s: %v", filename, field.Tag.Value, err)
return
}
for _, key := range []string{"json", "form", "query", "uri"} {
name := strings.Split(reflect.StructTag(rawTag).Get(key), ",")[0]
if name != "" && name != "-" && !isSnakeCaseHandlerParameter(name) {
t.Errorf("%s: %s tag %q must use snake_case", filename, key, name)
}
}
}
func checkSnakeCaseStringLiteral(t *testing.T, filename string, expression ast.Expr) {
literal, ok := expression.(*ast.BasicLit)
if !ok || literal.Kind != token.STRING {
return
}
name, err := strconv.Unquote(literal.Value)
if err != nil {
t.Errorf("%s: invalid request parameter %s: %v", filename, literal.Value, err)
return
}
if !isSnakeCaseHandlerParameter(name) {
t.Errorf("%s: request parameter %q must use snake_case", filename, name)
}
}
func isSnakeCaseHandlerParameter(name string) bool {
for _, r := range name {
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '_' {
continue
}
return false
}
return name != ""
}