Key changes: - Upgrade model mapping: Opus 4.5 → Opus 4.6-thinking with precise matching - Unified rate limiting: scope-level → model-level with Redis snapshot sync - Load-balanced scheduling by call count with smart retry mechanism - Force cache billing support - Model identity injection in prompts with leak prevention - Thinking mode auto-handling (max_tokens/budget_tokens fix) - Frontend: whitelist mode toggle, model mapping validation, status indicators - Gemini session fallback with Redis Trie O(L) matching - Ops: enhanced concurrency monitoring, account availability, retry logic - Migration scripts: 049-051 for model mapping unification
207 lines
7.0 KiB
Go
207 lines
7.0 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
|
||
)
|
||
|
||
// mockGeminiSessionCache 模拟 Redis 缓存
|
||
type mockGeminiSessionCache struct {
|
||
sessions map[string]string // key -> value
|
||
}
|
||
|
||
func newMockGeminiSessionCache() *mockGeminiSessionCache {
|
||
return &mockGeminiSessionCache{sessions: make(map[string]string)}
|
||
}
|
||
|
||
func (m *mockGeminiSessionCache) Save(groupID int64, prefixHash, digestChain, uuid string, accountID int64) {
|
||
key := BuildGeminiSessionKey(groupID, prefixHash, digestChain)
|
||
value := FormatGeminiSessionValue(uuid, accountID)
|
||
m.sessions[key] = value
|
||
}
|
||
|
||
func (m *mockGeminiSessionCache) Find(groupID int64, prefixHash, digestChain string) (uuid string, accountID int64, found bool) {
|
||
prefixes := GenerateDigestChainPrefixes(digestChain)
|
||
for _, p := range prefixes {
|
||
key := BuildGeminiSessionKey(groupID, prefixHash, p)
|
||
if val, ok := m.sessions[key]; ok {
|
||
return ParseGeminiSessionValue(val)
|
||
}
|
||
}
|
||
return "", 0, false
|
||
}
|
||
|
||
// TestGeminiSessionContinuousConversation 测试连续会话的摘要链匹配
|
||
func TestGeminiSessionContinuousConversation(t *testing.T) {
|
||
cache := newMockGeminiSessionCache()
|
||
groupID := int64(1)
|
||
prefixHash := "test_prefix_hash"
|
||
sessionUUID := "session-uuid-12345"
|
||
accountID := int64(100)
|
||
|
||
// 模拟第一轮对话
|
||
req1 := &antigravity.GeminiRequest{
|
||
SystemInstruction: &antigravity.GeminiContent{
|
||
Parts: []antigravity.GeminiPart{{Text: "You are a helpful assistant"}},
|
||
},
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Hello, what's your name?"}}},
|
||
},
|
||
}
|
||
chain1 := BuildGeminiDigestChain(req1)
|
||
t.Logf("Round 1 chain: %s", chain1)
|
||
|
||
// 第一轮:没有找到会话,创建新会话
|
||
_, _, found := cache.Find(groupID, prefixHash, chain1)
|
||
if found {
|
||
t.Error("Round 1: should not find existing session")
|
||
}
|
||
|
||
// 保存第一轮会话
|
||
cache.Save(groupID, prefixHash, chain1, sessionUUID, accountID)
|
||
|
||
// 模拟第二轮对话(用户继续对话)
|
||
req2 := &antigravity.GeminiRequest{
|
||
SystemInstruction: &antigravity.GeminiContent{
|
||
Parts: []antigravity.GeminiPart{{Text: "You are a helpful assistant"}},
|
||
},
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Hello, what's your name?"}}},
|
||
{Role: "model", Parts: []antigravity.GeminiPart{{Text: "I'm Claude, nice to meet you!"}}},
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "What can you do?"}}},
|
||
},
|
||
}
|
||
chain2 := BuildGeminiDigestChain(req2)
|
||
t.Logf("Round 2 chain: %s", chain2)
|
||
|
||
// 第二轮:应该能找到会话(通过前缀匹配)
|
||
foundUUID, foundAccID, found := cache.Find(groupID, prefixHash, chain2)
|
||
if !found {
|
||
t.Error("Round 2: should find session via prefix matching")
|
||
}
|
||
if foundUUID != sessionUUID {
|
||
t.Errorf("Round 2: expected UUID %s, got %s", sessionUUID, foundUUID)
|
||
}
|
||
if foundAccID != accountID {
|
||
t.Errorf("Round 2: expected accountID %d, got %d", accountID, foundAccID)
|
||
}
|
||
|
||
// 保存第二轮会话
|
||
cache.Save(groupID, prefixHash, chain2, sessionUUID, accountID)
|
||
|
||
// 模拟第三轮对话
|
||
req3 := &antigravity.GeminiRequest{
|
||
SystemInstruction: &antigravity.GeminiContent{
|
||
Parts: []antigravity.GeminiPart{{Text: "You are a helpful assistant"}},
|
||
},
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Hello, what's your name?"}}},
|
||
{Role: "model", Parts: []antigravity.GeminiPart{{Text: "I'm Claude, nice to meet you!"}}},
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "What can you do?"}}},
|
||
{Role: "model", Parts: []antigravity.GeminiPart{{Text: "I can help with coding, writing, and more!"}}},
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Great, help me write some Go code"}}},
|
||
},
|
||
}
|
||
chain3 := BuildGeminiDigestChain(req3)
|
||
t.Logf("Round 3 chain: %s", chain3)
|
||
|
||
// 第三轮:应该能找到会话(通过第二轮的前缀匹配)
|
||
foundUUID, foundAccID, found = cache.Find(groupID, prefixHash, chain3)
|
||
if !found {
|
||
t.Error("Round 3: should find session via prefix matching")
|
||
}
|
||
if foundUUID != sessionUUID {
|
||
t.Errorf("Round 3: expected UUID %s, got %s", sessionUUID, foundUUID)
|
||
}
|
||
if foundAccID != accountID {
|
||
t.Errorf("Round 3: expected accountID %d, got %d", accountID, foundAccID)
|
||
}
|
||
|
||
t.Log("✓ Continuous conversation session matching works correctly!")
|
||
}
|
||
|
||
// TestGeminiSessionDifferentConversations 测试不同会话不会错误匹配
|
||
func TestGeminiSessionDifferentConversations(t *testing.T) {
|
||
cache := newMockGeminiSessionCache()
|
||
groupID := int64(1)
|
||
prefixHash := "test_prefix_hash"
|
||
|
||
// 第一个会话
|
||
req1 := &antigravity.GeminiRequest{
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Tell me about Go programming"}}},
|
||
},
|
||
}
|
||
chain1 := BuildGeminiDigestChain(req1)
|
||
cache.Save(groupID, prefixHash, chain1, "session-1", 100)
|
||
|
||
// 第二个完全不同的会话
|
||
req2 := &antigravity.GeminiRequest{
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "What's the weather today?"}}},
|
||
},
|
||
}
|
||
chain2 := BuildGeminiDigestChain(req2)
|
||
|
||
// 不同会话不应该匹配
|
||
_, _, found := cache.Find(groupID, prefixHash, chain2)
|
||
if found {
|
||
t.Error("Different conversations should not match")
|
||
}
|
||
|
||
t.Log("✓ Different conversations are correctly isolated!")
|
||
}
|
||
|
||
// TestGeminiSessionPrefixMatchingOrder 测试前缀匹配的优先级(最长匹配优先)
|
||
func TestGeminiSessionPrefixMatchingOrder(t *testing.T) {
|
||
cache := newMockGeminiSessionCache()
|
||
groupID := int64(1)
|
||
prefixHash := "test_prefix_hash"
|
||
|
||
// 创建一个三轮对话
|
||
req := &antigravity.GeminiRequest{
|
||
SystemInstruction: &antigravity.GeminiContent{
|
||
Parts: []antigravity.GeminiPart{{Text: "System prompt"}},
|
||
},
|
||
Contents: []antigravity.GeminiContent{
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Q1"}}},
|
||
{Role: "model", Parts: []antigravity.GeminiPart{{Text: "A1"}}},
|
||
{Role: "user", Parts: []antigravity.GeminiPart{{Text: "Q2"}}},
|
||
},
|
||
}
|
||
fullChain := BuildGeminiDigestChain(req)
|
||
prefixes := GenerateDigestChainPrefixes(fullChain)
|
||
|
||
t.Logf("Full chain: %s", fullChain)
|
||
t.Logf("Prefixes (longest first): %v", prefixes)
|
||
|
||
// 验证前缀生成顺序(从长到短)
|
||
if len(prefixes) != 4 {
|
||
t.Errorf("Expected 4 prefixes, got %d", len(prefixes))
|
||
}
|
||
|
||
// 保存不同轮次的会话到不同账号
|
||
// 第一轮(最短前缀)-> 账号 1
|
||
cache.Save(groupID, prefixHash, prefixes[3], "session-round1", 1)
|
||
// 第二轮 -> 账号 2
|
||
cache.Save(groupID, prefixHash, prefixes[2], "session-round2", 2)
|
||
// 第三轮(最长前缀,完整链)-> 账号 3
|
||
cache.Save(groupID, prefixHash, prefixes[0], "session-round3", 3)
|
||
|
||
// 查找应该返回最长匹配(账号 3)
|
||
_, accID, found := cache.Find(groupID, prefixHash, fullChain)
|
||
if !found {
|
||
t.Error("Should find session")
|
||
}
|
||
if accID != 3 {
|
||
t.Errorf("Should match longest prefix (account 3), got account %d", accID)
|
||
}
|
||
|
||
t.Log("✓ Longest prefix matching works correctly!")
|
||
}
|
||
|
||
// 确保 context 包被使用(避免未使用的导入警告)
|
||
var _ = context.Background
|