File size: 6,444 Bytes
6d60378 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | 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))
}
}
}
|