gcli2api / internal /session /ids_test.go
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
6.44 kB
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))
}
}
}