From 9c36b75a7d2feaf20db7de25b3ab33f108e04279 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Tue, 28 Jul 2026 16:56:17 +0800 Subject: [PATCH 1/9] fix(claude): strip cache control from deferred tools --- ...teway_anthropic_apikey_passthrough_test.go | 24 ++++++++++ .../service/gateway_anthropic_passthrough.go | 1 + .../gateway_context_management_test.go | 46 +++++++++++++++++++ .../internal/service/gateway_count_tokens.go | 2 + .../internal/service/gateway_tool_rewrite.go | 43 +++++++++++++++-- .../service/gateway_tool_rewrite_test.go | 28 +++++++++++ .../service/gateway_upstream_request.go | 1 + 7 files changed, 141 insertions(+), 4 deletions(-) diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index 4fa52cc9ce..102c2b2904 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -769,6 +769,30 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_BuildRequestRejectsInvalidBas require.Error(t, err) } +func TestGatewayService_AnthropicAPIKeyPassthrough_StripsDeferredToolCacheControl(t *testing.T) { + gin.SetMode(gin.TestMode) + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) + svc := &GatewayService{cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}}}} + account := &Account{Platform: PlatformAnthropic, Type: AccountTypeAPIKey} + body := []byte(`{"tools":[{"name":"deferred","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"ordinary","custom":{"defer_loading":false},"cache_control":{"type":"ephemeral"}},{"name":"malformed","custom":{"defer_loading":"true"},"cache_control":{"type":"ephemeral"}}]}`) + + _, wireBody, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(context.Background(), c, account, body, "k") + require.NoError(t, err) + require.False(t, gjson.GetBytes(wireBody, "tools.0.cache_control").Exists()) + require.True(t, gjson.GetBytes(wireBody, "tools.1.cache_control").Exists()) + require.True(t, gjson.GetBytes(wireBody, "tools.2.cache_control").Exists()) + + countReq, err := svc.buildCountTokensRequestAnthropicAPIKeyPassthrough(context.Background(), c, account, body, "k") + require.NoError(t, err) + countBody, err := io.ReadAll(countReq.Body) + require.NoError(t, err) + require.False(t, gjson.GetBytes(countBody, "tools.0.cache_control").Exists()) + require.True(t, gjson.GetBytes(countBody, "tools.1.cache_control").Exists()) + require.True(t, gjson.GetBytes(countBody, "tools.2.cache_control").Exists()) +} + func TestGatewayService_AnthropicOAuth_NotAffectedByAPIKeyPassthroughToggle(t *testing.T) { gin.SetMode(gin.TestMode) rec := httptest.NewRecorder() diff --git a/backend/internal/service/gateway_anthropic_passthrough.go b/backend/internal/service/gateway_anthropic_passthrough.go index c18d5bf047..568d560bb3 100644 --- a/backend/internal/service/gateway_anthropic_passthrough.go +++ b/backend/internal/service/gateway_anthropic_passthrough.go @@ -310,6 +310,7 @@ func (s *GatewayService) buildUpstreamRequestAnthropicAPIKeyPassthrough( body []byte, token string, ) (*http.Request, []byte, error) { + body = stripDeferredToolCacheControl(body) targetURL := claudeAPIURL baseURL := account.GetBaseURL() if baseURL != "" { diff --git a/backend/internal/service/gateway_context_management_test.go b/backend/internal/service/gateway_context_management_test.go index bdeb2c600b..b7ab3e9429 100644 --- a/backend/internal/service/gateway_context_management_test.go +++ b/backend/internal/service/gateway_context_management_test.go @@ -4,6 +4,7 @@ package service import ( "context" + "fmt" "io" "net/http" "net/http/httptest" @@ -650,6 +651,51 @@ func TestBuildCountTokensRequest_APIKeyHaiku_StripsContextManagementEndToEnd(t * "count_tokens API-key + 客户端未带 beta token → body strip") } +func TestBuildCountTokensRequest_StripsCacheControlOnlyFromLiteralDeferredTools(t *testing.T) { + gin.SetMode(gin.TestMode) + body := []byte(`{"model":"claude-haiku-4-5","messages":[],"tools":[{"name":"deferred","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"ordinary","custom":{"defer_loading":false},"cache_control":{"type":"ephemeral"}},{"name":"string","custom":{"defer_loading":"true"},"cache_control":{"type":"ephemeral"}},{"name":"number","custom":{"defer_loading":1},"cache_control":{"type":"ephemeral"}},{"name":"object","custom":{"defer_loading":{}},"cache_control":{"type":"ephemeral"}}]}`) + + tests := []struct { + name string + account *Account + token string + tokenType string + }{ + { + name: "generic API key", + account: &Account{Platform: PlatformAnthropic, Type: AccountTypeAPIKey}, + token: "sk-ant-test", + tokenType: "apikey", + }, + { + name: "recognized Claude Code OAuth without mimicry", + account: &Account{Platform: PlatformAnthropic, Type: AccountTypeOAuth}, + token: "oauth-token", + tokenType: "oauth", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rec := httptest.NewRecorder() + c, _ := gin.CreateTestContext(rec) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages/count_tokens", nil) + svc := &GatewayService{cfg: &config.Config{}} + + req, wireBody, err := svc.buildCountTokensRequest( + context.Background(), c, tt.account, body, + tt.token, tt.tokenType, "claude-haiku-4-5", false, + ) + require.NoError(t, err) + require.False(t, gjson.GetBytes(wireBody, "tools.0.cache_control").Exists()) + for idx := 1; idx < 5; idx++ { + require.Equal(t, "ephemeral", gjson.GetBytes(wireBody, fmt.Sprintf("tools.%d.cache_control.type", idx)).String()) + } + require.JSONEq(t, string(wireBody), string(readUpstreamBodyForTest(t, req))) + }) + } +} + // count_tokens passthrough preserve 测试 func TestBuildCountTokensRequestAnthropicAPIKeyPassthrough_PreservesContextManagementWhenClientHeaderHasBeta(t *testing.T) { gin.SetMode(gin.TestMode) diff --git a/backend/internal/service/gateway_count_tokens.go b/backend/internal/service/gateway_count_tokens.go index 7b66ac9596..003cbb7c1e 100644 --- a/backend/internal/service/gateway_count_tokens.go +++ b/backend/internal/service/gateway_count_tokens.go @@ -366,6 +366,7 @@ func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough( body []byte, token string, ) (*http.Request, error) { + body = stripDeferredToolCacheControl(body) targetURL := claudeAPICountTokensURL baseURL := account.GetBaseURL() if baseURL != "" { @@ -429,6 +430,7 @@ func (s *GatewayService) buildCountTokensRequestAnthropicAPIKeyPassthrough( // buildCountTokensRequest 构建 count_tokens 上游请求 func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Context, account *Account, body []byte, token, tokenType, modelID string, mimicClaudeCode bool) (*http.Request, []byte, error) { + body = stripDeferredToolCacheControl(body) // 确定目标 URL targetURL := claudeAPICountTokensURL if account.Type == AccountTypeAPIKey { diff --git a/backend/internal/service/gateway_tool_rewrite.go b/backend/internal/service/gateway_tool_rewrite.go index da62daf961..42d786fe3e 100644 --- a/backend/internal/service/gateway_tool_rewrite.go +++ b/backend/internal/service/gateway_tool_rewrite.go @@ -248,14 +248,17 @@ func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte { return body } -// applyToolsLastCacheBreakpoint 在 tools 数组最后一个工具上注入 cache_control -// 断点,对齐 Parrot `tools[-1]["cache_control"] = {"type":"ephemeral","ttl":"1h"}` -// 行为,但 ttl 按本仓规则: +// applyToolsLastCacheBreakpoint 在最后一个非延迟加载工具上注入 cache_control +// 断点。Anthropic 不允许 custom.defer_loading=true 的工具携带 cache_control, +// 因此会先清理所有延迟加载工具上的客户端断点。其余行为对齐 Parrot +// `tools[-1]["cache_control"] = {"type":"ephemeral","ttl":"1h"}`, +// 但 ttl 按本仓规则: // - 客户端已为该 tool 显式设置 cache_control.ttl → 完全透传不覆盖 // - 否则注入 {"type":"ephemeral","ttl": claude.DefaultCacheControlTTL} // // 纯副作用函数,tools 不存在或为空数组时 no-op。 func applyToolsLastCacheBreakpoint(body []byte) []byte { + body = stripDeferredToolCacheControl(body) tools := gjson.GetBytes(body, "tools") if !tools.IsArray() { return body @@ -264,7 +267,17 @@ func applyToolsLastCacheBreakpoint(body []byte) []byte { if len(arr) == 0 { return body } - lastIdx := len(arr) - 1 + lastIdx := -1 + for idx, tool := range arr { + if isDeferredLoadingTool(tool) { + continue + } + lastIdx = idx + } + if lastIdx == -1 { + return body + } + existingCC := arr[lastIdx].Get("cache_control") if existingCC.Exists() && existingCC.Get("ttl").String() != "" { @@ -285,6 +298,28 @@ func applyToolsLastCacheBreakpoint(body []byte) []byte { return body } +func isDeferredLoadingTool(tool gjson.Result) bool { + return tool.Get("custom.defer_loading").Type == gjson.True +} + +// stripDeferredToolCacheControl removes the cache marker Anthropic rejects on +// deferred tools. Only the literal JSON boolean true enables deferred loading. +func stripDeferredToolCacheControl(body []byte) []byte { + tools := gjson.GetBytes(body, "tools") + if !tools.IsArray() { + return body + } + for idx, tool := range tools.Array() { + if !isDeferredLoadingTool(tool) || !tool.Get("cache_control").Exists() { + continue + } + if next, err := sjson.DeleteBytes(body, fmt.Sprintf("tools.%d.cache_control", idx)); err == nil { + body = next + } + } + return body +} + // restoreToolNamesInBytes 对 bytes chunk 做逆向还原:假名 → 真名。 // 按 ReverseOrdered 的假名长度倒序逐个 bytes.Replace,防止子串冲突 // (与 Parrot _restore_tool_names_in_chunk 的 sorted(..., reverse=True) 等价)。 diff --git a/backend/internal/service/gateway_tool_rewrite_test.go b/backend/internal/service/gateway_tool_rewrite_test.go index 9e6f6806da..d35f0181a7 100644 --- a/backend/internal/service/gateway_tool_rewrite_test.go +++ b/backend/internal/service/gateway_tool_rewrite_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "fmt" "strings" "testing" @@ -149,6 +150,33 @@ func TestApplyToolsLastCacheBreakpoint_PassesThroughClientTTL(t *testing.T) { require.Equal(t, "1h", gjson.GetBytes(out, "tools.0.cache_control.ttl").String()) } +func TestApplyToolsLastCacheBreakpoint_StripsDeferredToolCacheControl(t *testing.T) { + body := []byte(`{"tools":[{"name":"a","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral","ttl":"1h"}},{"name":"b","custom":{"defer_loading":true}}]}`) + out := applyToolsLastCacheBreakpoint(body) + + require.False(t, gjson.GetBytes(out, "tools.0.cache_control").Exists()) + require.False(t, gjson.GetBytes(out, "tools.1.cache_control").Exists()) +} + +func TestApplyToolsLastCacheBreakpoint_SkipsDeferredFinalTool(t *testing.T) { + body := []byte(`{"tools":[{"name":"a","input_schema":{}},{"name":"b","custom":{"defer_loading":true}}]}`) + out := applyToolsLastCacheBreakpoint(body) + + require.Equal(t, "ephemeral", gjson.GetBytes(out, "tools.0.cache_control.type").String()) + require.Equal(t, "5m", gjson.GetBytes(out, "tools.0.cache_control.ttl").String()) + require.False(t, gjson.GetBytes(out, "tools.1.cache_control").Exists()) +} + +func TestApplyToolsLastCacheBreakpoint_OnlyLiteralTrueIsDeferred(t *testing.T) { + body := []byte(`{"tools":[{"name":"true","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"false","custom":{"defer_loading":false},"cache_control":{"type":"ephemeral"}},{"name":"string","custom":{"defer_loading":"true"},"cache_control":{"type":"ephemeral"}},{"name":"number","custom":{"defer_loading":1},"cache_control":{"type":"ephemeral"}},{"name":"object","custom":{"defer_loading":{}},"cache_control":{"type":"ephemeral"}}]}`) + out := stripDeferredToolCacheControl(body) + + require.False(t, gjson.GetBytes(out, "tools.0.cache_control").Exists()) + for idx := 1; idx < 5; idx++ { + require.Equal(t, "ephemeral", gjson.GetBytes(out, fmt.Sprintf("tools.%d.cache_control.type", idx)).String()) + } +} + func TestStripMessageCacheControl(t *testing.T) { body := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"hi","cache_control":{"type":"ephemeral"}}]}]}`) out := stripMessageCacheControl(body) diff --git a/backend/internal/service/gateway_upstream_request.go b/backend/internal/service/gateway_upstream_request.go index c5d962f20b..0a8efbdd5e 100644 --- a/backend/internal/service/gateway_upstream_request.go +++ b/backend/internal/service/gateway_upstream_request.go @@ -19,6 +19,7 @@ import ( ) func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Context, account *Account, body []byte, token, tokenType, modelID string, reqStream bool, mimicClaudeCode bool) (*http.Request, []byte, error) { + body = stripDeferredToolCacheControl(body) if account.Platform == PlatformAnthropic && account.Type == AccountTypeServiceAccount { req, err := s.buildUpstreamRequestAnthropicVertex(ctx, c, account, body, token, modelID, reqStream) return req, body, err From 0b35370a7ab9fd419574922411bf94531e4350ee Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 29 Jul 2026 16:41:36 +0800 Subject: [PATCH 2/9] fix(claude): support top-level deferred tools --- .../service/gateway_anthropic_apikey_passthrough_test.go | 8 +++++--- backend/internal/service/gateway_tool_rewrite.go | 8 +++++--- backend/internal/service/gateway_tool_rewrite_test.go | 7 ++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go index 102c2b2904..acd6c6136f 100644 --- a/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go +++ b/backend/internal/service/gateway_anthropic_apikey_passthrough_test.go @@ -776,21 +776,23 @@ func TestGatewayService_AnthropicAPIKeyPassthrough_StripsDeferredToolCacheContro c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil) svc := &GatewayService{cfg: &config.Config{Security: config.SecurityConfig{URLAllowlist: config.URLAllowlistConfig{Enabled: false}}}} account := &Account{Platform: PlatformAnthropic, Type: AccountTypeAPIKey} - body := []byte(`{"tools":[{"name":"deferred","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"ordinary","custom":{"defer_loading":false},"cache_control":{"type":"ephemeral"}},{"name":"malformed","custom":{"defer_loading":"true"},"cache_control":{"type":"ephemeral"}}]}`) + body := []byte(`{"tools":[{"name":"deferred","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"top-level-deferred","defer_loading":true,"cache_control":{"type":"ephemeral"}},{"name":"ordinary","defer_loading":false,"cache_control":{"type":"ephemeral"}},{"name":"malformed","defer_loading":"true","cache_control":{"type":"ephemeral"}}]}`) _, wireBody, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(context.Background(), c, account, body, "k") require.NoError(t, err) require.False(t, gjson.GetBytes(wireBody, "tools.0.cache_control").Exists()) - require.True(t, gjson.GetBytes(wireBody, "tools.1.cache_control").Exists()) + require.False(t, gjson.GetBytes(wireBody, "tools.1.cache_control").Exists()) require.True(t, gjson.GetBytes(wireBody, "tools.2.cache_control").Exists()) + require.True(t, gjson.GetBytes(wireBody, "tools.3.cache_control").Exists()) countReq, err := svc.buildCountTokensRequestAnthropicAPIKeyPassthrough(context.Background(), c, account, body, "k") require.NoError(t, err) countBody, err := io.ReadAll(countReq.Body) require.NoError(t, err) require.False(t, gjson.GetBytes(countBody, "tools.0.cache_control").Exists()) - require.True(t, gjson.GetBytes(countBody, "tools.1.cache_control").Exists()) + require.False(t, gjson.GetBytes(countBody, "tools.1.cache_control").Exists()) require.True(t, gjson.GetBytes(countBody, "tools.2.cache_control").Exists()) + require.True(t, gjson.GetBytes(countBody, "tools.3.cache_control").Exists()) } func TestGatewayService_AnthropicOAuth_NotAffectedByAPIKeyPassthroughToggle(t *testing.T) { diff --git a/backend/internal/service/gateway_tool_rewrite.go b/backend/internal/service/gateway_tool_rewrite.go index 42d786fe3e..d1b6993723 100644 --- a/backend/internal/service/gateway_tool_rewrite.go +++ b/backend/internal/service/gateway_tool_rewrite.go @@ -249,8 +249,9 @@ func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte { } // applyToolsLastCacheBreakpoint 在最后一个非延迟加载工具上注入 cache_control -// 断点。Anthropic 不允许 custom.defer_loading=true 的工具携带 cache_control, -// 因此会先清理所有延迟加载工具上的客户端断点。其余行为对齐 Parrot +// 断点。Anthropic 不允许 defer_loading=true 的工具携带 cache_control, +// 因此会先清理所有延迟加载工具上的客户端断点。兼容官方顶层字段和 +// Claude Code 使用的 custom.defer_loading 字段。其余行为对齐 Parrot // `tools[-1]["cache_control"] = {"type":"ephemeral","ttl":"1h"}`, // 但 ttl 按本仓规则: // - 客户端已为该 tool 显式设置 cache_control.ttl → 完全透传不覆盖 @@ -299,7 +300,8 @@ func applyToolsLastCacheBreakpoint(body []byte) []byte { } func isDeferredLoadingTool(tool gjson.Result) bool { - return tool.Get("custom.defer_loading").Type == gjson.True + return tool.Get("defer_loading").Type == gjson.True || + tool.Get("custom.defer_loading").Type == gjson.True } // stripDeferredToolCacheControl removes the cache marker Anthropic rejects on diff --git a/backend/internal/service/gateway_tool_rewrite_test.go b/backend/internal/service/gateway_tool_rewrite_test.go index d35f0181a7..c894ef708c 100644 --- a/backend/internal/service/gateway_tool_rewrite_test.go +++ b/backend/internal/service/gateway_tool_rewrite_test.go @@ -159,7 +159,7 @@ func TestApplyToolsLastCacheBreakpoint_StripsDeferredToolCacheControl(t *testing } func TestApplyToolsLastCacheBreakpoint_SkipsDeferredFinalTool(t *testing.T) { - body := []byte(`{"tools":[{"name":"a","input_schema":{}},{"name":"b","custom":{"defer_loading":true}}]}`) + body := []byte(`{"tools":[{"name":"a","input_schema":{}},{"name":"b","defer_loading":true}]}`) out := applyToolsLastCacheBreakpoint(body) require.Equal(t, "ephemeral", gjson.GetBytes(out, "tools.0.cache_control.type").String()) @@ -168,11 +168,12 @@ func TestApplyToolsLastCacheBreakpoint_SkipsDeferredFinalTool(t *testing.T) { } func TestApplyToolsLastCacheBreakpoint_OnlyLiteralTrueIsDeferred(t *testing.T) { - body := []byte(`{"tools":[{"name":"true","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"false","custom":{"defer_loading":false},"cache_control":{"type":"ephemeral"}},{"name":"string","custom":{"defer_loading":"true"},"cache_control":{"type":"ephemeral"}},{"name":"number","custom":{"defer_loading":1},"cache_control":{"type":"ephemeral"}},{"name":"object","custom":{"defer_loading":{}},"cache_control":{"type":"ephemeral"}}]}`) + body := []byte(`{"tools":[{"name":"custom-true","custom":{"defer_loading":true},"cache_control":{"type":"ephemeral"}},{"name":"top-level-true","defer_loading":true,"cache_control":{"type":"ephemeral"}},{"name":"false","defer_loading":false,"cache_control":{"type":"ephemeral"}},{"name":"string","defer_loading":"true","cache_control":{"type":"ephemeral"}},{"name":"number","defer_loading":1,"cache_control":{"type":"ephemeral"}},{"name":"object","defer_loading":{},"cache_control":{"type":"ephemeral"}}]}`) out := stripDeferredToolCacheControl(body) require.False(t, gjson.GetBytes(out, "tools.0.cache_control").Exists()) - for idx := 1; idx < 5; idx++ { + require.False(t, gjson.GetBytes(out, "tools.1.cache_control").Exists()) + for idx := 2; idx < 6; idx++ { require.Equal(t, "ephemeral", gjson.GetBytes(out, fmt.Sprintf("tools.%d.cache_control.type", idx)).String()) } } From 2321c0e771a7638127f9931cd964ad5d84ab5277 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 29 Jul 2026 16:53:44 +0800 Subject: [PATCH 3/9] chore: retry CI checks From 1afb8264e53c4952cdd1a860a3605351e5ac330a Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 29 Jul 2026 23:25:08 +0800 Subject: [PATCH 4/9] fix(lint): use require.NotNil for staticcheck SA5011 Replace if-then-t.Fatal nil checks with testify require.NotNil so staticcheck can prove the pointer is non-nil before field access. Fixes SA5011 in payment_handler_test, parse_test, and openai_images_incomplete_test. --- backend/internal/handler/admin/payment_handler_test.go | 5 ++--- backend/internal/pkg/proxyurl/parse_test.go | 6 +++--- backend/internal/service/openai_images_incomplete_test.go | 5 ++--- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/internal/handler/admin/payment_handler_test.go b/backend/internal/handler/admin/payment_handler_test.go index d0752ec09a..c72132382f 100644 --- a/backend/internal/handler/admin/payment_handler_test.go +++ b/backend/internal/handler/admin/payment_handler_test.go @@ -8,6 +8,7 @@ import ( dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/require" ) func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) { @@ -32,9 +33,7 @@ func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) { } got := sanitizeAdminPaymentOrderForResponse(order) - if got == nil { - t.Fatal("expected sanitized order") - } + require.NotNil(t, got) if got.Currency != "USD" { t.Fatalf("expected currency USD, got %q", got.Currency) } diff --git a/backend/internal/pkg/proxyurl/parse_test.go b/backend/internal/pkg/proxyurl/parse_test.go index 5fb57c16f7..05fde1284d 100644 --- a/backend/internal/pkg/proxyurl/parse_test.go +++ b/backend/internal/pkg/proxyurl/parse_test.go @@ -3,6 +3,8 @@ package proxyurl import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestParse_空字符串直连(t *testing.T) { @@ -39,9 +41,7 @@ func TestParse_有效HTTP代理(t *testing.T) { if trimmed != "http://proxy.example.com:8080" { t.Errorf("trimmed 不匹配: got %q", trimmed) } - if parsed == nil { - t.Fatal("parsed 不应为 nil") - } + require.NotNil(t, parsed) if parsed.Host != "proxy.example.com:8080" { t.Errorf("Host 不匹配: got %q", parsed.Host) } diff --git a/backend/internal/service/openai_images_incomplete_test.go b/backend/internal/service/openai_images_incomplete_test.go index 6a8ba56225..aa2f92b42d 100644 --- a/backend/internal/service/openai_images_incomplete_test.go +++ b/backend/internal/service/openai_images_incomplete_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" ) // response.incomplete(生成超时/截断)应被识别为可重试的 502 上游错误,触发 failover。 @@ -16,9 +17,7 @@ func TestExtractImagesUpstreamError_IncompleteIsRetryable(t *testing.T) { body := "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\"}}\n\n" + "data: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"resp_1\",\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"max_output_tokens\"}}}\n\n" got := extractOpenAIImagesUpstreamError([]byte(body)) - if got == nil { - t.Fatal("incomplete event should produce an upstream error, got nil") - } + require.NotNil(t, got) if got.StatusCode != http.StatusBadGateway { t.Fatalf("incomplete(max_output_tokens) should be 502 retryable, got %d", got.StatusCode) } From 5ea03c178df54d0e8d7346c72935df6633fb634e Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 30 Jul 2026 00:06:44 +0800 Subject: [PATCH 5/9] fix(lint): resolve remaining nil dereference warnings --- backend/internal/service/openai_images_incomplete_test.go | 4 +--- backend/internal/service/ops_service_user_error_test.go | 5 ++--- backend/internal/service/ops_user_error_test.go | 6 +++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/openai_images_incomplete_test.go b/backend/internal/service/openai_images_incomplete_test.go index aa2f92b42d..e707fbf758 100644 --- a/backend/internal/service/openai_images_incomplete_test.go +++ b/backend/internal/service/openai_images_incomplete_test.go @@ -36,9 +36,7 @@ func TestExtractImagesUpstreamError_IncompleteIsRetryable(t *testing.T) { func TestExtractImagesUpstreamError_IncompleteContentFilterNotRetryable(t *testing.T) { body := "data: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"r\",\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"content_filter\"}}}\n\n" got := extractOpenAIImagesUpstreamError([]byte(body)) - if got == nil { - t.Fatal("content_filter incomplete should produce error") - } + require.NotNil(t, got) if got.StatusCode != http.StatusBadRequest { t.Fatalf("content_filter should be 400 (non-retryable), got %d", got.StatusCode) } diff --git a/backend/internal/service/ops_service_user_error_test.go b/backend/internal/service/ops_service_user_error_test.go index b1f6152702..3fa88739ad 100644 --- a/backend/internal/service/ops_service_user_error_test.go +++ b/backend/internal/service/ops_service_user_error_test.go @@ -6,6 +6,7 @@ import ( "testing" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" + "github.com/stretchr/testify/require" ) type stubOpsRepoForUserErr struct { @@ -119,9 +120,7 @@ func TestGetUserErrorRequestDetail_OwnershipEnforced(t *testing.T) { if err2 != nil { t.Fatalf("expected no error for legitimate access, got %v", err2) } - if got2 == nil { - t.Fatal("expected non-nil detail for legitimate access") - } + require.NotNil(t, got2) if got2.ID != 42 { t.Errorf("want ID=42, got %d", got2.ID) } diff --git a/backend/internal/service/ops_user_error_test.go b/backend/internal/service/ops_user_error_test.go index c72cf5dbfb..e02842d3d2 100644 --- a/backend/internal/service/ops_user_error_test.go +++ b/backend/internal/service/ops_user_error_test.go @@ -5,6 +5,8 @@ import ( "strings" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestMapUserErrorCategory(t *testing.T) { @@ -132,9 +134,7 @@ func TestToUserErrorRequestDetail_WhitelistAndRedacts(t *testing.T) { } out := ToUserErrorRequestDetail(src) - if out == nil { - t.Fatal("expected non-nil detail") - } + require.NotNil(t, out) // 基础字段正确映射 if out.ID != 999 { From 145b0ac35e06b168cddfab754b074afb20d234d5 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 30 Jul 2026 00:28:14 +0800 Subject: [PATCH 6/9] fix(lint): make remaining pointer assertions explicit --- .../internal/service/payment_order_result_test.go | 13 ++++--------- .../service/ratelimit_service_anthropic_test.go | 10 ++++------ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/backend/internal/service/payment_order_result_test.go b/backend/internal/service/payment_order_result_test.go index e77fbce482..ce405e0a23 100644 --- a/backend/internal/service/payment_order_result_test.go +++ b/backend/internal/service/payment_order_result_test.go @@ -9,6 +9,7 @@ import ( dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/payment" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" + "github.com/stretchr/testify/require" ) func TestShouldUseAlipayMobilePrecreate(t *testing.T) { @@ -389,15 +390,11 @@ func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if resp == nil { - t.Fatal("expected oauth_required response, got nil") - } + require.NotNil(t, resp) if resp.ResultType != payment.CreatePaymentResultOAuthRequired { t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired) } - if resp.OAuth == nil { - t.Fatal("expected oauth payload, got nil") - } + require.NotNil(t, resp.OAuth) if resp.OAuth.AppID != "wx123456" { t.Fatalf("appid = %q, want %q", resp.OAuth.AppID, "wx123456") } @@ -503,9 +500,7 @@ func TestMaybeBuildWeChatOAuthRequiredResponseFallsBackToConfiguredLegacySigning if err != nil { t.Fatalf("expected nil error, got %v", err) } - if resp == nil { - t.Fatal("expected oauth-required response, got nil") - } + require.NotNil(t, resp) if resp.ResultType != payment.CreatePaymentResultOAuthRequired { t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired) } diff --git a/backend/internal/service/ratelimit_service_anthropic_test.go b/backend/internal/service/ratelimit_service_anthropic_test.go index 0e75b4f914..c02ad00e1f 100644 --- a/backend/internal/service/ratelimit_service_anthropic_test.go +++ b/backend/internal/service/ratelimit_service_anthropic_test.go @@ -5,6 +5,8 @@ import ( "strconv" "testing" "time" + + "github.com/stretchr/testify/require" ) func TestCalculateAnthropic429ResetTime_Only5hExceeded(t *testing.T) { @@ -193,9 +195,7 @@ func TestSelectAnthropicFableWindowLimit_RejectedStatus(t *testing.T) { headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - if limit == nil { - t.Fatal("expected non-nil limit") - } + require.NotNil(t, limit) if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) } @@ -214,9 +214,7 @@ func TestSelectAnthropicFableWindowLimit_UtilizationOnly(t *testing.T) { headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - if limit == nil { - t.Fatal("expected non-nil limit") - } + require.NotNil(t, limit) if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) } From e087f1b73699d31a1077b03e10dae0e8c3992a9c Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 30 Jul 2026 00:49:48 +0800 Subject: [PATCH 7/9] fix(lint): use fatal pointer assertions in usage tests --- .../internal/service/account_usage_service_test.go | 14 +++++--------- .../service/ratelimit_service_anthropic_test.go | 4 +--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/backend/internal/service/account_usage_service_test.go b/backend/internal/service/account_usage_service_test.go index ca153a3c42..6d7fa8fb5f 100644 --- a/backend/internal/service/account_usage_service_test.go +++ b/backend/internal/service/account_usage_service_test.go @@ -5,6 +5,8 @@ import ( "net/http" "testing" "time" + + "github.com/stretchr/testify/require" ) type accountUsageCodexProbeRepo struct { @@ -216,9 +218,7 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_5h_reset_at": "2026-03-16T10:00:00Z", // 2h ago } progress := buildCodexUsageProgressFromExtra(extra, "5h", now) - if progress == nil { - t.Fatal("expected non-nil progress") - } + require.NotNil(t, progress) if progress.Utilization != 0 { t.Fatalf("expected Utilization=0 for expired window, got %v", progress.Utilization) } @@ -234,9 +234,7 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_5h_reset_at": resetAt, } progress := buildCodexUsageProgressFromExtra(extra, "5h", now) - if progress == nil { - t.Fatal("expected non-nil progress") - } + require.NotNil(t, progress) if progress.Utilization != 42.0 { t.Fatalf("expected Utilization=42, got %v", progress.Utilization) } @@ -248,9 +246,7 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_7d_reset_at": "2026-03-15T00:00:00Z", // yesterday } progress := buildCodexUsageProgressFromExtra(extra, "7d", now) - if progress == nil { - t.Fatal("expected non-nil progress") - } + require.NotNil(t, progress) if progress.Utilization != 0 { t.Fatalf("expected Utilization=0 for expired 7d window, got %v", progress.Utilization) } diff --git a/backend/internal/service/ratelimit_service_anthropic_test.go b/backend/internal/service/ratelimit_service_anthropic_test.go index c02ad00e1f..0755a02565 100644 --- a/backend/internal/service/ratelimit_service_anthropic_test.go +++ b/backend/internal/service/ratelimit_service_anthropic_test.go @@ -248,9 +248,7 @@ func TestSelectAnthropicFableWindowLimit_FallsBackToAggregateReset(t *testing.T) headers.Set("anthropic-ratelimit-unified-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - if limit == nil { - t.Fatal("expected non-nil limit via aggregate reset fallback") - } + require.NotNil(t, limit) if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) } From c3fc4331c1b150998a9b2dd9dfc3fe5d4d4d64bf Mon Sep 17 00:00:00 2001 From: wucm667 Date: Thu, 30 Jul 2026 01:12:42 +0800 Subject: [PATCH 8/9] fix(lint): make compatibility test assertions fatal --- .../service/gemini_messages_compat_service_test.go | 4 +--- .../openai_gateway_service_codex_snapshot_test.go | 10 ++++------ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/internal/service/gemini_messages_compat_service_test.go b/backend/internal/service/gemini_messages_compat_service_test.go index 59f83a6835..acf9075c54 100644 --- a/backend/internal/service/gemini_messages_compat_service_test.go +++ b/backend/internal/service/gemini_messages_compat_service_test.go @@ -782,9 +782,7 @@ func TestExtractGeminiUsage(t *testing.T) { } return } - if got == nil { - t.Fatalf("期望返回非 nil,实际返回 nil") - } + require.NotNil(t, got) if got.InputTokens != tt.wantUsage.InputTokens { t.Errorf("InputTokens: 期望 %d,实际 %d", tt.wantUsage.InputTokens, got.InputTokens) } diff --git a/backend/internal/service/openai_gateway_service_codex_snapshot_test.go b/backend/internal/service/openai_gateway_service_codex_snapshot_test.go index 27208b58d9..59568b33c9 100644 --- a/backend/internal/service/openai_gateway_service_codex_snapshot_test.go +++ b/backend/internal/service/openai_gateway_service_codex_snapshot_test.go @@ -3,6 +3,8 @@ package service import ( "testing" "time" + + "github.com/stretchr/testify/require" ) func TestCodexSnapshotBaseTime(t *testing.T) { @@ -50,9 +52,7 @@ func TestCodexResetAtRFC3339(t *testing.T) { t.Run("positive seconds", func(t *testing.T) { sec := 90 got := codexResetAtRFC3339(base, &sec) - if got == nil { - t.Fatal("expected non-nil") - } + require.NotNil(t, got) if *got != "2026-02-16T10:01:30Z" { t.Fatalf("got %s, want %s", *got, "2026-02-16T10:01:30Z") } @@ -61,9 +61,7 @@ func TestCodexResetAtRFC3339(t *testing.T) { t.Run("negative seconds clamp to base", func(t *testing.T) { sec := -3 got := codexResetAtRFC3339(base, &sec) - if got == nil { - t.Fatal("expected non-nil") - } + require.NotNil(t, got) if *got != "2026-02-16T10:00:00Z" { t.Fatalf("got %s, want %s", *got, "2026-02-16T10:00:00Z") } From c91fbeb0c008b2323e98ca5b73cedf074c8543dd Mon Sep 17 00:00:00 2001 From: wucm667 Date: Tue, 11 Aug 2026 16:01:06 +0800 Subject: [PATCH 9/9] chore: remove unrelated test refactors --- .../internal/handler/admin/payment_handler_test.go | 5 +++-- backend/internal/pkg/proxyurl/parse_test.go | 6 +++--- .../internal/service/account_usage_service_test.go | 14 +++++++++----- .../service/gemini_messages_compat_service_test.go | 4 +++- .../openai_gateway_service_codex_snapshot_test.go | 10 ++++++---- .../service/openai_images_incomplete_test.go | 9 ++++++--- .../service/ops_service_user_error_test.go | 5 +++-- backend/internal/service/ops_user_error_test.go | 6 +++--- .../internal/service/payment_order_result_test.go | 13 +++++++++---- .../service/ratelimit_service_anthropic_test.go | 14 +++++++++----- 10 files changed, 54 insertions(+), 32 deletions(-) diff --git a/backend/internal/handler/admin/payment_handler_test.go b/backend/internal/handler/admin/payment_handler_test.go index c72132382f..d0752ec09a 100644 --- a/backend/internal/handler/admin/payment_handler_test.go +++ b/backend/internal/handler/admin/payment_handler_test.go @@ -8,7 +8,6 @@ import ( dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/service" - "github.com/stretchr/testify/require" ) func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) { @@ -33,7 +32,9 @@ func TestSanitizeAdminPaymentOrderForResponseAddsCurrency(t *testing.T) { } got := sanitizeAdminPaymentOrderForResponse(order) - require.NotNil(t, got) + if got == nil { + t.Fatal("expected sanitized order") + } if got.Currency != "USD" { t.Fatalf("expected currency USD, got %q", got.Currency) } diff --git a/backend/internal/pkg/proxyurl/parse_test.go b/backend/internal/pkg/proxyurl/parse_test.go index 05fde1284d..5fb57c16f7 100644 --- a/backend/internal/pkg/proxyurl/parse_test.go +++ b/backend/internal/pkg/proxyurl/parse_test.go @@ -3,8 +3,6 @@ package proxyurl import ( "strings" "testing" - - "github.com/stretchr/testify/require" ) func TestParse_空字符串直连(t *testing.T) { @@ -41,7 +39,9 @@ func TestParse_有效HTTP代理(t *testing.T) { if trimmed != "http://proxy.example.com:8080" { t.Errorf("trimmed 不匹配: got %q", trimmed) } - require.NotNil(t, parsed) + if parsed == nil { + t.Fatal("parsed 不应为 nil") + } if parsed.Host != "proxy.example.com:8080" { t.Errorf("Host 不匹配: got %q", parsed.Host) } diff --git a/backend/internal/service/account_usage_service_test.go b/backend/internal/service/account_usage_service_test.go index 6d7fa8fb5f..ca153a3c42 100644 --- a/backend/internal/service/account_usage_service_test.go +++ b/backend/internal/service/account_usage_service_test.go @@ -5,8 +5,6 @@ import ( "net/http" "testing" "time" - - "github.com/stretchr/testify/require" ) type accountUsageCodexProbeRepo struct { @@ -218,7 +216,9 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_5h_reset_at": "2026-03-16T10:00:00Z", // 2h ago } progress := buildCodexUsageProgressFromExtra(extra, "5h", now) - require.NotNil(t, progress) + if progress == nil { + t.Fatal("expected non-nil progress") + } if progress.Utilization != 0 { t.Fatalf("expected Utilization=0 for expired window, got %v", progress.Utilization) } @@ -234,7 +234,9 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_5h_reset_at": resetAt, } progress := buildCodexUsageProgressFromExtra(extra, "5h", now) - require.NotNil(t, progress) + if progress == nil { + t.Fatal("expected non-nil progress") + } if progress.Utilization != 42.0 { t.Fatalf("expected Utilization=42, got %v", progress.Utilization) } @@ -246,7 +248,9 @@ func TestBuildCodexUsageProgressFromExtra_ZerosExpiredWindow(t *testing.T) { "codex_7d_reset_at": "2026-03-15T00:00:00Z", // yesterday } progress := buildCodexUsageProgressFromExtra(extra, "7d", now) - require.NotNil(t, progress) + if progress == nil { + t.Fatal("expected non-nil progress") + } if progress.Utilization != 0 { t.Fatalf("expected Utilization=0 for expired 7d window, got %v", progress.Utilization) } diff --git a/backend/internal/service/gemini_messages_compat_service_test.go b/backend/internal/service/gemini_messages_compat_service_test.go index acf9075c54..59f83a6835 100644 --- a/backend/internal/service/gemini_messages_compat_service_test.go +++ b/backend/internal/service/gemini_messages_compat_service_test.go @@ -782,7 +782,9 @@ func TestExtractGeminiUsage(t *testing.T) { } return } - require.NotNil(t, got) + if got == nil { + t.Fatalf("期望返回非 nil,实际返回 nil") + } if got.InputTokens != tt.wantUsage.InputTokens { t.Errorf("InputTokens: 期望 %d,实际 %d", tt.wantUsage.InputTokens, got.InputTokens) } diff --git a/backend/internal/service/openai_gateway_service_codex_snapshot_test.go b/backend/internal/service/openai_gateway_service_codex_snapshot_test.go index 59568b33c9..27208b58d9 100644 --- a/backend/internal/service/openai_gateway_service_codex_snapshot_test.go +++ b/backend/internal/service/openai_gateway_service_codex_snapshot_test.go @@ -3,8 +3,6 @@ package service import ( "testing" "time" - - "github.com/stretchr/testify/require" ) func TestCodexSnapshotBaseTime(t *testing.T) { @@ -52,7 +50,9 @@ func TestCodexResetAtRFC3339(t *testing.T) { t.Run("positive seconds", func(t *testing.T) { sec := 90 got := codexResetAtRFC3339(base, &sec) - require.NotNil(t, got) + if got == nil { + t.Fatal("expected non-nil") + } if *got != "2026-02-16T10:01:30Z" { t.Fatalf("got %s, want %s", *got, "2026-02-16T10:01:30Z") } @@ -61,7 +61,9 @@ func TestCodexResetAtRFC3339(t *testing.T) { t.Run("negative seconds clamp to base", func(t *testing.T) { sec := -3 got := codexResetAtRFC3339(base, &sec) - require.NotNil(t, got) + if got == nil { + t.Fatal("expected non-nil") + } if *got != "2026-02-16T10:00:00Z" { t.Fatalf("got %s, want %s", *got, "2026-02-16T10:00:00Z") } diff --git a/backend/internal/service/openai_images_incomplete_test.go b/backend/internal/service/openai_images_incomplete_test.go index e707fbf758..6a8ba56225 100644 --- a/backend/internal/service/openai_images_incomplete_test.go +++ b/backend/internal/service/openai_images_incomplete_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/gin-gonic/gin" - "github.com/stretchr/testify/require" ) // response.incomplete(生成超时/截断)应被识别为可重试的 502 上游错误,触发 failover。 @@ -17,7 +16,9 @@ func TestExtractImagesUpstreamError_IncompleteIsRetryable(t *testing.T) { body := "data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\"}}\n\n" + "data: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"resp_1\",\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"max_output_tokens\"}}}\n\n" got := extractOpenAIImagesUpstreamError([]byte(body)) - require.NotNil(t, got) + if got == nil { + t.Fatal("incomplete event should produce an upstream error, got nil") + } if got.StatusCode != http.StatusBadGateway { t.Fatalf("incomplete(max_output_tokens) should be 502 retryable, got %d", got.StatusCode) } @@ -36,7 +37,9 @@ func TestExtractImagesUpstreamError_IncompleteIsRetryable(t *testing.T) { func TestExtractImagesUpstreamError_IncompleteContentFilterNotRetryable(t *testing.T) { body := "data: {\"type\":\"response.incomplete\",\"response\":{\"id\":\"r\",\"status\":\"incomplete\",\"incomplete_details\":{\"reason\":\"content_filter\"}}}\n\n" got := extractOpenAIImagesUpstreamError([]byte(body)) - require.NotNil(t, got) + if got == nil { + t.Fatal("content_filter incomplete should produce error") + } if got.StatusCode != http.StatusBadRequest { t.Fatalf("content_filter should be 400 (non-retryable), got %d", got.StatusCode) } diff --git a/backend/internal/service/ops_service_user_error_test.go b/backend/internal/service/ops_service_user_error_test.go index 3fa88739ad..b1f6152702 100644 --- a/backend/internal/service/ops_service_user_error_test.go +++ b/backend/internal/service/ops_service_user_error_test.go @@ -6,7 +6,6 @@ import ( "testing" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" - "github.com/stretchr/testify/require" ) type stubOpsRepoForUserErr struct { @@ -120,7 +119,9 @@ func TestGetUserErrorRequestDetail_OwnershipEnforced(t *testing.T) { if err2 != nil { t.Fatalf("expected no error for legitimate access, got %v", err2) } - require.NotNil(t, got2) + if got2 == nil { + t.Fatal("expected non-nil detail for legitimate access") + } if got2.ID != 42 { t.Errorf("want ID=42, got %d", got2.ID) } diff --git a/backend/internal/service/ops_user_error_test.go b/backend/internal/service/ops_user_error_test.go index e02842d3d2..c72cf5dbfb 100644 --- a/backend/internal/service/ops_user_error_test.go +++ b/backend/internal/service/ops_user_error_test.go @@ -5,8 +5,6 @@ import ( "strings" "testing" "time" - - "github.com/stretchr/testify/require" ) func TestMapUserErrorCategory(t *testing.T) { @@ -134,7 +132,9 @@ func TestToUserErrorRequestDetail_WhitelistAndRedacts(t *testing.T) { } out := ToUserErrorRequestDetail(src) - require.NotNil(t, out) + if out == nil { + t.Fatal("expected non-nil detail") + } // 基础字段正确映射 if out.ID != 999 { diff --git a/backend/internal/service/payment_order_result_test.go b/backend/internal/service/payment_order_result_test.go index ce405e0a23..e77fbce482 100644 --- a/backend/internal/service/payment_order_result_test.go +++ b/backend/internal/service/payment_order_result_test.go @@ -9,7 +9,6 @@ import ( dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/internal/payment" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" - "github.com/stretchr/testify/require" ) func TestShouldUseAlipayMobilePrecreate(t *testing.T) { @@ -390,11 +389,15 @@ func TestMaybeBuildWeChatOAuthRequiredResponse(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - require.NotNil(t, resp) + if resp == nil { + t.Fatal("expected oauth_required response, got nil") + } if resp.ResultType != payment.CreatePaymentResultOAuthRequired { t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired) } - require.NotNil(t, resp.OAuth) + if resp.OAuth == nil { + t.Fatal("expected oauth payload, got nil") + } if resp.OAuth.AppID != "wx123456" { t.Fatalf("appid = %q, want %q", resp.OAuth.AppID, "wx123456") } @@ -500,7 +503,9 @@ func TestMaybeBuildWeChatOAuthRequiredResponseFallsBackToConfiguredLegacySigning if err != nil { t.Fatalf("expected nil error, got %v", err) } - require.NotNil(t, resp) + if resp == nil { + t.Fatal("expected oauth-required response, got nil") + } if resp.ResultType != payment.CreatePaymentResultOAuthRequired { t.Fatalf("result type = %q, want %q", resp.ResultType, payment.CreatePaymentResultOAuthRequired) } diff --git a/backend/internal/service/ratelimit_service_anthropic_test.go b/backend/internal/service/ratelimit_service_anthropic_test.go index 0755a02565..0e75b4f914 100644 --- a/backend/internal/service/ratelimit_service_anthropic_test.go +++ b/backend/internal/service/ratelimit_service_anthropic_test.go @@ -5,8 +5,6 @@ import ( "strconv" "testing" "time" - - "github.com/stretchr/testify/require" ) func TestCalculateAnthropic429ResetTime_Only5hExceeded(t *testing.T) { @@ -195,7 +193,9 @@ func TestSelectAnthropicFableWindowLimit_RejectedStatus(t *testing.T) { headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - require.NotNil(t, limit) + if limit == nil { + t.Fatal("expected non-nil limit") + } if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) } @@ -214,7 +214,9 @@ func TestSelectAnthropicFableWindowLimit_UtilizationOnly(t *testing.T) { headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - require.NotNil(t, limit) + if limit == nil { + t.Fatal("expected non-nil limit") + } if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) } @@ -248,7 +250,9 @@ func TestSelectAnthropicFableWindowLimit_FallsBackToAggregateReset(t *testing.T) headers.Set("anthropic-ratelimit-unified-reset", strconv.FormatInt(reset.Unix(), 10)) limit := selectAnthropicFableWindowLimit(headers, now) - require.NotNil(t, limit) + if limit == nil { + t.Fatal("expected non-nil limit via aggregate reset fallback") + } if !limit.resetAt.Equal(reset) { t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt) }