digest_session_store_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. //go:build unit
  2. package service
  3. import (
  4. "fmt"
  5. "sync"
  6. "testing"
  7. "time"
  8. gocache "github.com/patrickmn/go-cache"
  9. "github.com/stretchr/testify/assert"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func TestDigestSessionStore_SaveAndFind(t *testing.T) {
  13. store := NewDigestSessionStore()
  14. store.Save(1, "prefix", "s:a1-u:b2-m:c3", "uuid-1", 100, "")
  15. uuid, accountID, _, found := store.Find(1, "prefix", "s:a1-u:b2-m:c3")
  16. require.True(t, found)
  17. assert.Equal(t, "uuid-1", uuid)
  18. assert.Equal(t, int64(100), accountID)
  19. }
  20. func TestDigestSessionStore_PrefixMatch(t *testing.T) {
  21. store := NewDigestSessionStore()
  22. // 保存短链
  23. store.Save(1, "prefix", "u:a-m:b", "uuid-short", 10, "")
  24. // 用长链查找,应前缀匹配到短链
  25. uuid, accountID, matchedChain, found := store.Find(1, "prefix", "u:a-m:b-u:c-m:d")
  26. require.True(t, found)
  27. assert.Equal(t, "uuid-short", uuid)
  28. assert.Equal(t, int64(10), accountID)
  29. assert.Equal(t, "u:a-m:b", matchedChain)
  30. }
  31. func TestDigestSessionStore_LongestPrefixMatch(t *testing.T) {
  32. store := NewDigestSessionStore()
  33. store.Save(1, "prefix", "u:a", "uuid-1", 1, "")
  34. store.Save(1, "prefix", "u:a-m:b", "uuid-2", 2, "")
  35. store.Save(1, "prefix", "u:a-m:b-u:c", "uuid-3", 3, "")
  36. // 应匹配最深的 "u:a-m:b-u:c"(从完整 chain 逐段截断,先命中最长的)
  37. uuid, accountID, _, found := store.Find(1, "prefix", "u:a-m:b-u:c-m:d-u:e")
  38. require.True(t, found)
  39. assert.Equal(t, "uuid-3", uuid)
  40. assert.Equal(t, int64(3), accountID)
  41. // 查找中等长度,应匹配到 "u:a-m:b"
  42. uuid, accountID, _, found = store.Find(1, "prefix", "u:a-m:b-u:x")
  43. require.True(t, found)
  44. assert.Equal(t, "uuid-2", uuid)
  45. assert.Equal(t, int64(2), accountID)
  46. }
  47. func TestDigestSessionStore_SaveDeletesOldChain(t *testing.T) {
  48. store := NewDigestSessionStore()
  49. // 第一轮:保存 "u:a-m:b"
  50. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "")
  51. // 第二轮:同一 uuid 保存更长的链,传入旧 chain
  52. store.Save(1, "prefix", "u:a-m:b-u:c-m:d", "uuid-1", 100, "u:a-m:b")
  53. // 旧链 "u:a-m:b" 应已被删除
  54. _, _, _, found := store.Find(1, "prefix", "u:a-m:b")
  55. assert.False(t, found, "old chain should be deleted")
  56. // 新链应能找到
  57. uuid, accountID, _, found := store.Find(1, "prefix", "u:a-m:b-u:c-m:d")
  58. require.True(t, found)
  59. assert.Equal(t, "uuid-1", uuid)
  60. assert.Equal(t, int64(100), accountID)
  61. }
  62. func TestDigestSessionStore_DifferentSessionsNoInterference(t *testing.T) {
  63. store := NewDigestSessionStore()
  64. // 相同系统提示词,不同用户提示词
  65. store.Save(1, "prefix", "s:sys-u:user1", "uuid-1", 100, "")
  66. store.Save(1, "prefix", "s:sys-u:user2", "uuid-2", 200, "")
  67. uuid, accountID, _, found := store.Find(1, "prefix", "s:sys-u:user1-m:reply1")
  68. require.True(t, found)
  69. assert.Equal(t, "uuid-1", uuid)
  70. assert.Equal(t, int64(100), accountID)
  71. uuid, accountID, _, found = store.Find(1, "prefix", "s:sys-u:user2-m:reply2")
  72. require.True(t, found)
  73. assert.Equal(t, "uuid-2", uuid)
  74. assert.Equal(t, int64(200), accountID)
  75. }
  76. func TestDigestSessionStore_NoMatch(t *testing.T) {
  77. store := NewDigestSessionStore()
  78. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "")
  79. // 完全不同的 chain
  80. _, _, _, found := store.Find(1, "prefix", "u:x-m:y")
  81. assert.False(t, found)
  82. }
  83. func TestDigestSessionStore_DifferentPrefixHash(t *testing.T) {
  84. store := NewDigestSessionStore()
  85. store.Save(1, "prefix1", "u:a-m:b", "uuid-1", 100, "")
  86. // 不同 prefixHash 应隔离
  87. _, _, _, found := store.Find(1, "prefix2", "u:a-m:b")
  88. assert.False(t, found)
  89. }
  90. func TestDigestSessionStore_DifferentGroupID(t *testing.T) {
  91. store := NewDigestSessionStore()
  92. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "")
  93. // 不同 groupID 应隔离
  94. _, _, _, found := store.Find(2, "prefix", "u:a-m:b")
  95. assert.False(t, found)
  96. }
  97. func TestDigestSessionStore_EmptyDigestChain(t *testing.T) {
  98. store := NewDigestSessionStore()
  99. // 空链不应保存
  100. store.Save(1, "prefix", "", "uuid-1", 100, "")
  101. _, _, _, found := store.Find(1, "prefix", "")
  102. assert.False(t, found)
  103. }
  104. func TestDigestSessionStore_TTLExpiration(t *testing.T) {
  105. store := &DigestSessionStore{
  106. cache: gocache.New(100*time.Millisecond, 50*time.Millisecond),
  107. }
  108. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "")
  109. // 立即应该能找到
  110. _, _, _, found := store.Find(1, "prefix", "u:a-m:b")
  111. require.True(t, found)
  112. // 等待过期 + 清理周期
  113. time.Sleep(300 * time.Millisecond)
  114. // 过期后应找不到
  115. _, _, _, found = store.Find(1, "prefix", "u:a-m:b")
  116. assert.False(t, found)
  117. }
  118. func TestDigestSessionStore_ConcurrentSafety(t *testing.T) {
  119. store := NewDigestSessionStore()
  120. var wg sync.WaitGroup
  121. const goroutines = 50
  122. const operations = 100
  123. wg.Add(goroutines)
  124. for g := 0; g < goroutines; g++ {
  125. go func(id int) {
  126. defer wg.Done()
  127. prefix := fmt.Sprintf("prefix-%d", id%5)
  128. for i := 0; i < operations; i++ {
  129. chain := fmt.Sprintf("u:%d-m:%d", id, i)
  130. uuid := fmt.Sprintf("uuid-%d-%d", id, i)
  131. store.Save(1, prefix, chain, uuid, int64(id), "")
  132. store.Find(1, prefix, chain)
  133. }
  134. }(g)
  135. }
  136. wg.Wait()
  137. }
  138. func TestDigestSessionStore_MultipleSessions(t *testing.T) {
  139. store := NewDigestSessionStore()
  140. sessions := []struct {
  141. chain string
  142. uuid string
  143. accountID int64
  144. }{
  145. {"u:session1", "uuid-1", 1},
  146. {"u:session2-m:reply2", "uuid-2", 2},
  147. {"u:session3-m:reply3-u:msg3", "uuid-3", 3},
  148. }
  149. for _, sess := range sessions {
  150. store.Save(1, "prefix", sess.chain, sess.uuid, sess.accountID, "")
  151. }
  152. // 验证每个会话都能正确查找
  153. for _, sess := range sessions {
  154. uuid, accountID, _, found := store.Find(1, "prefix", sess.chain)
  155. require.True(t, found, "should find session: %s", sess.chain)
  156. assert.Equal(t, sess.uuid, uuid)
  157. assert.Equal(t, sess.accountID, accountID)
  158. }
  159. // 验证继续对话的场景
  160. uuid, accountID, _, found := store.Find(1, "prefix", "u:session2-m:reply2-u:newmsg")
  161. require.True(t, found)
  162. assert.Equal(t, "uuid-2", uuid)
  163. assert.Equal(t, int64(2), accountID)
  164. }
  165. func TestDigestSessionStore_Performance1000Sessions(t *testing.T) {
  166. store := NewDigestSessionStore()
  167. // 插入 1000 个会话
  168. for i := 0; i < 1000; i++ {
  169. chain := fmt.Sprintf("s:sys-u:user%d-m:reply%d", i, i)
  170. store.Save(1, "prefix", chain, fmt.Sprintf("uuid-%d", i), int64(i), "")
  171. }
  172. // 查找性能测试
  173. start := time.Now()
  174. const lookups = 10000
  175. for i := 0; i < lookups; i++ {
  176. idx := i % 1000
  177. chain := fmt.Sprintf("s:sys-u:user%d-m:reply%d-u:newmsg", idx, idx)
  178. _, _, _, found := store.Find(1, "prefix", chain)
  179. assert.True(t, found)
  180. }
  181. elapsed := time.Since(start)
  182. t.Logf("%d lookups in %v (%.0f ns/op)", lookups, elapsed, float64(elapsed.Nanoseconds())/lookups)
  183. }
  184. func TestDigestSessionStore_FindReturnsMatchedChain(t *testing.T) {
  185. store := NewDigestSessionStore()
  186. store.Save(1, "prefix", "u:a-m:b-u:c", "uuid-1", 100, "")
  187. // 精确匹配
  188. _, _, matchedChain, found := store.Find(1, "prefix", "u:a-m:b-u:c")
  189. require.True(t, found)
  190. assert.Equal(t, "u:a-m:b-u:c", matchedChain)
  191. // 前缀匹配(截断后命中)
  192. _, _, matchedChain, found = store.Find(1, "prefix", "u:a-m:b-u:c-m:d-u:e")
  193. require.True(t, found)
  194. assert.Equal(t, "u:a-m:b-u:c", matchedChain)
  195. }
  196. func TestDigestSessionStore_CacheItemCountStable(t *testing.T) {
  197. store := NewDigestSessionStore()
  198. // 模拟 100 个独立会话,每个进行 10 轮对话
  199. // 正确传递 oldDigestChain 时,每个会话始终只保留 1 个 key
  200. for conv := 0; conv < 100; conv++ {
  201. var prevMatchedChain string
  202. for round := 0; round < 10; round++ {
  203. chain := fmt.Sprintf("s:sys-u:user%d", conv)
  204. for r := 0; r < round; r++ {
  205. chain += fmt.Sprintf("-m:a%d-u:q%d", r, r+1)
  206. }
  207. uuid := fmt.Sprintf("uuid-conv%d", conv)
  208. _, _, matched, _ := store.Find(1, "prefix", chain)
  209. store.Save(1, "prefix", chain, uuid, int64(conv), matched)
  210. prevMatchedChain = matched
  211. _ = prevMatchedChain
  212. }
  213. }
  214. // 100 个会话 × 1 key/会话 = 应该 ≤ 100 个 key
  215. // 允许少量并发残留,但绝不能接近 100×10=1000
  216. itemCount := store.cache.ItemCount()
  217. assert.LessOrEqual(t, itemCount, 100, "cache should have at most 100 items (1 per conversation), got %d", itemCount)
  218. t.Logf("Cache item count after 100 conversations × 10 rounds: %d", itemCount)
  219. }
  220. func TestDigestSessionStore_TTLPreventsUnboundedGrowth(t *testing.T) {
  221. // 使用极短 TTL 验证大量写入后 cache 能被清理
  222. store := &DigestSessionStore{
  223. cache: gocache.New(100*time.Millisecond, 50*time.Millisecond),
  224. }
  225. // 插入 500 个不同的 key(无 oldDigestChain,模拟最坏场景:全是新会话首轮)
  226. for i := 0; i < 500; i++ {
  227. chain := fmt.Sprintf("u:user%d", i)
  228. store.Save(1, "prefix", chain, fmt.Sprintf("uuid-%d", i), int64(i), "")
  229. }
  230. assert.Equal(t, 500, store.cache.ItemCount())
  231. // 等待 TTL + 清理周期
  232. time.Sleep(300 * time.Millisecond)
  233. assert.Equal(t, 0, store.cache.ItemCount(), "all items should be expired and cleaned up")
  234. }
  235. func TestDigestSessionStore_SaveSameChainNoDelete(t *testing.T) {
  236. store := NewDigestSessionStore()
  237. // 保存 chain
  238. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "")
  239. // 用户重发相同消息:oldDigestChain == digestChain,不应删掉刚设置的 key
  240. store.Save(1, "prefix", "u:a-m:b", "uuid-1", 100, "u:a-m:b")
  241. // 仍然能找到
  242. uuid, accountID, _, found := store.Find(1, "prefix", "u:a-m:b")
  243. require.True(t, found)
  244. assert.Equal(t, "uuid-1", uuid)
  245. assert.Equal(t, int64(100), accountID)
  246. }