openai_encrypted_content_lineage_test.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "testing"
  6. "time"
  7. "github.com/stretchr/testify/require"
  8. )
  9. func TestCollectOpenAIEncryptedContentDigestsRaw(t *testing.T) {
  10. t.Parallel()
  11. digests := collectOpenAIEncryptedContentDigestsRaw([]byte(`{"input":[
  12. {"type":"reasoning","encrypted_content":"cipher-a"},
  13. {"type":"compaction","encrypted_content":"cipher-b"},
  14. {"type":"input_text","text":"hello"},
  15. {"type":"reasoning"},
  16. {"type":"message","encrypted_content":"cipher-alien"}
  17. ]}`))
  18. // 剥离端处理不了的类型(message)不收摘要,否则该摘要永远命中却剥不掉。
  19. require.Equal(t, []string{
  20. openAIEncryptedContentDigest("cipher-a"),
  21. openAIEncryptedContentDigest("cipher-b"),
  22. }, digests)
  23. require.Nil(t, collectOpenAIEncryptedContentDigestsRaw([]byte(`{"model":"gpt-5"}`)))
  24. }
  25. func TestStripOpenAIInvalidEncryptedContentItems(t *testing.T) {
  26. t.Parallel()
  27. invalid := map[string]struct{}{
  28. openAIEncryptedContentDigest("stale-cipher"): {},
  29. }
  30. t.Run("only_strips_matching_items", func(t *testing.T) {
  31. t.Parallel()
  32. reqBody := map[string]any{
  33. "input": []any{
  34. map[string]any{"type": "reasoning", "id": "rs_1", "encrypted_content": "stale-cipher", "summary": []any{}},
  35. map[string]any{"type": "reasoning", "id": "rs_2", "encrypted_content": "fresh-cipher"},
  36. map[string]any{"type": "input_text", "text": "hello"},
  37. },
  38. }
  39. stripped := stripOpenAIInvalidEncryptedContentItems(reqBody, invalid)
  40. require.Equal(t, 1, stripped)
  41. input, ok := reqBody["input"].([]any)
  42. require.True(t, ok)
  43. require.Len(t, input, 3)
  44. first, ok := input[0].(map[string]any)
  45. require.True(t, ok)
  46. _, hasEncrypted := first["encrypted_content"]
  47. require.False(t, hasEncrypted, "命中项应剥离 encrypted_content")
  48. require.Equal(t, "rs_1", first["id"], "reasoning 骨架应保留")
  49. second, ok := input[1].(map[string]any)
  50. require.True(t, ok)
  51. require.Equal(t, "fresh-cipher", second["encrypted_content"], "新密文不得误删")
  52. })
  53. t.Run("compaction_removed_entirely", func(t *testing.T) {
  54. t.Parallel()
  55. reqBody := map[string]any{
  56. "input": []any{
  57. map[string]any{"type": "compaction", "encrypted_content": "stale-cipher"},
  58. map[string]any{"type": "input_text", "text": "hello"},
  59. },
  60. }
  61. stripped := stripOpenAIInvalidEncryptedContentItems(reqBody, invalid)
  62. require.Equal(t, 1, stripped)
  63. input, ok := reqBody["input"].([]any)
  64. require.True(t, ok)
  65. require.Len(t, input, 1)
  66. survivor, ok := input[0].(map[string]any)
  67. require.True(t, ok)
  68. require.Equal(t, "input_text", survivor["type"])
  69. })
  70. t.Run("no_hit_no_change", func(t *testing.T) {
  71. t.Parallel()
  72. reqBody := map[string]any{
  73. "input": []any{
  74. map[string]any{"type": "reasoning", "encrypted_content": "fresh-cipher"},
  75. },
  76. }
  77. require.Zero(t, stripOpenAIInvalidEncryptedContentItems(reqBody, invalid))
  78. })
  79. }
  80. func TestStripOpenAIInvalidEncryptedContentFromReplayItems(t *testing.T) {
  81. t.Parallel()
  82. invalid := map[string]struct{}{
  83. openAIEncryptedContentDigest("stale-cipher"): {},
  84. }
  85. t.Run("rewrites_hit_items_and_shares_the_rest", func(t *testing.T) {
  86. t.Parallel()
  87. original := json.RawMessage(`{"type":"reasoning","id":"rs_1","encrypted_content":"stale-cipher"}`)
  88. clean := json.RawMessage(`{"type":"input_text","text":"hello"}`)
  89. compaction := json.RawMessage(`{"type":"compaction","encrypted_content":"stale-cipher"}`)
  90. items := []json.RawMessage{original, clean, compaction}
  91. next, stripped := stripOpenAIInvalidEncryptedContentFromReplayItems(items, invalid)
  92. require.Equal(t, 2, stripped)
  93. require.Len(t, next, 2, "compaction 命中项应整项删除")
  94. var rewritten map[string]any
  95. require.NoError(t, json.Unmarshal(next[0], &rewritten))
  96. require.Equal(t, "rs_1", rewritten["id"])
  97. _, hasEncrypted := rewritten["encrypted_content"]
  98. require.False(t, hasEncrypted)
  99. // 未命中项正文共享;原命中项正文不被修改(整体替换语义)。
  100. require.Same(t, &clean[0], &next[1][0])
  101. require.Contains(t, string(original), "stale-cipher")
  102. })
  103. t.Run("miss_returns_original_header", func(t *testing.T) {
  104. t.Parallel()
  105. items := []json.RawMessage{json.RawMessage(`{"type":"reasoning","encrypted_content":"fresh-cipher"}`)}
  106. next, stripped := stripOpenAIInvalidEncryptedContentFromReplayItems(items, invalid)
  107. require.Zero(t, stripped)
  108. require.Same(t, &items[0][0], &next[0][0])
  109. })
  110. }
  111. func TestStripOpenAIInvalidEncryptedContentRaw(t *testing.T) {
  112. t.Parallel()
  113. invalid := map[string]struct{}{
  114. openAIEncryptedContentDigest("stale-cipher"): {},
  115. }
  116. payload := []byte(`{"model":"gpt-5","input":[
  117. {"type":"reasoning","id":"rs_1","encrypted_content":"stale-cipher"},
  118. {"type":"reasoning","id":"rs_2","encrypted_content":"fresh-cipher"},
  119. {"type":"input_text","text":"hello"}
  120. ],"stream":true}`)
  121. stripped, count, err := stripOpenAIInvalidEncryptedContentRaw(payload, invalid)
  122. require.NoError(t, err)
  123. require.Equal(t, 1, count)
  124. var decoded map[string]any
  125. require.NoError(t, json.Unmarshal(stripped, &decoded))
  126. require.Equal(t, "gpt-5", decoded["model"])
  127. input, ok := decoded["input"].([]any)
  128. require.True(t, ok)
  129. require.Len(t, input, 3)
  130. first, ok := input[0].(map[string]any)
  131. require.True(t, ok)
  132. _, hasEncrypted := first["encrypted_content"]
  133. require.False(t, hasEncrypted)
  134. second, ok := input[1].(map[string]any)
  135. require.True(t, ok)
  136. require.Equal(t, "fresh-cipher", second["encrypted_content"])
  137. t.Run("miss_fast_path_returns_original", func(t *testing.T) {
  138. t.Parallel()
  139. cleanPayload := []byte(`{"input":[{"type":"reasoning","encrypted_content":"fresh-cipher"}]}`)
  140. unchanged, count, err := stripOpenAIInvalidEncryptedContentRaw(cleanPayload, invalid)
  141. require.NoError(t, err)
  142. require.Zero(t, count)
  143. require.Same(t, &cleanPayload[0], &unchanged[0], "未命中时应原样返回,不重编码")
  144. })
  145. }
  146. func TestOpenAIWSStateStoreInvalidEncryptedContentLineage(t *testing.T) {
  147. t.Parallel()
  148. t.Run("mark_get_roundtrip_with_merge", func(t *testing.T) {
  149. t.Parallel()
  150. store := NewOpenAIWSStateStore(nil)
  151. require.False(t, store.HasAnySessionInvalidEncryptedContent())
  152. require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-a"))
  153. store.MarkSessionInvalidEncryptedContent(1, "session-a", []string{"d1", "d2"}, time.Minute)
  154. store.MarkSessionInvalidEncryptedContent(1, "session-a", []string{"d2", "d3"}, time.Minute)
  155. require.True(t, store.HasAnySessionInvalidEncryptedContent())
  156. digests := store.GetSessionInvalidEncryptedContentDigests(1, "session-a")
  157. require.Len(t, digests, 3)
  158. for _, digest := range []string{"d1", "d2", "d3"} {
  159. require.Contains(t, digests, digest)
  160. }
  161. // 组隔离:另一组同名会话不可见。
  162. require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(2, "session-a"))
  163. // 返回值是拷贝,调用方修改不得影响存储。
  164. digests["d4"] = struct{}{}
  165. require.Len(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-a"), 3)
  166. })
  167. t.Run("expired_binding_not_returned", func(t *testing.T) {
  168. t.Parallel()
  169. raw := NewOpenAIWSStateStore(nil)
  170. raw.MarkSessionInvalidEncryptedContent(1, "session-b", []string{"d1"}, time.Minute)
  171. store, ok := raw.(*defaultOpenAIWSStateStore)
  172. require.True(t, ok)
  173. store.sessionInvalidEncryptedMu.Lock()
  174. binding := store.sessionInvalidEncrypted["1:session-b"]
  175. binding.expiresAt = time.Now().Add(-time.Second)
  176. store.sessionInvalidEncrypted["1:session-b"] = binding
  177. store.sessionInvalidEncryptedMu.Unlock()
  178. require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-b"))
  179. })
  180. t.Run("per_session_capacity_degrades_gracefully", func(t *testing.T) {
  181. t.Parallel()
  182. store := NewOpenAIWSStateStore(nil)
  183. oversized := make([]string, 0, openAIWSInvalidEncryptedDigestsPerSession+10)
  184. for i := range openAIWSInvalidEncryptedDigestsPerSession + 10 {
  185. oversized = append(oversized, openAIEncryptedContentDigest(fmt.Sprintf("cipher-%d", i)))
  186. }
  187. store.MarkSessionInvalidEncryptedContent(1, "session-c", oversized, time.Minute)
  188. digests := store.GetSessionInvalidEncryptedContentDigests(1, "session-c")
  189. require.Len(t, digests, openAIWSInvalidEncryptedDigestsPerSession)
  190. })
  191. }