mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-11-02 00:30:32 +00:00
invalid_encrypted_content 恢复此前只清理当次请求体:同一失效密文随客户 端维护的会话历史在后续每一轮重新出现,反复触发"整包被拒→剥离→重试/ 重连"(#6382 生产观测 2h 内 251 次 recovery、273 次 WS 重连),既是 QoS 放大器也是内存压力放大器。 按会话记录被上游拒绝过的 encrypted_content SHA-256 摘要(lineage): - OpenAIWSStateStore 新增会话级摘要绑定,TTL 复用 session sticky, 单会话 512 摘要为存储自保护,超出仅退化为既有单次 recovery - 写点覆盖 forward WSv2 recovery、forward 非 WS 400 重试、ingress error 事件、bridge 400/流内 error;读点在 forward 进场与 ingress/ bridge turn 循环入口按摘要预剥离,新密文摘要不同不受影响 - 只收剥离端可处理的项类型(reasoning/compaction/compaction_summary), 避免记入永远剥不掉的摘要后每轮空转解码 - replay 历史序列同步剥离,避免与已剥离的当前 input 项错位导致 prefix 复用失配 含端到端集成测试:turn1 上游拒绝即记录,turn2 上游收到的请求不再携带 失效密文且 reasoning 骨架保留。
217 lines
8.0 KiB
Go
217 lines
8.0 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestCollectOpenAIEncryptedContentDigestsRaw(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
digests := collectOpenAIEncryptedContentDigestsRaw([]byte(`{"input":[
|
|
{"type":"reasoning","encrypted_content":"cipher-a"},
|
|
{"type":"compaction","encrypted_content":"cipher-b"},
|
|
{"type":"input_text","text":"hello"},
|
|
{"type":"reasoning"},
|
|
{"type":"message","encrypted_content":"cipher-alien"}
|
|
]}`))
|
|
// 剥离端处理不了的类型(message)不收摘要,否则该摘要永远命中却剥不掉。
|
|
require.Equal(t, []string{
|
|
openAIEncryptedContentDigest("cipher-a"),
|
|
openAIEncryptedContentDigest("cipher-b"),
|
|
}, digests)
|
|
|
|
require.Nil(t, collectOpenAIEncryptedContentDigestsRaw([]byte(`{"model":"gpt-5"}`)))
|
|
}
|
|
|
|
func TestStripOpenAIInvalidEncryptedContentItems(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalid := map[string]struct{}{
|
|
openAIEncryptedContentDigest("stale-cipher"): {},
|
|
}
|
|
|
|
t.Run("only_strips_matching_items", func(t *testing.T) {
|
|
t.Parallel()
|
|
reqBody := map[string]any{
|
|
"input": []any{
|
|
map[string]any{"type": "reasoning", "id": "rs_1", "encrypted_content": "stale-cipher", "summary": []any{}},
|
|
map[string]any{"type": "reasoning", "id": "rs_2", "encrypted_content": "fresh-cipher"},
|
|
map[string]any{"type": "input_text", "text": "hello"},
|
|
},
|
|
}
|
|
stripped := stripOpenAIInvalidEncryptedContentItems(reqBody, invalid)
|
|
require.Equal(t, 1, stripped)
|
|
input, ok := reqBody["input"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, input, 3)
|
|
first, ok := input[0].(map[string]any)
|
|
require.True(t, ok)
|
|
_, hasEncrypted := first["encrypted_content"]
|
|
require.False(t, hasEncrypted, "命中项应剥离 encrypted_content")
|
|
require.Equal(t, "rs_1", first["id"], "reasoning 骨架应保留")
|
|
second, ok := input[1].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "fresh-cipher", second["encrypted_content"], "新密文不得误删")
|
|
})
|
|
|
|
t.Run("compaction_removed_entirely", func(t *testing.T) {
|
|
t.Parallel()
|
|
reqBody := map[string]any{
|
|
"input": []any{
|
|
map[string]any{"type": "compaction", "encrypted_content": "stale-cipher"},
|
|
map[string]any{"type": "input_text", "text": "hello"},
|
|
},
|
|
}
|
|
stripped := stripOpenAIInvalidEncryptedContentItems(reqBody, invalid)
|
|
require.Equal(t, 1, stripped)
|
|
input, ok := reqBody["input"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, input, 1)
|
|
survivor, ok := input[0].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "input_text", survivor["type"])
|
|
})
|
|
|
|
t.Run("no_hit_no_change", func(t *testing.T) {
|
|
t.Parallel()
|
|
reqBody := map[string]any{
|
|
"input": []any{
|
|
map[string]any{"type": "reasoning", "encrypted_content": "fresh-cipher"},
|
|
},
|
|
}
|
|
require.Zero(t, stripOpenAIInvalidEncryptedContentItems(reqBody, invalid))
|
|
})
|
|
}
|
|
|
|
func TestStripOpenAIInvalidEncryptedContentFromReplayItems(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalid := map[string]struct{}{
|
|
openAIEncryptedContentDigest("stale-cipher"): {},
|
|
}
|
|
|
|
t.Run("rewrites_hit_items_and_shares_the_rest", func(t *testing.T) {
|
|
t.Parallel()
|
|
original := json.RawMessage(`{"type":"reasoning","id":"rs_1","encrypted_content":"stale-cipher"}`)
|
|
clean := json.RawMessage(`{"type":"input_text","text":"hello"}`)
|
|
compaction := json.RawMessage(`{"type":"compaction","encrypted_content":"stale-cipher"}`)
|
|
items := []json.RawMessage{original, clean, compaction}
|
|
|
|
next, stripped := stripOpenAIInvalidEncryptedContentFromReplayItems(items, invalid)
|
|
require.Equal(t, 2, stripped)
|
|
require.Len(t, next, 2, "compaction 命中项应整项删除")
|
|
var rewritten map[string]any
|
|
require.NoError(t, json.Unmarshal(next[0], &rewritten))
|
|
require.Equal(t, "rs_1", rewritten["id"])
|
|
_, hasEncrypted := rewritten["encrypted_content"]
|
|
require.False(t, hasEncrypted)
|
|
// 未命中项正文共享;原命中项正文不被修改(整体替换语义)。
|
|
require.Same(t, &clean[0], &next[1][0])
|
|
require.Contains(t, string(original), "stale-cipher")
|
|
})
|
|
|
|
t.Run("miss_returns_original_header", func(t *testing.T) {
|
|
t.Parallel()
|
|
items := []json.RawMessage{json.RawMessage(`{"type":"reasoning","encrypted_content":"fresh-cipher"}`)}
|
|
next, stripped := stripOpenAIInvalidEncryptedContentFromReplayItems(items, invalid)
|
|
require.Zero(t, stripped)
|
|
require.Same(t, &items[0][0], &next[0][0])
|
|
})
|
|
}
|
|
|
|
func TestStripOpenAIInvalidEncryptedContentRaw(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
invalid := map[string]struct{}{
|
|
openAIEncryptedContentDigest("stale-cipher"): {},
|
|
}
|
|
payload := []byte(`{"model":"gpt-5","input":[
|
|
{"type":"reasoning","id":"rs_1","encrypted_content":"stale-cipher"},
|
|
{"type":"reasoning","id":"rs_2","encrypted_content":"fresh-cipher"},
|
|
{"type":"input_text","text":"hello"}
|
|
],"stream":true}`)
|
|
|
|
stripped, count, err := stripOpenAIInvalidEncryptedContentRaw(payload, invalid)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, count)
|
|
var decoded map[string]any
|
|
require.NoError(t, json.Unmarshal(stripped, &decoded))
|
|
require.Equal(t, "gpt-5", decoded["model"])
|
|
input, ok := decoded["input"].([]any)
|
|
require.True(t, ok)
|
|
require.Len(t, input, 3)
|
|
first, ok := input[0].(map[string]any)
|
|
require.True(t, ok)
|
|
_, hasEncrypted := first["encrypted_content"]
|
|
require.False(t, hasEncrypted)
|
|
second, ok := input[1].(map[string]any)
|
|
require.True(t, ok)
|
|
require.Equal(t, "fresh-cipher", second["encrypted_content"])
|
|
|
|
t.Run("miss_fast_path_returns_original", func(t *testing.T) {
|
|
t.Parallel()
|
|
cleanPayload := []byte(`{"input":[{"type":"reasoning","encrypted_content":"fresh-cipher"}]}`)
|
|
unchanged, count, err := stripOpenAIInvalidEncryptedContentRaw(cleanPayload, invalid)
|
|
require.NoError(t, err)
|
|
require.Zero(t, count)
|
|
require.Same(t, &cleanPayload[0], &unchanged[0], "未命中时应原样返回,不重编码")
|
|
})
|
|
}
|
|
|
|
func TestOpenAIWSStateStoreInvalidEncryptedContentLineage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("mark_get_roundtrip_with_merge", func(t *testing.T) {
|
|
t.Parallel()
|
|
store := NewOpenAIWSStateStore(nil)
|
|
require.False(t, store.HasAnySessionInvalidEncryptedContent())
|
|
require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-a"))
|
|
|
|
store.MarkSessionInvalidEncryptedContent(1, "session-a", []string{"d1", "d2"}, time.Minute)
|
|
store.MarkSessionInvalidEncryptedContent(1, "session-a", []string{"d2", "d3"}, time.Minute)
|
|
require.True(t, store.HasAnySessionInvalidEncryptedContent())
|
|
|
|
digests := store.GetSessionInvalidEncryptedContentDigests(1, "session-a")
|
|
require.Len(t, digests, 3)
|
|
for _, digest := range []string{"d1", "d2", "d3"} {
|
|
require.Contains(t, digests, digest)
|
|
}
|
|
// 组隔离:另一组同名会话不可见。
|
|
require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(2, "session-a"))
|
|
// 返回值是拷贝,调用方修改不得影响存储。
|
|
digests["d4"] = struct{}{}
|
|
require.Len(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-a"), 3)
|
|
})
|
|
|
|
t.Run("expired_binding_not_returned", func(t *testing.T) {
|
|
t.Parallel()
|
|
raw := NewOpenAIWSStateStore(nil)
|
|
raw.MarkSessionInvalidEncryptedContent(1, "session-b", []string{"d1"}, time.Minute)
|
|
store, ok := raw.(*defaultOpenAIWSStateStore)
|
|
require.True(t, ok)
|
|
store.sessionInvalidEncryptedMu.Lock()
|
|
binding := store.sessionInvalidEncrypted["1:session-b"]
|
|
binding.expiresAt = time.Now().Add(-time.Second)
|
|
store.sessionInvalidEncrypted["1:session-b"] = binding
|
|
store.sessionInvalidEncryptedMu.Unlock()
|
|
require.Nil(t, store.GetSessionInvalidEncryptedContentDigests(1, "session-b"))
|
|
})
|
|
|
|
t.Run("per_session_capacity_degrades_gracefully", func(t *testing.T) {
|
|
t.Parallel()
|
|
store := NewOpenAIWSStateStore(nil)
|
|
oversized := make([]string, 0, openAIWSInvalidEncryptedDigestsPerSession+10)
|
|
for i := range openAIWSInvalidEncryptedDigestsPerSession + 10 {
|
|
oversized = append(oversized, openAIEncryptedContentDigest(fmt.Sprintf("cipher-%d", i)))
|
|
}
|
|
store.MarkSessionInvalidEncryptedContent(1, "session-c", oversized, time.Minute)
|
|
digests := store.GetSessionInvalidEncryptedContentDigests(1, "session-c")
|
|
require.Len(t, digests, openAIWSInvalidEncryptedDigestsPerSession)
|
|
})
|
|
}
|