#!/usr/bin/env python3 """Write test implementation plan files for Phase 1-5.""" import os BASE = r"D:\code\new-api\docs\superpowers\plans" def write_file(name, content): path = os.path.join(BASE, name) with open(path, 'w', encoding='utf-8') as f: f.write(content) lines = content.count('\n') print(f" Written: {name} ({lines} lines)") def main(): # Phase 1 write_file("phase1-model-tests.md", PHASE1) # Phase 2 write_file("phase2-service-billing.md", PHASE2) # Phase 3 write_file("phase3-middleware.md", PHASE3) # Phase 4 write_file("phase4-openai-adaptor.md", PHASE4) # Phase 5 write_file("phase5-e2e.md", PHASE5) print("All plan files written.") # ─── Phase 1 ─────────────────────────────────────────────────────────────── PHASE1 = r"""# Phase 1: Model 层核心 CRUD 测试 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** 为 model/ 目录下的 user, token, channel, ability, utils 添加约 73 个集成测试 **Architecture:** SQLite 内存 DB + 真实业务逻辑,testutil.SetupTestDB 初始化,表驱动测试优先。 **Tech Stack:** Go, github.com/glebarez/sqlite, github.com/stretchr/testify, gorm.io/gorm **Pre-requisites:** Phase 0 (testutil) 已完成 --- ## 通用模式 所有 model 测试共享 import 和 DB setup: ```go import ( "fmt" "sync" "testing" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupModelTestDB(t *testing.T) *gorm.DB { t.Helper() return testutil.SetupTestDB(t, &model.User{}, &model.Token{}, &model.Channel{}, &model.Ability{}, &model.Log{}, &model.Model{}, ) } ``` --- ## Task 1: model/user_test.go (22 tests) - [ ] Write TestUserInsert: normal registration, verify hashed password, defaults, aff code - [ ] Write TestUserInsert_DuplicateUsername: unique constraint error - [ ] Write TestUserValidateAndFill: table-driven (correct/wrong/not-found/disabled) - [ ] Write TestIncreaseUserQuota / TestDecreaseUserQuota: atomic quota operations - [ ] Write TestDecreaseUserQuota_Insufficient: behavior when quota goes negative - [ ] Write TestAtomicDecreaseSyncedQuota: table-driven (sufficient/exact/insufficient/threshold/zero) - [ ] Write TestUserInsert_WithInviter: verify inviter AffCount increment - [ ] Write TestUserInsert_WithInviter_Concurrent: 10 goroutines, verify known race condition - [ ] Write TestTransferAffQuotaToQuota / _Insufficient / _Concurrent: FOR UPDATE lock verification - [ ] Write P2 tests: Update, Edit, Delete, HardDelete, GetAllUsers, SearchUsers - [ ] Run: `go test -v -run "TestUser" ./model/ -count=1` - [ ] Commit: `git commit -m "test: add user model integration tests (22 tests)"` ### Key Test Code ```go func TestUserInsert(t *testing.T) { db := setupModelTestDB(t) _ = db user := &model.User{Username: "testuser", Password: "password123"} err := user.Insert(0) require.NoError(t, err) assert.Greater(t, user.Id, 0) assert.NotEqual(t, "password123", user.Password) assert.Equal(t, common.RoleCommonUser, user.Role) assert.NotEmpty(t, user.AffCode) } ``` ```go func TestAtomicDecreaseSyncedQuota(t *testing.T) { db := setupModelTestDB(t) _ = db tests := []struct{ name string; syncedQuota, amount, threshold int; wantOk bool }{ {"sufficient", 1000, 300, 0, true}, {"exactly_zero", 300, 300, 0, true}, {"below_threshold", 500, 300, 300, false}, {"insufficient", 100, 200, 0, false}, {"zero_quota", 0, 100, 0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { user := testutil.SeedUser(t, db, func(u *model.User) { u.Source = "synced"; u.SyncedQuota = tt.syncedQuota }) model.DB.Model(user).Update("synced_quota", tt.syncedQuota) balance, ok, err := model.AtomicDecreaseSyncedQuota(user.Id, tt.amount, tt.threshold) require.NoError(t, err) assert.Equal(t, tt.wantOk, ok) if tt.wantOk { assert.Equal(t, tt.syncedQuota-tt.amount, balance) } }) } } ``` --- ## Task 2: model/token_test.go (16 tests) - [ ] Write TestValidateUserToken: table-driven (enabled/exhausted/expired/disabled) - [ ] Write TestValidateUserToken_NotFound - [ ] Write TestIncreaseTokenQuota / TestDecreaseTokenQuota - [ ] Write TestTokenInsert / TestTokenUpdate / TestTokenDelete - [ ] Write TestBatchDeleteTokens - [ ] Write P2: GetAllUserTokens_Pagination, CountUserTokens, TokenSelectUpdate - [ ] Run: `go test -v -run "TestToken|TestValidate|TestIncrease|TestDecrease|TestBatch" ./model/ -count=1` - [ ] Commit: `git commit -m "test: add token model integration tests (16 tests)"` ### Key Test Code ```go func TestValidateUserToken(t *testing.T) { db := setupModelTestDB(t) _ = db tests := []struct { name string; setupToken func() *model.Token; wantErr bool; errContains string }{ {"enabled", func() *model.Token { return testutil.SeedToken(t, db, 0, func(tok *model.Token) { tok.Status = common.TokenStatusEnabled; tok.RemainQuota = 1000; tok.ExpiredTime = -1 }) }, false, ""}, {"exhausted", func() *model.Token { return testutil.SeedToken(t, db, 0, func(tok *model.Token) { tok.Status = common.TokenStatusExhausted }) }, true, "耗尽"}, {"expired", func() *model.Token { return testutil.SeedToken(t, db, 0, func(tok *model.Token) { tok.Status = common.TokenStatusEnabled; tok.ExpiredTime = time.Now().Unix() - 3600 }) }, true, "过期"}, {"disabled", func() *model.Token { return testutil.SeedToken(t, db, 0, func(tok *model.Token) { tok.Status = common.TokenStatusDisabled }) }, true, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { token := tt.setupToken() found, err := model.ValidateUserToken(token.Key) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err); assert.Equal(t, token.Id, found.Id) } }) } } ``` --- ## Task 3: model/channel_test.go (17 tests) - [ ] Write TestChannelInsert: verify Ability auto-generation - [ ] Write TestChannelDelete: verify Ability cleanup - [ ] Write TestUpdateChannelStatus_SingleKey: enable/disable cycle - [ ] Write TestUpdateChannelStatus_MultiKey_AllDisabled: auto AutoDisabled - [ ] Write TestUpdateChannelStatus_MultiKey_PartialDisabled - [ ] Write TestGetNextEnabledKey_Random / _Polling - [ ] Write P1: ChannelUpdate_AbilityRebuild, BatchInsertChannels, BatchDeleteChannels, EnableChannelByTag, EditChannelByTag - [ ] Write P2: ChannelSave, GetAllChannels, SearchChannels, DisableChannelByTag, DeleteChannelByStatus - [ ] Run: `go test -v -run "TestChannel|TestUpdateChannel|TestGetNext|TestBatch" ./model/ -count=1` - [ ] Commit: `git commit -m "test: add channel model integration tests (17 tests)"` ### Key Test Code ```go func TestChannelInsert(t *testing.T) { db := setupModelTestDB(t) _ = db channel := testutil.SeedChannel(t, db) assert.Greater(t, channel.Id, 0) abilities, err := model.GetAbilitiesByChannelId(channel.Id) require.NoError(t, err) assert.GreaterOrEqual(t, len(abilities), 2) // gpt-4 + gpt-3.5-turbo } func TestUpdateChannelStatus_MultiKey_AllDisabled(t *testing.T) { db := setupModelTestDB(t) _ = db keys := []string{"key1", "key2", "key3"} channel := testutil.SeedChannel(t, db, func(ch *model.Channel) { ch.Keys = keys ch.ChannelInfo = model.ChannelInfo{ IsMultiKey: true, MultiKeySize: 3, MultiKeyStatusList: map[int]int{0: 1, 1: 1, 2: 1}, } }) for i, key := range keys { model.UpdateChannelStatus(channel.Id, key, common.ChannelStatusManuallyDisabled, fmt.Sprintf("key %d", i)) } found, _ := model.GetChannelById(channel.Id, true) assert.Equal(t, common.ChannelStatusAutoDisabled, found.Status) } ``` --- ## Task 4: model/ability_test.go (9 tests) - [ ] Write TestGetChannel_WeightedRandom: 100 iterations, higher weight selected more - [ ] Write TestGetChannel_PriorityFallback: retry=0 high priority, retry=1 fallback - [ ] Write TestAddAbilities_ModelSync: verify Model table auto-sync - [ ] Write P1: UpdateAbilities, FixAbility, UpdateAbilityStatus - [ ] Write P2: GetGroupEnabledModels, GetAllEnableAbilities, GetAbilitiesByChannelId - [ ] Run + Commit --- ## Task 5: model/utils_test.go (6 tests) - [ ] Write TestAddNewRecord_Concurrent: 100 goroutines no panic - [ ] Write TestBatchUpdate_Flush: addNewRecord + batchUpdate + verify DB - [ ] Write TestBatchUpdate_MultipleTypes: user+token+used quota types - [ ] Write TestBatchUpdate_EmptyMap: no data no SQL - [ ] Write TestShouldUpdateRedis / TestRecordExist - [ ] Run + Commit --- ## Task 6: Final Verification - [ ] `go test -v ./model/ -count=1` — all pass - [ ] `go test -race ./model/ -count=1` — no race conditions """ # ─── Phase 2 ─────────────────────────────────────────────────────────────── PHASE2 = r"""# Phase 2: Service 层计费系统测试 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** 为计费系统(funding_source, billing_session, billing, quota)添加约 55 个测试 **Architecture:** SQLite 内存 DB + 真实业务逻辑。需要构造 RelayInfo 和 gin.Context。BillingSession 使用真实 FundingSource 实现。 **Tech Stack:** Go, github.com/glebarez/sqlite, github.com/stretchr/testify, gorm.io/gorm, github.com/gin-gonic/gin **Pre-requisites:** Phase 0 (testutil) + Phase 1 (model tests) 已完成 --- ## 通用 setup ```go import ( "testing" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/testutil" relaycommon "github.com/QuantumNous/new-api/relay/common" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func setupBillingTestDB(t *testing.T) *gorm.DB { t.Helper() return testutil.SetupTestDB(t, &model.User{}, &model.Token{}, &model.Channel{}, &model.Ability{}, &model.Log{}, &model.Model{}, &model.UserSubscription{}, &model.SubscriptionPlan{}, ) } ``` --- ## Task 1: service/funding_source_test.go (15 tests) - [ ] Write WalletFunding tests (8): Source, PreConsume, PreConsume_Zero, Settle_Positive, Settle_Negative, Settle_Zero, Refund, Refund_NotConsumed - [ ] Write SubscriptionFunding tests (7): Source, PreConsume, PreConsume_IgnoresAmount, Settle_Positive, Settle_Negative, Refund, Refund_Retry - [ ] Run: `go test -v -run "TestWalletFunding|TestSubscriptionFunding" ./service/ -count=1` - [ ] Commit: `git commit -m "test: add funding source tests (15 tests)"` ### Key Test Code ```go func TestWalletFunding_PreConsume(t *testing.T) { db := setupBillingTestDB(t) _ = db user := testutil.SeedUser(t, db, func(u *model.User) { u.Quota = 10000 }) w := service.NewWalletFunding(user.Id) // 或构造函数 err := w.PreConsume(1000) require.NoError(t, err) testutil.AssertQuotaEquals(t, db, user.Id, 9000) } func TestWalletFunding_Refund(t *testing.T) { db := setupBillingTestDB(t) _ = db user := testutil.SeedUser(t, db, func(u *model.User) { u.Quota = 10000 }) w := service.NewWalletFunding(user.Id) _ = w.PreConsume(1000) err := w.Refund() require.NoError(t, err) testutil.AssertQuotaEquals(t, db, user.Id, 10000) // 全额退回 } func TestSubscriptionFunding_PreConsume_IgnoresAmount(t *testing.T) { db := setupBillingTestDB(t) _ = db user := testutil.SeedUser(t, db) sub := testutil.SeedSubscription(t, db, user.Id) s := service.NewSubscriptionFunding("req-123", user.Id, "gpt-4", 5000, sub.Id) // 传入 9999 但应该被忽略 err := s.PreConsume(9999) require.NoError(t, err) assert.Equal(t, "subscription", s.Source()) } ``` Note: `NewWalletFunding` / `NewSubscriptionFunding` 构造函数签名需根据实际代码调整。如果这些类型不是导出的,测试需要在 service 包内。 --- ## Task 2: service/billing_session_test.go (25 tests) - [ ] Write PreConsume tests (12): 4 preference paths, trust bypass, force pre-consume, subscription no bypass, funding fail rollback - [ ] Write Settle tests (6): zero/positive/negative delta, idempotent, wallet/subscription full flow - [ ] Write Refund tests (4): full refund, idempotent, after settle skipped, zero consumed skipped - [ ] Write Lifecycle tests (3): normal PreConsume->Settle, failure PreConsume->Refund, concurrent - [ ] Run: `go test -v -run "TestNewBillingSession|TestSettle|TestRefund|TestBillingSession_Lifecycle" ./service/ -count=1` - [ ] Commit: `git commit -m "test: add billing session tests (25 tests)"` ### Key Test Code ```go func TestNewBillingSession_WalletOnly(t *testing.T) { db := setupBillingTestDB(t) _ = db user := testutil.SeedUser(t, db, func(u *model.User) { u.Quota = 100000 }) token := testutil.SeedToken(t, db, user.Id, func(tok *model.Token) { tok.UnlimitedQuota = true }) c, _ := testutil.NewTestGinContext("POST", "/v1/chat/completions", nil, nil) relayInfo := &relaycommon.RelayInfo{ UserId: user.Id, TokenId: token.Id, TokenKey: token.Key, UserGroup: "default", UsingGroup: "default", } session, apiErr := service.NewBillingSession(c, relayInfo, 1000) require.Nil(t, apiErr) assert.NotNil(t, session) assert.Equal(t, 1000, session.GetPreConsumedQuota()) } ``` Note: `BillingSession` 的构造需要设置用户计费偏好 (`billing_preference`)。可能需要 mock 或设置 `common.NormalizeBillingPreference` 返回值。具体实现取决于代码中偏好设置的读取方式。 --- ## Task 3: service/billing_test.go (5 tests) - [ ] Write SettleBilling_WithBillingSession / _WithoutBillingSession - [ ] Write PreConsumeBilling_AssignsRelayInfo - [ ] Write SettleBilling_NotifiesQuota / _ZeroActualQuota - [ ] Run + Commit --- ## Task 4: service/quota_test.go (5 tests) - [ ] Write PostConsumeQuota_Wallet / _Subscription / _NegativeQuota - [ ] Write PreConsumeTokenQuota_Success / _Insufficient - [ ] Run + Commit --- ## Task 5: Final Verification - [ ] `go test -v ./service/ -count=1` — all pass """ # ─── Phase 3 ─────────────────────────────────────────────────────────────── PHASE3 = r"""# Phase 3: Middleware 层测试 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** 为 middleware/ 目录下的 auth, rate-limit, distributor 添加约 55 个测试 **Architecture:** gin.TestMode + httptest.NewRecorder 构造 Context。需要 mock model 层全局函数(通过 DB 种子数据或设置全局变量)。 **Tech Stack:** Go, github.com/gin-gonic/gin, net/http/httptest, github.com/stretchr/testify **Pre-requisites:** Phase 0-1 已完成 --- ## Task 1: middleware/auth_test.go (20 tests) - [ ] Write TokenAuth tests (11): BearerToken, MissingAuth, InvalidKey, DisabledToken, ExpiredToken, ExhaustedToken, WithChannelId, SetsContextCorrectly, DisabledUser, GroupNotInRatio, IPWhitelist - [ ] Write MultiProtocol tests (4): AnthropicXApiKey, GeminiQueryParam, GeminiHeader, WebSocketProtocol - [ ] Write authHelper tests (5): UserAuth_Valid, AdminAuth_NonAdmin, RootAuth_NonRoot, TokenOrUserAuth_Fallback - [ ] Run: `go test -v -run "TestTokenAuth|TestUserAuth|TestAdminAuth|TestRootAuth" ./middleware/ -count=1` - [ ] Commit ### Key Pattern ```go func setupAuthTest(t *testing.T, token *model.Token, user *model.User) (*gin.Context, *httptest.ResponseRecorder) { t.Helper() gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("POST", "/v1/chat/completions", nil) if token != nil { c.Request.Header.Set("Authorization", "Bearer "+token.Key) } return c, w } ``` Note: TokenAuth 内部直接调用 `model.ValidateUserToken` 等全局函数。测试需要设置真实的 DB 数据(通过 testutil 种子数据),让这些函数正常工作。 --- ## Task 2: middleware/rate_limit_test.go (15 tests) - [ ] Write GlobalAPIRateLimit tests (3): within/over/window expires - [ ] Write CriticalRateLimit, MemoryMode, ConcurrentRequests - [ ] Write ModelRateLimit tests (4): within/over/bygroup/success only - [ ] Write other rate limit tests (5): email, search, upload, download, web - [ ] Run + Commit ### Key Pattern ```go func TestRateLimit_MemoryMode(t *testing.T) { common.RedisEnabled = false defer func() { common.RedisEnabled = true }() // 设置限流参数 common.GlobalApiRateLimitNum = 3 common.GlobalApiRateLimitDuration = 60 defer func() { common.GlobalApiRateLimitNum = 180 common.GlobalApiRateLimitDuration = 60 }() handler := middleware.GlobalAPIRateLimit() for i := 0; i < 3; i++ { c, w := testutil.NewTestGinContext("GET", "/api/test", nil, nil) handler(c) assert.Equal(t, 200, w.Code) // 或者 c.IsAborted() == false } // 第4次应被限流 c, w := testutil.NewTestGinContext("GET", "/api/test", nil, nil) handler(c) assert.True(t, c.IsAborted()) } ``` --- ## Task 3: middleware/distributor_test.go (20 tests) - [ ] Write getModelRequest tests (9): ChatCompletions, Embeddings, Images, AudioSpeech, AudioTranscription, Rerank, Responses, GeminiNative, Realtime - [ ] Write Distribute tests (8): SpecificChannel, Disabled, ModelNotSupported, RandomSelection, AffinityReuse, NoAvailableChannel, TokenModelLimit, Allowed - [ ] Write helper tests (3): CORS, RequestId, Recover - [ ] Run + Commit ### Key Pattern ```go func TestGetModelRequest_ChatCompletions(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) body := strings.NewReader(`{"model":"gpt-4","messages":[{"role":"user","content":"hi"}]}`) c.Request = httptest.NewRequest("POST", "/v1/chat/completions", body) c.Request.Header.Set("Content-Type", "application/json") req, shouldSelect, err := getModelRequest(c) require.NoError(t, err) assert.True(t, shouldSelect) assert.Equal(t, "gpt-4", req.Model) } ``` --- ## Task 4: Final Verification - [ ] `go test -v ./middleware/ -count=1` """ # ─── Phase 4 ─────────────────────────────────────────────────────────────── PHASE4 = r"""# Phase 4: OpenAI 适配器测试 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** 为 relay/channel/openai/ 添加约 55 个测试 **Architecture:** 纯逻辑函数直接测试。HTTP handler 使用 httptest.NewServer mock 上游。不需要数据库。 **Tech Stack:** Go, github.com/gin-gonic/gin, net/http/httptest, github.com/stretchr/testify **Pre-requisites:** Phase 0 已完成(使用 testutil.NewTestGinContext) --- ## Task 1: relay/channel/openai/adaptor_test.go (15 tests) - [ ] Write parseReasoningEffortFromModelSuffix tests (table-driven) - [ ] Write detectImageMimeType tests (table-driven) - [ ] Write Init, GetRequestURL (default/azure/custom), SetupRequestHeader (bearer/azure/org) - [ ] Write ConvertOpenAIRequest tests (6): MaxCompletionTokens, TemperatureCleared, SystemToDeveloper, ReasoningEffort, NormalModel, OpenRouter - [ ] Run + Commit ### Key Test Code ```go func TestParseReasoningEffortFromModelSuffix(t *testing.T) { tests := []struct{ model, wantEffort, wantOrigin string }{ {"o3-mini:low", "low", "o3-mini"}, {"o3-mini:high", "high", "o3-mini"}, {"o3-mini:medium", "medium", "o3-mini"}, {"o3-mini", "", "o3-mini"}, {"gpt-4", "", "gpt-4"}, } for _, tt := range tests { t.Run(tt.model, func(t *testing.T) { effort, origin := parseReasoningEffortFromModelSuffix(tt.model) assert.Equal(t, tt.wantEffort, effort) assert.Equal(t, tt.wantOrigin, origin) }) } } func TestDetectImageMimeType(t *testing.T) { tests := []struct{ filename, want string }{ {"photo.png", "image/png"}, {"photo.jpg", "image/jpeg"}, {"photo.jpeg", "image/jpeg"}, {"photo.webp", "image/webp"}, {"photo.gif", "image/png"}, // fallback } for _, tt := range tests { t.Run(tt.filename, func(t *testing.T) { assert.Equal(t, tt.want, detectImageMimeType(tt.filename)) }) } } ``` --- ## Task 2: relay/channel/openai/relay_openai_test.go (20 tests) - [ ] Write OpenaiHandler tests (5): Success, WithCacheTokens, ErrorResponse, NoPromptTokens, ContentFilter - [ ] Write OaiStreamHandler tests (4): BasicStream, UsageExtraction, ThinkingContent, DoneSignal - [ ] Write applyUsagePostProcessing tests (6): DeepSeek, Zhipu, Moonshot, NoProvider, ExtractCachedTokens, ExtractMoonshot - [ ] Write special handler tests (5): ImageResponse, TTS, STT, FormatConversion_Claude, FormatConversion_Gemini - [ ] Run + Commit ### Key Pattern — HTTP Mock ```go func mockOpenAIResponse(t *testing.T, body string) *http.Response { return &http.Response{ StatusCode: 200, Body: io.NopCloser(strings.NewReader(body)), Header: http.Header{"Content-Type": []string{"application/json"}}, } } func TestOpenaiHandler_Success(t *testing.T) { gin.SetMode(gin.TestMode) c, _ := testutil.NewTestGinContext("POST", "/v1/chat/completions", nil, nil) respBody := `{"id":"chatcmpl-123","object":"chat.completion","model":"gpt-4",` + `"choices":[{"message":{"role":"assistant","content":"hello"}}],` + `"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}` resp := mockOpenAIResponse(t, respBody) info := &relaycommon.RelayInfo{ IsStream: false, RelayMode: relayconstant.RelayModeChatCompletions, UpstreamModelName: "gpt-4", } usage, apiErr := OpenaiHandler(c, info, resp) assert.Nil(t, apiErr) require.NotNil(t, usage) assert.Equal(t, 10, usage.PromptTokens) assert.Equal(t, 5, usage.CompletionTokens) } ``` --- ## Task 3: relay/channel/openai/helper_test.go (10 tests) - [ ] Write ProcessStreamResponse, processTokens, handleLastResponse tests - [ ] Write HandleStreamFormat_OpenAI, HandleStreamFormat_Claude tests - [ ] Run + Commit --- ## Task 4: relay/channel/openai/chat_via_responses_test.go (5 tests) - [ ] Write stringDeltaFromPrefix tests (table-driven) - [ ] Write responsesStreamIndexKey tests - [ ] Write OaiResponsesToChatHandler, OaiResponsesToChatStreamHandler, ToolCalls tests - [ ] Run + Commit ### Key Test Code ```go func TestStringDeltaFromPrefix(t *testing.T) { tests := []struct{ prev, next, want string }{ {"", "hello", "hello"}, {"hel", "hello", "lo"}, {"hello", "hello", ""}, {"abc", "xyz", "xyz"}, // no prefix match } for _, tt := range tests { got := stringDeltaFromPrefix(tt.prev, tt.next) assert.Equal(t, tt.want, got) } } ``` --- ## Task 5: relay/channel/openai/audio_test.go (5 tests) - [ ] Write TTS and STT handler tests with mock HTTP responses - [ ] Run + Commit --- ## Task 6: Final Verification - [ ] `go test -v ./relay/channel/openai/ -count=1` """ # ─── Phase 5 ─────────────────────────────────────────────────────────────── PHASE5 = r"""# Phase 5: 端到端集成测试 实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** 编写 20 个端到端集成测试,验证完整请求生命周期 **Architecture:** 完整 middleware chain (TokenAuth -> Distribute -> Relay) + mock 上游 HTTP 服务器。使用 SQLite 内存 DB。 **Tech Stack:** Go, github.com/glebarez/sqlite, github.com/gin-gonic/gin, net/http/httptest **Pre-requisites:** Phase 0-4 全部完成 --- ## Task 1: test/e2e/billing_lifecycle_test.go (20 tests) - [ ] Write TestE2E_ChatRequest_WalletBilling: full lifecycle with wallet - [ ] Write TestE2E_ChatRequest_SubscriptionBilling: full lifecycle with subscription - [ ] Write TestE2E_ChatRequest_UpstreamError_Refund: upstream failure triggers refund - [ ] Write TestE2E_StreamingRequest: SSE stream with token counting - [ ] Write TestE2E_QuotaExceeded: insufficient quota returns 429 - [ ] Write TestE2E_TokenExpired: expired token returns 401 - [ ] Write TestE2E_ChannelFailover: channel failure triggers retry - [ ] Write TestE2E_ConcurrentRequests: concurrent quota accuracy - [ ] Write TestE2E_FreeModel_NoBilling: free model skips billing - [ ] Write TestE2E_PerCallBilling: per-call billing model - [ ] Write remaining 10 tests (Embeddings, Images, Audio, Rerank, Responses, Claude/Gemini format, TrustQuota, MultiChannel, Affinity) - [ ] Run: `go test -v ./test/e2e/ -count=1` - [ ] Commit ### Key Pattern — E2E Test Setup ```go func setupE2ETest(t *testing.T) (*gin.Engine, *httptest.Server, *gorm.DB) { t.Helper() db := testutil.SetupTestDB(t, &model.User{}, &model.Token{}, &model.Channel{}, &model.Ability{}, &model.Log{}, &model.Model{}, &model.UserSubscription{}, &model.SubscriptionPlan{}, ) // Mock upstream OpenAI server upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) json.NewEncoder(w).Encode(map[string]any{ "id": "chatcmpl-test", "object": "chat.completion", "model": "gpt-4", "choices": []map[string]any{{"message": map[string]any{"role": "assistant", "content": "hi"}}}, "usage": map[string]any{"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}, }) })) t.Cleanup(upstream.Close) // Setup router with real middleware chain gin.SetMode(gin.TestMode) router := gin.New() // ... register middleware + routes return router, upstream, db } func TestE2E_ChatRequest_WalletBilling(t *testing.T) { router, upstream, db := setupE2ETest(t) _ = upstream user := testutil.SeedUser(t, db, func(u *model.User) { u.Quota = 1000000 }) token := testutil.SeedToken(t, db, user.Id, func(tok *model.Token) { tok.UnlimitedQuota = true }) initialQuota := user.Quota // Make request body := `{"model":"gpt-4","messages":[{"role":"user","content":"hello"}]}` req := httptest.NewRequest("POST", "/v1/chat/completions", strings.NewReader(body)) req.Header.Set("Authorization", "Bearer "+token.Key) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, 200, w.Code) testutil.AssertQuotaEquals(t, db, user.Id, initialQuota-expectedCost) } ``` Note: E2E tests are the most complex. The exact setup depends on how the router is configured. The key is to: 1. Create a real gin.Engine with the actual middleware chain 2. Replace the upstream HTTP call with a mock server 3. Verify the full request/response cycle and quota changes --- ## Task 2: Final Verification - [ ] `go test -v ./test/e2e/ -count=1` - [ ] `go test -v ./... -count=1` — full suite """ if __name__ == "__main__": main()