feat: 配置 golangci-lint 静态分析并修复存量违规
- 新增 backend/.golangci.yml 配置 12 个 linter(forbidigo、errorlint、errcheck、staticcheck、revive、gocritic、gosec、bodyclose、noctx、nilerr、goimports、gocyclo) - 新增 lefthook.yml 配置 pre-commit hook 自动运行 lint - 修复存量代码违规:errors.Is/As 替换、zap.Error 替换、import 排序、errcheck 修复 - 更新 README 补充编码规范说明 - 归档 backend-code-lint 变更
This commit is contained in:
@@ -11,20 +11,21 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"nex/backend/internal/conversion"
|
||||
"nex/backend/internal/conversion/anthropic"
|
||||
"nex/backend/internal/handler"
|
||||
"nex/backend/internal/handler/middleware"
|
||||
"nex/backend/internal/provider"
|
||||
"nex/backend/internal/repository"
|
||||
"nex/backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"nex/backend/internal/conversion"
|
||||
"nex/backend/internal/conversion/anthropic"
|
||||
openaiConv "nex/backend/internal/conversion/openai"
|
||||
"nex/backend/internal/handler"
|
||||
"nex/backend/internal/handler/middleware"
|
||||
"nex/backend/internal/provider"
|
||||
"nex/backend/internal/repository"
|
||||
"nex/backend/internal/service"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -39,7 +40,8 @@ func setupConversionTest(t *testing.T) (*gin.Engine, *gorm.DB, *httptest.Server)
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 默认返回成功,由各测试 case 覆盖
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"error":"not mocked"}`))
|
||||
_, err := w.Write([]byte(`{"error":"not mocked"}`))
|
||||
require.NoError(t, err)
|
||||
}))
|
||||
|
||||
db := setupTestDB(t)
|
||||
@@ -124,7 +126,6 @@ func createProviderAndModel(t *testing.T, r *gin.Engine, providerID, protocol, m
|
||||
require.Equal(t, 201, w.Code)
|
||||
|
||||
modelBody, _ := json.Marshal(map[string]string{
|
||||
|
||||
"provider_id": providerID,
|
||||
"model_name": modelName,
|
||||
})
|
||||
@@ -143,9 +144,10 @@ func TestConversion_OpenAIToAnthropic_NonStream(t *testing.T) {
|
||||
// 配置上游返回 Anthropic 格式响应
|
||||
upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 验证请求被转换为 Anthropic 格式
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req map[string]any
|
||||
json.Unmarshal(body, &req)
|
||||
require.NoError(t, json.Unmarshal(body, &req))
|
||||
|
||||
assert.Equal(t, "/v1/messages", r.URL.Path)
|
||||
assert.Contains(t, r.Header.Get("Content-Type"), "application/json")
|
||||
@@ -166,7 +168,7 @@ func TestConversion_OpenAIToAnthropic_NonStream(t *testing.T) {
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(resp))
|
||||
})
|
||||
|
||||
createProviderAndModel(t, r, "anthropic_p", "anthropic", "claude-3-opus", upstream.URL)
|
||||
@@ -189,13 +191,16 @@ func TestConversion_OpenAIToAnthropic_NonStream(t *testing.T) {
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
var resp map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "chat.completion", resp["object"])
|
||||
|
||||
choices := resp["choices"].([]any)
|
||||
choices, ok := resp["choices"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, choices, 1)
|
||||
choice := choices[0].(map[string]any)
|
||||
msg := choice["message"].(map[string]any)
|
||||
choice, ok := choices[0].(map[string]any)
|
||||
require.True(t, ok)
|
||||
msg, ok := choice["message"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Contains(t, msg["content"], "Hello from Anthropic!")
|
||||
}
|
||||
|
||||
@@ -203,9 +208,10 @@ func TestConversion_AnthropicToOpenAI_NonStream(t *testing.T) {
|
||||
r, _, upstream := setupConversionTest(t)
|
||||
|
||||
upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req map[string]any
|
||||
json.Unmarshal(body, &req)
|
||||
require.NoError(t, json.Unmarshal(body, &req))
|
||||
|
||||
assert.Equal(t, "/chat/completions", r.URL.Path)
|
||||
assert.Contains(t, r.Header.Get("Authorization"), "Bearer test-key")
|
||||
@@ -229,7 +235,7 @@ func TestConversion_AnthropicToOpenAI_NonStream(t *testing.T) {
|
||||
},
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
require.NoError(t, json.NewEncoder(w).Encode(resp))
|
||||
})
|
||||
|
||||
createProviderAndModel(t, r, "openai_p", "openai", "gpt-4", upstream.URL)
|
||||
@@ -252,12 +258,14 @@ func TestConversion_AnthropicToOpenAI_NonStream(t *testing.T) {
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
var resp map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.Equal(t, "message", resp["type"])
|
||||
|
||||
content := resp["content"].([]any)
|
||||
content, ok := resp["content"].([]any)
|
||||
require.True(t, ok)
|
||||
require.Len(t, content, 1)
|
||||
block := content[0].(map[string]any)
|
||||
block, ok2 := content[0].(map[string]any)
|
||||
require.True(t, ok2)
|
||||
assert.Contains(t, block["text"], "Hello from OpenAI!")
|
||||
}
|
||||
|
||||
@@ -269,21 +277,23 @@ func TestConversion_OpenAIToOpenAI_Passthrough(t *testing.T) {
|
||||
upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/chat/completions", r.URL.Path)
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req map[string]any
|
||||
json.Unmarshal(body, &req)
|
||||
require.NoError(t, json.Unmarshal(body, &req))
|
||||
// Smart Passthrough: 请求体中的统一 ID 应被改写为上游模型名
|
||||
assert.Equal(t, "gpt-4", req["model"])
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// 上游返回上游模型名
|
||||
w.Write([]byte(`{"id":"chatcmpl-pass","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"passthrough"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":1,"total_tokens":6}}`))
|
||||
_, err = w.Write([]byte(`{"id":"chatcmpl-pass","object":"chat.completion","model":"gpt-4","choices":[{"index":0,"message":{"role":"assistant","content":"passthrough"},"finish_reason":"stop"}],"usage":{"prompt_tokens":5,"completion_tokens":1,"total_tokens":6}}`))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
createProviderAndModel(t, r, "openai_p", "openai", "gpt-4", upstream.URL)
|
||||
|
||||
reqBody := map[string]any{
|
||||
"model": "openai_p/gpt-4", // 客户端发送统一 ID
|
||||
"model": "openai_p/gpt-4", // 客户端发送统一 ID
|
||||
"messages": []map[string]any{{"role": "user", "content": "test"}},
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
@@ -304,21 +314,23 @@ func TestConversion_AnthropicToAnthropic_Passthrough(t *testing.T) {
|
||||
upstream.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/v1/messages", r.URL.Path)
|
||||
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
var req map[string]any
|
||||
json.Unmarshal(body, &req)
|
||||
require.NoError(t, json.Unmarshal(body, &req))
|
||||
// Smart Passthrough: 请求体中的统一 ID 应被改写为上游模型名
|
||||
assert.Equal(t, "claude-3-opus", req["model"])
|
||||
|
||||
// 上游返回上游模型名
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"id":"msg-pass","type":"message","role":"assistant","model":"claude-3-opus","content":[{"type":"text","text":"passthrough"}],"stop_reason":"end_turn","usage":{"input_tokens":5,"output_tokens":1}}`))
|
||||
_, err = w.Write([]byte(`{"id":"msg-pass","type":"message","role":"assistant","model":"claude-3-opus","content":[{"type":"text","text":"passthrough"}],"stop_reason":"end_turn","usage":{"input_tokens":5,"output_tokens":1}}`))
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
createProviderAndModel(t, r, "anthropic_p", "anthropic", "claude-3-opus", upstream.URL)
|
||||
|
||||
reqBody := map[string]any{
|
||||
"model": "anthropic_p/claude-3-opus", // 客户端发送统一 ID
|
||||
"model": "anthropic_p/claude-3-opus", // 客户端发送统一 ID
|
||||
"max_tokens": 1024,
|
||||
"messages": []map[string]any{{"role": "user", "content": "test"}},
|
||||
}
|
||||
@@ -352,7 +364,8 @@ func TestConversion_OpenAIToAnthropic_Stream(t *testing.T) {
|
||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n",
|
||||
}
|
||||
for _, e := range events {
|
||||
w.Write([]byte(e))
|
||||
_, err := w.Write([]byte(e))
|
||||
require.NoError(t, err)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
@@ -393,7 +406,8 @@ func TestConversion_AnthropicToOpenAI_Stream(t *testing.T) {
|
||||
"data: [DONE]\n\n",
|
||||
}
|
||||
for _, e := range events {
|
||||
w.Write([]byte(e))
|
||||
_, err := w.Write([]byte(e))
|
||||
require.NoError(t, err)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
@@ -447,11 +461,13 @@ func TestConversion_Models_CrossProtocol(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var anthropicResp map[string]any
|
||||
json.Unmarshal(anthropicBody, &anthropicResp)
|
||||
data := anthropicResp["data"].([]any)
|
||||
require.NoError(t, json.Unmarshal(anthropicBody, &anthropicResp))
|
||||
data, okd := anthropicResp["data"].([]any)
|
||||
require.True(t, okd)
|
||||
assert.Len(t, data, 2)
|
||||
|
||||
first := data[0].(map[string]any)
|
||||
first, okf := data[0].(map[string]any)
|
||||
require.True(t, okf)
|
||||
assert.Equal(t, "gpt-4", first["id"])
|
||||
assert.Equal(t, "model", first["type"])
|
||||
|
||||
@@ -466,11 +482,12 @@ func TestConversion_Models_CrossProtocol(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
var openaiResp map[string]any
|
||||
json.Unmarshal(openaiBody, &err)
|
||||
json.Unmarshal(openaiBody, &openaiResp)
|
||||
oaiData := openaiResp["data"].([]any)
|
||||
require.NoError(t, json.Unmarshal(openaiBody, &openaiResp))
|
||||
oaiData, oki := openaiResp["data"].([]any)
|
||||
require.True(t, oki)
|
||||
assert.Len(t, oaiData, 1)
|
||||
firstOai := oaiData[0].(map[string]any)
|
||||
firstOai, okf2 := oaiData[0].(map[string]any)
|
||||
require.True(t, okf2)
|
||||
assert.Equal(t, "claude-3-opus", firstOai["id"])
|
||||
}
|
||||
|
||||
@@ -537,7 +554,7 @@ func TestConversion_ProviderWithProtocol(t *testing.T) {
|
||||
require.Equal(t, 201, w.Code)
|
||||
|
||||
var created map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &created)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created))
|
||||
assert.Equal(t, "sk-test", created["api_key"])
|
||||
|
||||
// 获取时应包含 protocol
|
||||
@@ -547,7 +564,7 @@ func TestConversion_ProviderWithProtocol(t *testing.T) {
|
||||
assert.Equal(t, 200, w.Code)
|
||||
|
||||
var fetched map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &fetched)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &fetched))
|
||||
assert.Equal(t, "anthropic", fetched["protocol"])
|
||||
}
|
||||
|
||||
@@ -570,11 +587,13 @@ func TestConversion_ProviderDefaultProtocol(t *testing.T) {
|
||||
require.Equal(t, 201, w.Code)
|
||||
|
||||
var created map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &created)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created))
|
||||
assert.Equal(t, "openai", created["protocol"])
|
||||
}
|
||||
|
||||
// Suppress unused imports
|
||||
var _ = fmt.Sprintf
|
||||
var _ = strings.Contains
|
||||
var _ = time.Second
|
||||
var (
|
||||
_ = fmt.Sprintf
|
||||
_ = strings.Contains
|
||||
_ = time.Second
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user