package session import ( "crypto/sha256" "encoding/hex" "strings" "testing" ) // imagePartSig 计算 part 原文摘要(sha256 前 8 hex),供期望值精确构造—— // 签名算法变更时测试期望值随此 helper 单点同步。 func imagePartSig(t *testing.T, part string) string { t.Helper() sum := sha256.Sum256([]byte(part)) return hex.EncodeToString(sum[:4]) } // TestResolveConversationID 覆盖 conversationId 提取的 snake/camel/缺失三态: // - metadata.conversation_id / metadata.conversationId → 取值 // - 顶层 conversation_id / conversationId → 取值(snake 优先同 ExtractKey) // - 缺失 / 只有 user_id → ""(会话头族语义只认对话 ID,绝不回落 user_id) func TestResolveConversationID(t *testing.T) { cases := []struct { name string body string want string }{ {"metadata snake_case", `{"metadata":{"conversation_id":"conv-1"}}`, "conv-1"}, {"metadata camelCase", `{"metadata":{"conversationId":"conv-2"}}`, "conv-2"}, {"top-level snake_case", `{"conversation_id":"conv-3"}`, "conv-3"}, {"top-level camelCase", `{"conversationId":"conv-4"}`, "conv-4"}, {"snake wins over camel", `{"conversation_id":"conv-s","conversationId":"conv-c"}`, "conv-s"}, {"missing", `{"model":"glm-5.2"}`, ""}, {"empty body", ``, ""}, {"broken json", `{broken`, ""}, {"metadata user_id only", `{"metadata":{"user_id":"u1"}}`, ""}, {"top-level user_id only", `{"user_id":"u1"}`, ""}, {"empty string value", `{"conversationId":""}`, ""}, {"non-string value", `{"conversationId":123}`, ""}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := ResolveConversationID([]byte(c.body)); got != c.want { t.Errorf("ResolveConversationID(%q) = %q want %q", c.body, got, c.want) } }) } } // TestNewMessageIDFormat 32 位 hex 且不为空、两次调用大概率不同(随机性冒烟)。 func TestNewMessageIDFormat(t *testing.T) { for i := 0; i < 50; i++ { id := NewMessageID() if len(id) != 32 { t.Fatalf("NewMessageID() = %q len=%d want 32", id, len(id)) } for _, ch := range id { if !strings.ContainsRune("0123456789abcdef", ch) { t.Fatalf("NewMessageID() = %q has non-hex char %q", id, ch) } } } } // TestRequestIDForKeyStability 同 key 恒稳定、异 key 各不同、空 key 每次新值。 func TestRequestIDForKeyStability(t *testing.T) { // 预热清空包级缓存,避免其他测试污染 key(测试隔离)。 k1a := RequestIDForKey("conv-a") k1b := RequestIDForKey("conv-a") if k1a != k1b { t.Errorf("same key should be stable: %q vs %q", k1a, k1b) } k2 := RequestIDForKey("conv-b") if k1a == k2 { t.Errorf("different keys should differ: %q", k1a) } // 空 key:每次调用生成新值(无会话则无"会话内稳定"语义)。 e1 := RequestIDForKey("") e2 := RequestIDForKey("") if e1 == e2 { t.Errorf("empty key should yield fresh values each call: %q", e1) } // 稳定值自身也须是 32 hex(可作 B3 TraceId 直接使用)。 for _, id := range []string{k1a, k2, e1} { if len(id) != 32 { t.Errorf("RequestIDForKey value %q len=%d want 32", id, len(id)) } } } // TestTurnKeyExtraction 轮级兜底键的提取:取**最后一条** user 消息的「序号+文本」, // 轮内追加 assistant/tool 消息不改变键;无 user / 无文本 / 坏 JSON 一律空串。 func TestTurnKeyExtraction(t *testing.T) { cases := []struct { name string body string want string }{ {"single user", `{"messages":[{"role":"user","content":"你好"}]}`, "u0:你好"}, {"last user wins", `{"messages":[{"role":"user","content":"第一问"},{"role":"assistant","content":"答"},{"role":"user","content":"第二问"}]}`, "u2:第二问"}, // agent 多步:轮内追加 assistant/tool 消息,末条 user 位置与内容不变 → 同键。 {"agent step keeps same key", `{"messages":[{"role":"user","content":"任务"},{"role":"assistant","tool_calls":[{"id":"c1"}]},{"role":"tool","content":"结果"}]}`, "u0:任务"}, {"multimodal parts text joined", `{"messages":[{"role":"user","content":[{"type":"text","text":"看图"},{"type":"image_url","image_url":{"url":"data:x"}}]}]}`, "u0:看图\n[image_url:" + imagePartSig(t, `{"type":"image_url","image_url":{"url":"data:x"}}`) + "]"}, {"no user message", `{"messages":[{"role":"system","content":"sys"}]}`, ""}, {"empty messages", `{"messages":[]}`, ""}, {"messages key absent", `{"model":"glm-5.2"}`, ""}, {"broken json", `{broken`, ""}, {"empty body", ``, ""}, {"empty content", `{"messages":[{"role":"user","content":""}]}`, ""}, {"null content", `{"messages":[{"role":"user","content":null}]}`, ""}, // 纯图片 content 按内容签名派生非空轮级键(G1 修复,原为 "");键含 image part 摘要。 {"image only content", `{"messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":"x"}}]}]}`, "u0:[image_url:" + imagePartSig(t, `{"type":"image_url","image_url":{"url":"x"}}`) + "]"}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := TurnKey([]byte(c.body)); got != c.want { t.Errorf("TurnKey(%q) = %q want %q", c.body, got, c.want) } }) } } // TestTurnRequestIDDerivation 轮级 ID 是纯派生:同键恒同值、异键异值、空键每次新值、 // 形态恒 32 hex。 func TestTurnRequestIDDerivation(t *testing.T) { a1 := TurnRequestID("u0:同一个问题") a2 := TurnRequestID("u0:同一个问题") if a1 != a2 { t.Errorf("same turn key should derive same id: %q vs %q", a1, a2) } if b := TurnRequestID("u0:另一个问题"); b == a1 { t.Errorf("different turn keys should derive different ids: %q", b) } // 同文本但序号不同(不同轮里内容相同的提问)也要分开。 if c := TurnRequestID("u2:同一个问题"); c == a1 { t.Errorf("same text at different position should differ: %q", c) } // 空键:无轮可聚合 → 每次新值(保持原有请求级独立行为)。 if e1, e2 := TurnRequestID(""), TurnRequestID(""); e1 == e2 { t.Errorf("empty turn key should yield fresh values each call: %q", e1) } for _, id := range []string{a1, TurnRequestID(""), TurnRequestID("x")} { if len(id) != 32 { t.Errorf("TurnRequestID value %q len=%d want 32", id, len(id)) } } }