gemini_session_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. package service
  2. import (
  3. "testing"
  4. "github.com/Wei-Shaw/sub2api/internal/pkg/antigravity"
  5. )
  6. func TestShortHash(t *testing.T) {
  7. tests := []struct {
  8. name string
  9. input []byte
  10. }{
  11. {"empty", []byte{}},
  12. {"simple", []byte("hello world")},
  13. {"json", []byte(`{"role":"user","parts":[{"text":"hello"}]}`)},
  14. }
  15. for _, tt := range tests {
  16. t.Run(tt.name, func(t *testing.T) {
  17. result := shortHash(tt.input)
  18. // Base36 编码的 uint64 最长 13 个字符
  19. if len(result) > 13 {
  20. t.Errorf("shortHash result too long: %d characters", len(result))
  21. }
  22. // 相同输入应该产生相同输出
  23. result2 := shortHash(tt.input)
  24. if result != result2 {
  25. t.Errorf("shortHash not deterministic: %s vs %s", result, result2)
  26. }
  27. })
  28. }
  29. }
  30. func TestBuildGeminiDigestChain(t *testing.T) {
  31. tests := []struct {
  32. name string
  33. req *antigravity.GeminiRequest
  34. wantLen int // 预期的分段数量
  35. hasEmpty bool // 是否应该是空字符串
  36. }{
  37. {
  38. name: "nil request",
  39. req: nil,
  40. hasEmpty: true,
  41. },
  42. {
  43. name: "empty contents",
  44. req: &antigravity.GeminiRequest{
  45. Contents: []antigravity.GeminiContent{},
  46. },
  47. hasEmpty: true,
  48. },
  49. {
  50. name: "single user message",
  51. req: &antigravity.GeminiRequest{
  52. Contents: []antigravity.GeminiContent{
  53. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  54. },
  55. },
  56. wantLen: 1, // u:<hash>
  57. },
  58. {
  59. name: "user and model messages",
  60. req: &antigravity.GeminiRequest{
  61. Contents: []antigravity.GeminiContent{
  62. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  63. {Role: "model", Parts: []antigravity.GeminiPart{{Text: "hi there"}}},
  64. },
  65. },
  66. wantLen: 2, // u:<hash>-m:<hash>
  67. },
  68. {
  69. name: "with system instruction",
  70. req: &antigravity.GeminiRequest{
  71. SystemInstruction: &antigravity.GeminiContent{
  72. Role: "user",
  73. Parts: []antigravity.GeminiPart{{Text: "You are a helpful assistant"}},
  74. },
  75. Contents: []antigravity.GeminiContent{
  76. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  77. },
  78. },
  79. wantLen: 2, // s:<hash>-u:<hash>
  80. },
  81. {
  82. name: "conversation with system",
  83. req: &antigravity.GeminiRequest{
  84. SystemInstruction: &antigravity.GeminiContent{
  85. Role: "user",
  86. Parts: []antigravity.GeminiPart{{Text: "System prompt"}},
  87. },
  88. Contents: []antigravity.GeminiContent{
  89. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  90. {Role: "model", Parts: []antigravity.GeminiPart{{Text: "hi"}}},
  91. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "how are you?"}}},
  92. },
  93. },
  94. wantLen: 4, // s:<hash>-u:<hash>-m:<hash>-u:<hash>
  95. },
  96. }
  97. for _, tt := range tests {
  98. t.Run(tt.name, func(t *testing.T) {
  99. result := BuildGeminiDigestChain(tt.req)
  100. if tt.hasEmpty {
  101. if result != "" {
  102. t.Errorf("expected empty string, got: %s", result)
  103. }
  104. return
  105. }
  106. // 检查分段数量
  107. parts := splitChain(result)
  108. if len(parts) != tt.wantLen {
  109. t.Errorf("expected %d parts, got %d: %s", tt.wantLen, len(parts), result)
  110. }
  111. // 验证每个分段的格式
  112. for _, part := range parts {
  113. if len(part) < 3 || part[1] != ':' {
  114. t.Errorf("invalid part format: %s", part)
  115. }
  116. prefix := part[0]
  117. if prefix != 's' && prefix != 'u' && prefix != 'm' {
  118. t.Errorf("invalid prefix: %c", prefix)
  119. }
  120. }
  121. })
  122. }
  123. }
  124. func TestGenerateGeminiPrefixHash(t *testing.T) {
  125. hash1 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Mozilla/5.0", "antigravity", "gemini-2.5-pro")
  126. hash2 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Mozilla/5.0", "antigravity", "gemini-2.5-pro")
  127. hash3 := GenerateGeminiPrefixHash(2, 100, "192.168.1.1", "Mozilla/5.0", "antigravity", "gemini-2.5-pro")
  128. // 相同输入应该产生相同输出
  129. if hash1 != hash2 {
  130. t.Errorf("GenerateGeminiPrefixHash not deterministic: %s vs %s", hash1, hash2)
  131. }
  132. // 不同输入应该产生不同输出
  133. if hash1 == hash3 {
  134. t.Errorf("GenerateGeminiPrefixHash collision for different inputs")
  135. }
  136. // Base64 URL 编码的 12 字节正好是 16 字符
  137. if len(hash1) != 16 {
  138. t.Errorf("expected 16 characters, got %d: %s", len(hash1), hash1)
  139. }
  140. }
  141. func TestGenerateGeminiPrefixHash_IgnoresUserAgentVersionNoise(t *testing.T) {
  142. hash1 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Mozilla/5.0 codex_cli_rs/0.1.0", "antigravity", "gemini-2.5-pro")
  143. hash2 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Mozilla/5.0 codex_cli_rs/0.1.1", "antigravity", "gemini-2.5-pro")
  144. if hash1 != hash2 {
  145. t.Fatalf("version-only User-Agent changes should not perturb Gemini prefix hash: %s vs %s", hash1, hash2)
  146. }
  147. }
  148. func TestGenerateGeminiPrefixHash_IgnoresFreeformUserAgentVersionNoise(t *testing.T) {
  149. hash1 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Codex CLI 0.1.0", "antigravity", "gemini-2.5-pro")
  150. hash2 := GenerateGeminiPrefixHash(1, 100, "192.168.1.1", "Codex CLI 0.1.1", "antigravity", "gemini-2.5-pro")
  151. if hash1 != hash2 {
  152. t.Fatalf("free-form version-only User-Agent changes should not perturb Gemini prefix hash: %s vs %s", hash1, hash2)
  153. }
  154. }
  155. func TestParseGeminiSessionValue(t *testing.T) {
  156. tests := []struct {
  157. name string
  158. value string
  159. wantUUID string
  160. wantAccID int64
  161. wantOK bool
  162. }{
  163. {
  164. name: "empty",
  165. value: "",
  166. wantOK: false,
  167. },
  168. {
  169. name: "no colon",
  170. value: "abc123",
  171. wantOK: false,
  172. },
  173. {
  174. name: "valid",
  175. value: "uuid-1234:100",
  176. wantUUID: "uuid-1234",
  177. wantAccID: 100,
  178. wantOK: true,
  179. },
  180. {
  181. name: "uuid with colon",
  182. value: "a:b:c:123",
  183. wantUUID: "a:b:c",
  184. wantAccID: 123,
  185. wantOK: true,
  186. },
  187. {
  188. name: "invalid account id",
  189. value: "uuid:abc",
  190. wantOK: false,
  191. },
  192. }
  193. for _, tt := range tests {
  194. t.Run(tt.name, func(t *testing.T) {
  195. uuid, accID, ok := ParseGeminiSessionValue(tt.value)
  196. if ok != tt.wantOK {
  197. t.Errorf("ok: expected %v, got %v", tt.wantOK, ok)
  198. }
  199. if tt.wantOK {
  200. if uuid != tt.wantUUID {
  201. t.Errorf("uuid: expected %s, got %s", tt.wantUUID, uuid)
  202. }
  203. if accID != tt.wantAccID {
  204. t.Errorf("accountID: expected %d, got %d", tt.wantAccID, accID)
  205. }
  206. }
  207. })
  208. }
  209. }
  210. func TestFormatGeminiSessionValue(t *testing.T) {
  211. result := FormatGeminiSessionValue("test-uuid", 123)
  212. expected := "test-uuid:123"
  213. if result != expected {
  214. t.Errorf("expected %s, got %s", expected, result)
  215. }
  216. // 验证往返一致性
  217. uuid, accID, ok := ParseGeminiSessionValue(result)
  218. if !ok {
  219. t.Error("ParseGeminiSessionValue failed on formatted value")
  220. }
  221. if uuid != "test-uuid" || accID != 123 {
  222. t.Errorf("round-trip failed: uuid=%s, accID=%d", uuid, accID)
  223. }
  224. }
  225. // splitChain 辅助函数:按 "-" 分割摘要链
  226. func splitChain(chain string) []string {
  227. if chain == "" {
  228. return nil
  229. }
  230. var parts []string
  231. start := 0
  232. for i := 0; i < len(chain); i++ {
  233. if chain[i] == '-' {
  234. parts = append(parts, chain[start:i])
  235. start = i + 1
  236. }
  237. }
  238. if start < len(chain) {
  239. parts = append(parts, chain[start:])
  240. }
  241. return parts
  242. }
  243. func TestDigestChainDifferentSysInstruction(t *testing.T) {
  244. req1 := &antigravity.GeminiRequest{
  245. SystemInstruction: &antigravity.GeminiContent{
  246. Parts: []antigravity.GeminiPart{{Text: "SYS_ORIGINAL"}},
  247. },
  248. Contents: []antigravity.GeminiContent{
  249. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  250. },
  251. }
  252. req2 := &antigravity.GeminiRequest{
  253. SystemInstruction: &antigravity.GeminiContent{
  254. Parts: []antigravity.GeminiPart{{Text: "SYS_MODIFIED"}},
  255. },
  256. Contents: []antigravity.GeminiContent{
  257. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  258. },
  259. }
  260. chain1 := BuildGeminiDigestChain(req1)
  261. chain2 := BuildGeminiDigestChain(req2)
  262. t.Logf("Chain1: %s", chain1)
  263. t.Logf("Chain2: %s", chain2)
  264. if chain1 == chain2 {
  265. t.Error("Different systemInstruction should produce different chains")
  266. }
  267. }
  268. func TestDigestChainTamperedMiddleContent(t *testing.T) {
  269. req1 := &antigravity.GeminiRequest{
  270. Contents: []antigravity.GeminiContent{
  271. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  272. {Role: "model", Parts: []antigravity.GeminiPart{{Text: "ORIGINAL_REPLY"}}},
  273. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "next"}}},
  274. },
  275. }
  276. req2 := &antigravity.GeminiRequest{
  277. Contents: []antigravity.GeminiContent{
  278. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "hello"}}},
  279. {Role: "model", Parts: []antigravity.GeminiPart{{Text: "TAMPERED_REPLY"}}},
  280. {Role: "user", Parts: []antigravity.GeminiPart{{Text: "next"}}},
  281. },
  282. }
  283. chain1 := BuildGeminiDigestChain(req1)
  284. chain2 := BuildGeminiDigestChain(req2)
  285. t.Logf("Chain1: %s", chain1)
  286. t.Logf("Chain2: %s", chain2)
  287. if chain1 == chain2 {
  288. t.Error("Tampered middle content should produce different chains")
  289. }
  290. // 验证第一个 user 的 hash 相同
  291. parts1 := splitChain(chain1)
  292. parts2 := splitChain(chain2)
  293. if parts1[0] != parts2[0] {
  294. t.Error("First user message hash should be the same")
  295. }
  296. if parts1[1] == parts2[1] {
  297. t.Error("Model reply hash should be different")
  298. }
  299. }
  300. func TestGenerateGeminiDigestSessionKey(t *testing.T) {
  301. tests := []struct {
  302. name string
  303. prefixHash string
  304. uuid string
  305. want string
  306. }{
  307. {
  308. name: "normal 16 char hash with uuid",
  309. prefixHash: "abcdefgh12345678",
  310. uuid: "550e8400-e29b-41d4-a716-446655440000",
  311. want: "gemini:digest:abcdefgh:550e8400",
  312. },
  313. {
  314. name: "exactly 8 chars prefix and uuid",
  315. prefixHash: "12345678",
  316. uuid: "abcdefgh",
  317. want: "gemini:digest:12345678:abcdefgh",
  318. },
  319. {
  320. name: "short hash and short uuid (less than 8)",
  321. prefixHash: "abc",
  322. uuid: "xyz",
  323. want: "gemini:digest:abc:xyz",
  324. },
  325. {
  326. name: "empty hash and uuid",
  327. prefixHash: "",
  328. uuid: "",
  329. want: "gemini:digest::",
  330. },
  331. {
  332. name: "normal prefix with short uuid",
  333. prefixHash: "abcdefgh12345678",
  334. uuid: "short",
  335. want: "gemini:digest:abcdefgh:short",
  336. },
  337. }
  338. for _, tt := range tests {
  339. t.Run(tt.name, func(t *testing.T) {
  340. got := GenerateGeminiDigestSessionKey(tt.prefixHash, tt.uuid)
  341. if got != tt.want {
  342. t.Errorf("GenerateGeminiDigestSessionKey(%q, %q) = %q, want %q", tt.prefixHash, tt.uuid, got, tt.want)
  343. }
  344. })
  345. }
  346. // 验证确定性:相同输入产生相同输出
  347. t.Run("deterministic", func(t *testing.T) {
  348. hash := "testprefix123456"
  349. uuid := "test-uuid-12345"
  350. result1 := GenerateGeminiDigestSessionKey(hash, uuid)
  351. result2 := GenerateGeminiDigestSessionKey(hash, uuid)
  352. if result1 != result2 {
  353. t.Errorf("GenerateGeminiDigestSessionKey not deterministic: %s vs %s", result1, result2)
  354. }
  355. })
  356. // 验证不同 uuid 产生不同 sessionKey(负载均衡核心逻辑)
  357. t.Run("different uuid different key", func(t *testing.T) {
  358. hash := "sameprefix123456"
  359. uuid1 := "uuid0001-session-a"
  360. uuid2 := "uuid0002-session-b"
  361. result1 := GenerateGeminiDigestSessionKey(hash, uuid1)
  362. result2 := GenerateGeminiDigestSessionKey(hash, uuid2)
  363. if result1 == result2 {
  364. t.Errorf("Different UUIDs should produce different session keys: %s vs %s", result1, result2)
  365. }
  366. })
  367. }