ParseGatewayRequest only parsed Anthropic format (system/messages), ignoring Gemini native format (systemInstruction/contents). This caused GenerateSessionHash to produce identical hashes for all Gemini sessions. Add protocol parameter to ParseGatewayRequest to branch between Anthropic and Gemini parsing. Update GenerateSessionHash message traversal to extract text from both formats.
1214 lines
34 KiB
Go
1214 lines
34 KiB
Go
//go:build unit
|
||
|
||
package service
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// ============ 基础优先级测试 ============
|
||
|
||
func TestGenerateSessionHash_NilParsedRequest(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
require.Empty(t, svc.GenerateSessionHash(nil))
|
||
}
|
||
|
||
func TestGenerateSessionHash_EmptyRequest(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
require.Empty(t, svc.GenerateSessionHash(&ParsedRequest{}))
|
||
}
|
||
|
||
func TestGenerateSessionHash_MetadataHasHighestPriority(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed := &ParsedRequest{
|
||
MetadataUserID: "session_123e4567-e89b-12d3-a456-426614174000",
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
|
||
hash := svc.GenerateSessionHash(parsed)
|
||
require.Equal(t, "123e4567-e89b-12d3-a456-426614174000", hash, "metadata session_id should have highest priority")
|
||
}
|
||
|
||
// ============ System + Messages 基础测试 ============
|
||
|
||
func TestGenerateSessionHash_SystemPlusMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
withSystem := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
withoutSystem := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(withSystem)
|
||
h2 := svc.GenerateSessionHash(withoutSystem)
|
||
require.NotEmpty(t, h1)
|
||
require.NotEmpty(t, h2)
|
||
require.NotEqual(t, h1, h2, "system prompt should be part of digest, producing different hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SystemOnlyProducesHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
}
|
||
hash := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, hash, "system prompt alone should produce a hash as part of full digest")
|
||
}
|
||
|
||
func TestGenerateSessionHash_DifferentSystemsSameMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed1 := &ParsedRequest{
|
||
System: "You are assistant A.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
System: "You are assistant B.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h1, h2, "different system prompts with same messages should produce different hashes")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SameSystemSameMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
mk := func() *ParsedRequest {
|
||
return &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
map[string]any{"role": "assistant", "content": "hi"},
|
||
},
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(mk())
|
||
h2 := svc.GenerateSessionHash(mk())
|
||
require.Equal(t, h1, h2, "same system + same messages should produce identical hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_DifferentMessagesProduceDifferentHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed1 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "help me with Go"},
|
||
},
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "help me with Python"},
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h1, h2, "same system but different messages should produce different hashes")
|
||
}
|
||
|
||
// ============ SessionContext 核心测试 ============
|
||
|
||
func TestGenerateSessionHash_DifferentSessionContextProducesDifferentHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 相同消息 + 不同 SessionContext → 不同 hash(解决碰撞问题的核心场景)
|
||
parsed1 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "192.168.1.1",
|
||
UserAgent: "Mozilla/5.0",
|
||
APIKeyID: 100,
|
||
},
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "10.0.0.1",
|
||
UserAgent: "curl/7.0",
|
||
APIKeyID: 200,
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEmpty(t, h1)
|
||
require.NotEmpty(t, h2)
|
||
require.NotEqual(t, h1, h2, "same messages but different SessionContext should produce different hashes")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SameSessionContextProducesSameHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
mk := func() *ParsedRequest {
|
||
return &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "192.168.1.1",
|
||
UserAgent: "Mozilla/5.0",
|
||
APIKeyID: 100,
|
||
},
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(mk())
|
||
h2 := svc.GenerateSessionHash(mk())
|
||
require.Equal(t, h1, h2, "same messages + same SessionContext should produce identical hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_MetadataOverridesSessionContext(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed := &ParsedRequest{
|
||
MetadataUserID: "session_123e4567-e89b-12d3-a456-426614174000",
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "192.168.1.1",
|
||
UserAgent: "Mozilla/5.0",
|
||
APIKeyID: 100,
|
||
},
|
||
}
|
||
|
||
hash := svc.GenerateSessionHash(parsed)
|
||
require.Equal(t, "123e4567-e89b-12d3-a456-426614174000", hash,
|
||
"metadata session_id should take priority over SessionContext")
|
||
}
|
||
|
||
func TestGenerateSessionHash_NilSessionContextBackwardCompatible(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
withCtx := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: nil,
|
||
}
|
||
withoutCtx := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(withCtx)
|
||
h2 := svc.GenerateSessionHash(withoutCtx)
|
||
require.Equal(t, h1, h2, "nil SessionContext should produce same hash as no SessionContext")
|
||
}
|
||
|
||
// ============ 多轮连续会话测试 ============
|
||
|
||
func TestGenerateSessionHash_ContinuousConversation_HashChangesWithMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 模拟连续会话:每增加一轮对话,hash 应该不同(内容累积变化)
|
||
round1 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
round2 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
map[string]any{"role": "assistant", "content": "Hi there!"},
|
||
map[string]any{"role": "user", "content": "How are you?"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
round3 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
map[string]any{"role": "assistant", "content": "Hi there!"},
|
||
map[string]any{"role": "user", "content": "How are you?"},
|
||
map[string]any{"role": "assistant", "content": "I'm doing well!"},
|
||
map[string]any{"role": "user", "content": "Tell me a joke"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(round1)
|
||
h2 := svc.GenerateSessionHash(round2)
|
||
h3 := svc.GenerateSessionHash(round3)
|
||
|
||
require.NotEmpty(t, h1)
|
||
require.NotEmpty(t, h2)
|
||
require.NotEmpty(t, h3)
|
||
require.NotEqual(t, h1, h2, "different conversation rounds should produce different hashes")
|
||
require.NotEqual(t, h2, h3, "each new round should produce a different hash")
|
||
require.NotEqual(t, h1, h3, "round 1 and round 3 should differ")
|
||
}
|
||
|
||
func TestGenerateSessionHash_ContinuousConversation_SameRoundSameHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 同一轮对话重复请求(如重试)应产生相同 hash
|
||
mk := func() *ParsedRequest {
|
||
return &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
map[string]any{"role": "assistant", "content": "Hi there!"},
|
||
map[string]any{"role": "user", "content": "How are you?"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(mk())
|
||
h2 := svc.GenerateSessionHash(mk())
|
||
require.Equal(t, h1, h2, "same conversation state should produce identical hash on retry")
|
||
}
|
||
|
||
// ============ 消息回退测试 ============
|
||
|
||
func TestGenerateSessionHash_MessageRollback(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 模拟消息回退:用户删掉最后一轮再重发
|
||
original := &ParsedRequest{
|
||
System: "System prompt",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "msg1"},
|
||
map[string]any{"role": "assistant", "content": "reply1"},
|
||
map[string]any{"role": "user", "content": "msg2"},
|
||
map[string]any{"role": "assistant", "content": "reply2"},
|
||
map[string]any{"role": "user", "content": "msg3"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
// 回退到 msg2 后,用新的 msg3 替代
|
||
rollback := &ParsedRequest{
|
||
System: "System prompt",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "msg1"},
|
||
map[string]any{"role": "assistant", "content": "reply1"},
|
||
map[string]any{"role": "user", "content": "msg2"},
|
||
map[string]any{"role": "assistant", "content": "reply2"},
|
||
map[string]any{"role": "user", "content": "different msg3"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
hOrig := svc.GenerateSessionHash(original)
|
||
hRollback := svc.GenerateSessionHash(rollback)
|
||
require.NotEqual(t, hOrig, hRollback, "rollback with different last message should produce different hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_MessageRollbackSameContent(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 回退后重新发送相同内容 → 相同 hash(合理的粘性恢复)
|
||
mk := func() *ParsedRequest {
|
||
return &ParsedRequest{
|
||
System: "System prompt",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "msg1"},
|
||
map[string]any{"role": "assistant", "content": "reply1"},
|
||
map[string]any{"role": "user", "content": "msg2"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(mk())
|
||
h2 := svc.GenerateSessionHash(mk())
|
||
require.Equal(t, h1, h2, "rollback and resend same content should produce same hash")
|
||
}
|
||
|
||
// ============ 相同 System、不同用户消息 ============
|
||
|
||
func TestGenerateSessionHash_SameSystemDifferentUsers(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 两个不同用户使用相同 system prompt 但发送不同消息
|
||
user1 := &ParsedRequest{
|
||
System: "You are a code reviewer.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "Review this Go code"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "vscode",
|
||
APIKeyID: 1,
|
||
},
|
||
}
|
||
user2 := &ParsedRequest{
|
||
System: "You are a code reviewer.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "Review this Python code"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "2.2.2.2",
|
||
UserAgent: "vscode",
|
||
APIKeyID: 2,
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(user1)
|
||
h2 := svc.GenerateSessionHash(user2)
|
||
require.NotEqual(t, h1, h2, "different users with different messages should get different hashes")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SameSystemSameMessageDifferentContext(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 这是修复的核心场景:两个不同用户发送完全相同的 system + messages(如 "hello")
|
||
// 有了 SessionContext 后应该产生不同 hash
|
||
user1 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "Mozilla/5.0",
|
||
APIKeyID: 10,
|
||
},
|
||
}
|
||
user2 := &ParsedRequest{
|
||
System: "You are a helpful assistant.",
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "2.2.2.2",
|
||
UserAgent: "Mozilla/5.0",
|
||
APIKeyID: 20,
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(user1)
|
||
h2 := svc.GenerateSessionHash(user2)
|
||
require.NotEqual(t, h1, h2, "CRITICAL: same system+messages but different users should get different hashes")
|
||
}
|
||
|
||
// ============ SessionContext 各字段独立影响测试 ============
|
||
|
||
func TestGenerateSessionHash_SessionContext_IPDifference(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
base := func(ip string) *ParsedRequest {
|
||
return &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "test"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: ip,
|
||
UserAgent: "same-ua",
|
||
APIKeyID: 1,
|
||
},
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(base("1.1.1.1"))
|
||
h2 := svc.GenerateSessionHash(base("2.2.2.2"))
|
||
require.NotEqual(t, h1, h2, "different IP should produce different hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SessionContext_UADifference(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
base := func(ua string) *ParsedRequest {
|
||
return &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "test"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: ua,
|
||
APIKeyID: 1,
|
||
},
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(base("Mozilla/5.0"))
|
||
h2 := svc.GenerateSessionHash(base("curl/7.0"))
|
||
require.NotEqual(t, h1, h2, "different User-Agent should produce different hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SessionContext_APIKeyIDDifference(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
base := func(keyID int64) *ParsedRequest {
|
||
return &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "test"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "same-ua",
|
||
APIKeyID: keyID,
|
||
},
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(base(1))
|
||
h2 := svc.GenerateSessionHash(base(2))
|
||
require.NotEqual(t, h1, h2, "different APIKeyID should produce different hash")
|
||
}
|
||
|
||
// ============ 多用户并发相同消息场景 ============
|
||
|
||
func TestGenerateSessionHash_MultipleUsersSameFirstMessage(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 模拟 5 个不同用户同时发送 "hello" → 应该产生 5 个不同的 hash
|
||
hashes := make(map[string]bool)
|
||
for i := 0; i < 5; i++ {
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "192.168.1." + string(rune('1'+i)),
|
||
UserAgent: "client-" + string(rune('A'+i)),
|
||
APIKeyID: int64(i + 1),
|
||
},
|
||
}
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h)
|
||
require.False(t, hashes[h], "hash collision detected for user %d", i)
|
||
hashes[h] = true
|
||
}
|
||
require.Len(t, hashes, 5, "5 different users should produce 5 unique hashes")
|
||
}
|
||
|
||
// ============ 连续会话粘性:多轮对话同一用户 ============
|
||
|
||
func TestGenerateSessionHash_SameUserGrowingConversation(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "browser", APIKeyID: 42}
|
||
|
||
// 模拟同一用户的连续会话,每轮 hash 不同但同用户重试保持一致
|
||
messages := []map[string]any{
|
||
{"role": "user", "content": "msg1"},
|
||
{"role": "assistant", "content": "reply1"},
|
||
{"role": "user", "content": "msg2"},
|
||
{"role": "assistant", "content": "reply2"},
|
||
{"role": "user", "content": "msg3"},
|
||
{"role": "assistant", "content": "reply3"},
|
||
{"role": "user", "content": "msg4"},
|
||
}
|
||
|
||
prevHash := ""
|
||
for round := 1; round <= len(messages); round += 2 {
|
||
// 构建前 round 条消息
|
||
msgs := make([]any, round)
|
||
for j := 0; j < round; j++ {
|
||
msgs[j] = messages[j]
|
||
}
|
||
parsed := &ParsedRequest{
|
||
System: "System",
|
||
HasSystem: true,
|
||
Messages: msgs,
|
||
SessionContext: ctx,
|
||
}
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "round %d hash should not be empty", round)
|
||
|
||
if prevHash != "" {
|
||
require.NotEqual(t, prevHash, h, "round %d hash should differ from previous round", round)
|
||
}
|
||
prevHash = h
|
||
|
||
// 同一轮重试应该相同
|
||
h2 := svc.GenerateSessionHash(parsed)
|
||
require.Equal(t, h, h2, "retry of round %d should produce same hash", round)
|
||
}
|
||
}
|
||
|
||
// ============ 多轮消息内容结构化测试 ============
|
||
|
||
func TestGenerateSessionHash_MultipleUserMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 5 条用户消息(无 assistant 回复)
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "first"},
|
||
map[string]any{"role": "user", "content": "second"},
|
||
map[string]any{"role": "user", "content": "third"},
|
||
map[string]any{"role": "user", "content": "fourth"},
|
||
map[string]any{"role": "user", "content": "fifth"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h)
|
||
|
||
// 修改中间一条消息应该改变 hash
|
||
parsed2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "first"},
|
||
map[string]any{"role": "user", "content": "CHANGED"},
|
||
map[string]any{"role": "user", "content": "third"},
|
||
map[string]any{"role": "user", "content": "fourth"},
|
||
map[string]any{"role": "user", "content": "fifth"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h, h2, "changing any message should change the hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_MessageOrderMatters(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
parsed1 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "alpha"},
|
||
map[string]any{"role": "user", "content": "beta"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "beta"},
|
||
map[string]any{"role": "user", "content": "alpha"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h1, h2, "message order should affect the hash")
|
||
}
|
||
|
||
// ============ 复杂内容格式测试 ============
|
||
|
||
func TestGenerateSessionHash_StructuredContent(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 结构化 content(数组形式)
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"content": []any{
|
||
map[string]any{"type": "text", "text": "Look at this"},
|
||
map[string]any{"type": "text", "text": "And this too"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "structured content should produce a hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_ArraySystemPrompt(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 数组格式的 system prompt
|
||
parsed := &ParsedRequest{
|
||
System: []any{
|
||
map[string]any{"type": "text", "text": "You are a helpful assistant."},
|
||
map[string]any{"type": "text", "text": "Be concise."},
|
||
},
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "array system prompt should produce a hash")
|
||
}
|
||
|
||
// ============ SessionContext 与 cache_control 优先级 ============
|
||
|
||
func TestGenerateSessionHash_CacheControlOverridesSessionContext(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 当有 cache_control: ephemeral 时,使用第 2 级优先级
|
||
// SessionContext 不应影响结果
|
||
parsed1 := &ParsedRequest{
|
||
System: []any{
|
||
map[string]any{
|
||
"type": "text",
|
||
"text": "You are a tool-specific assistant.",
|
||
"cache_control": map[string]any{"type": "ephemeral"},
|
||
},
|
||
},
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "ua1",
|
||
APIKeyID: 100,
|
||
},
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
System: []any{
|
||
map[string]any{
|
||
"type": "text",
|
||
"text": "You are a tool-specific assistant.",
|
||
"cache_control": map[string]any{"type": "ephemeral"},
|
||
},
|
||
},
|
||
HasSystem: true,
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "hello"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "2.2.2.2",
|
||
UserAgent: "ua2",
|
||
APIKeyID: 200,
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.Equal(t, h1, h2, "cache_control ephemeral has higher priority, SessionContext should not affect result")
|
||
}
|
||
|
||
// ============ 边界情况 ============
|
||
|
||
func TestGenerateSessionHash_EmptyMessages(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "test",
|
||
APIKeyID: 1,
|
||
},
|
||
}
|
||
|
||
// 空 messages + 只有 SessionContext 时,combined.Len() > 0 因为有 context 写入
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "empty messages with SessionContext should still produce a hash from context")
|
||
}
|
||
|
||
func TestGenerateSessionHash_EmptyMessagesNoContext(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{},
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.Empty(t, h, "empty messages without SessionContext should produce empty hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_SessionContextWithEmptyFields(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// SessionContext 字段为空字符串和零值时仍应影响 hash
|
||
withEmptyCtx := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "test"},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "",
|
||
UserAgent: "",
|
||
APIKeyID: 0,
|
||
},
|
||
}
|
||
withoutCtx := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{"role": "user", "content": "test"},
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(withEmptyCtx)
|
||
h2 := svc.GenerateSessionHash(withoutCtx)
|
||
// 有 SessionContext(即使字段为空)仍然会写入分隔符 "::" 等
|
||
require.NotEqual(t, h1, h2, "empty-field SessionContext should still differ from nil SessionContext")
|
||
}
|
||
|
||
// ============ 长对话历史测试 ============
|
||
|
||
func TestGenerateSessionHash_LongConversation(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "test", APIKeyID: 1}
|
||
|
||
// 构建 20 轮对话
|
||
messages := make([]any, 0, 40)
|
||
for i := 0; i < 20; i++ {
|
||
messages = append(messages, map[string]any{
|
||
"role": "user",
|
||
"content": "user message " + string(rune('A'+i)),
|
||
})
|
||
messages = append(messages, map[string]any{
|
||
"role": "assistant",
|
||
"content": "assistant reply " + string(rune('A'+i)),
|
||
})
|
||
}
|
||
|
||
parsed := &ParsedRequest{
|
||
System: "System prompt",
|
||
HasSystem: true,
|
||
Messages: messages,
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h)
|
||
|
||
// 再加一轮应该不同
|
||
moreMessages := make([]any, len(messages)+2)
|
||
copy(moreMessages, messages)
|
||
moreMessages[len(messages)] = map[string]any{"role": "user", "content": "one more"}
|
||
moreMessages[len(messages)+1] = map[string]any{"role": "assistant", "content": "ok"}
|
||
|
||
parsed2 := &ParsedRequest{
|
||
System: "System prompt",
|
||
HasSystem: true,
|
||
Messages: moreMessages,
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h, h2, "adding more messages to long conversation should change hash")
|
||
}
|
||
|
||
// ============ Gemini 原生格式 session hash 测试 ============
|
||
|
||
func TestGenerateSessionHash_GeminiContentsProducesHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// Gemini 格式: contents[].parts[].text
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Hello from Gemini"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.2.3.4",
|
||
UserAgent: "gemini-cli",
|
||
APIKeyID: 1,
|
||
},
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "Gemini contents with parts should produce a non-empty hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiDifferentContentsDifferentHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
parsed1 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Hello"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
parsed2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Goodbye"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(parsed1)
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h1, h2, "different Gemini contents should produce different hashes")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiSameContentsSameHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
mk := func() *ParsedRequest {
|
||
return &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Hello"},
|
||
},
|
||
},
|
||
map[string]any{
|
||
"role": "model",
|
||
"parts": []any{
|
||
map[string]any{"text": "Hi there!"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(mk())
|
||
h2 := svc.GenerateSessionHash(mk())
|
||
require.Equal(t, h1, h2, "same Gemini contents should produce identical hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiMultiTurnHashChanges(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
round1 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
round2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
map[string]any{
|
||
"role": "model",
|
||
"parts": []any{map[string]any{"text": "Hi!"}},
|
||
},
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "How are you?"}},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(round1)
|
||
h2 := svc.GenerateSessionHash(round2)
|
||
require.NotEmpty(t, h1)
|
||
require.NotEmpty(t, h2)
|
||
require.NotEqual(t, h1, h2, "Gemini multi-turn should produce different hashes per round")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiDifferentUsersSameContentDifferentHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 核心场景:两个不同用户发送相同 Gemini 格式消息应得到不同 hash
|
||
user1 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "1.1.1.1",
|
||
UserAgent: "gemini-cli",
|
||
APIKeyID: 10,
|
||
},
|
||
}
|
||
user2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
},
|
||
SessionContext: &SessionContext{
|
||
ClientIP: "2.2.2.2",
|
||
UserAgent: "gemini-cli",
|
||
APIKeyID: 20,
|
||
},
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(user1)
|
||
h2 := svc.GenerateSessionHash(user2)
|
||
require.NotEqual(t, h1, h2, "CRITICAL: different Gemini users with same content must get different hashes")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiSystemInstructionAffectsHash(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
// systemInstruction 经 ParseGatewayRequest 解析后存入 parsed.System
|
||
withSys := &ParsedRequest{
|
||
System: []any{
|
||
map[string]any{"text": "You are a coding assistant."},
|
||
},
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
withoutSys := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{map[string]any{"text": "hello"}},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h1 := svc.GenerateSessionHash(withSys)
|
||
h2 := svc.GenerateSessionHash(withoutSys)
|
||
require.NotEqual(t, h1, h2, "systemInstruction should affect the hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiMultiPartMessage(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
// 多 parts 的消息
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Part 1"},
|
||
map[string]any{"text": "Part 2"},
|
||
map[string]any{"text": "Part 3"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "multi-part Gemini message should produce a hash")
|
||
|
||
// 不同内容的多 parts
|
||
parsed2 := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Part 1"},
|
||
map[string]any{"text": "CHANGED"},
|
||
map[string]any{"text": "Part 3"},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.NotEqual(t, h, h2, "changing a part should change the hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiNonTextPartsIgnored(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "1.2.3.4", UserAgent: "gemini-cli", APIKeyID: 1}
|
||
|
||
// 含非 text 类型 parts(如 inline_data),应被跳过但不报错
|
||
parsed := &ParsedRequest{
|
||
Messages: []any{
|
||
map[string]any{
|
||
"role": "user",
|
||
"parts": []any{
|
||
map[string]any{"text": "Describe this image"},
|
||
map[string]any{"inline_data": map[string]any{"mime_type": "image/png", "data": "base64..."}},
|
||
},
|
||
},
|
||
},
|
||
SessionContext: ctx,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "Gemini message with mixed parts should still produce a hash from text parts")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiMultiTurnHashNotSticky(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
ctx := &SessionContext{ClientIP: "10.0.0.1", UserAgent: "gemini-cli", APIKeyID: 42}
|
||
|
||
// 模拟同一 Gemini 会话的三轮请求,每轮 contents 累积增长。
|
||
// 验证预期行为:每轮 hash 都不同,即 GenerateSessionHash 不具备跨轮粘性。
|
||
// 这是 by-design 的——Gemini 的跨轮粘性由 Digest Fallback(BuildGeminiDigestChain)负责。
|
||
round1Body := []byte(`{
|
||
"systemInstruction": {"parts": [{"text": "You are a coding assistant."}]},
|
||
"contents": [
|
||
{"role": "user", "parts": [{"text": "Write a Go function"}]}
|
||
]
|
||
}`)
|
||
round2Body := []byte(`{
|
||
"systemInstruction": {"parts": [{"text": "You are a coding assistant."}]},
|
||
"contents": [
|
||
{"role": "user", "parts": [{"text": "Write a Go function"}]},
|
||
{"role": "model", "parts": [{"text": "func hello() {}"}]},
|
||
{"role": "user", "parts": [{"text": "Add error handling"}]}
|
||
]
|
||
}`)
|
||
round3Body := []byte(`{
|
||
"systemInstruction": {"parts": [{"text": "You are a coding assistant."}]},
|
||
"contents": [
|
||
{"role": "user", "parts": [{"text": "Write a Go function"}]},
|
||
{"role": "model", "parts": [{"text": "func hello() {}"}]},
|
||
{"role": "user", "parts": [{"text": "Add error handling"}]},
|
||
{"role": "model", "parts": [{"text": "func hello() error { return nil }"}]},
|
||
{"role": "user", "parts": [{"text": "Now add tests"}]}
|
||
]
|
||
}`)
|
||
|
||
hashes := make([]string, 3)
|
||
for i, body := range [][]byte{round1Body, round2Body, round3Body} {
|
||
parsed, err := ParseGatewayRequest(body, "gemini")
|
||
require.NoError(t, err)
|
||
parsed.SessionContext = ctx
|
||
hashes[i] = svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, hashes[i], "round %d hash should not be empty", i+1)
|
||
}
|
||
|
||
// 每轮 hash 都不同——这是预期行为
|
||
require.NotEqual(t, hashes[0], hashes[1], "round 1 vs 2 hash should differ (contents grow)")
|
||
require.NotEqual(t, hashes[1], hashes[2], "round 2 vs 3 hash should differ (contents grow)")
|
||
require.NotEqual(t, hashes[0], hashes[2], "round 1 vs 3 hash should differ")
|
||
|
||
// 同一轮重试应产生相同 hash
|
||
parsed1Again, err := ParseGatewayRequest(round2Body, "gemini")
|
||
require.NoError(t, err)
|
||
parsed1Again.SessionContext = ctx
|
||
h2Again := svc.GenerateSessionHash(parsed1Again)
|
||
require.Equal(t, hashes[1], h2Again, "retry of same round should produce same hash")
|
||
}
|
||
|
||
func TestGenerateSessionHash_GeminiEndToEnd(t *testing.T) {
|
||
svc := &GatewayService{}
|
||
|
||
// 端到端测试:模拟 ParseGatewayRequest + GenerateSessionHash 完整流程
|
||
body := []byte(`{
|
||
"model": "gemini-2.5-pro",
|
||
"systemInstruction": {
|
||
"parts": [{"text": "You are a coding assistant."}]
|
||
},
|
||
"contents": [
|
||
{"role": "user", "parts": [{"text": "Write a Go function"}]},
|
||
{"role": "model", "parts": [{"text": "Here is a function..."}]},
|
||
{"role": "user", "parts": [{"text": "Now add error handling"}]}
|
||
]
|
||
}`)
|
||
|
||
parsed, err := ParseGatewayRequest(body, "gemini")
|
||
require.NoError(t, err)
|
||
parsed.SessionContext = &SessionContext{
|
||
ClientIP: "10.0.0.1",
|
||
UserAgent: "gemini-cli/1.0",
|
||
APIKeyID: 42,
|
||
}
|
||
|
||
h := svc.GenerateSessionHash(parsed)
|
||
require.NotEmpty(t, h, "end-to-end Gemini flow should produce a hash")
|
||
|
||
// 同一请求再次解析应产生相同 hash
|
||
parsed2, err := ParseGatewayRequest(body, "gemini")
|
||
require.NoError(t, err)
|
||
parsed2.SessionContext = &SessionContext{
|
||
ClientIP: "10.0.0.1",
|
||
UserAgent: "gemini-cli/1.0",
|
||
APIKeyID: 42,
|
||
}
|
||
|
||
h2 := svc.GenerateSessionHash(parsed2)
|
||
require.Equal(t, h, h2, "same request should produce same hash")
|
||
|
||
// 不同用户发送相同请求应产生不同 hash
|
||
parsed3, err := ParseGatewayRequest(body, "gemini")
|
||
require.NoError(t, err)
|
||
parsed3.SessionContext = &SessionContext{
|
||
ClientIP: "10.0.0.2",
|
||
UserAgent: "gemini-cli/1.0",
|
||
APIKeyID: 99,
|
||
}
|
||
|
||
h3 := svc.GenerateSessionHash(parsed3)
|
||
require.NotEqual(t, h, h3, "different user with same Gemini request should get different hash")
|
||
}
|