mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
245 lines
9.4 KiB
Go
245 lines
9.4 KiB
Go
package service
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newOpenAIWSExecutionScopeTestContext(headers map[string]string) *gin.Context {
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1/responses", nil)
|
|
for k, v := range headers {
|
|
c.Request.Header.Set(k, v)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func TestResolveOpenAIWSClientThreadID(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
cases := []struct {
|
|
name string
|
|
headers map[string]string
|
|
body string
|
|
want string
|
|
}{
|
|
{
|
|
name: "thread-id header wins",
|
|
headers: map[string]string{
|
|
openAIWSThreadIDHeader: " t-plain ",
|
|
openAIWSTurnMetadataHeader: `{"session_id":"s","thread_id":"t-header"}`,
|
|
openAIWSWindowIDHeader: "t-window:0",
|
|
},
|
|
body: `{"client_metadata":{"thread_id":"t-body"}}`,
|
|
want: "t-plain",
|
|
},
|
|
{
|
|
name: "turn metadata header second",
|
|
headers: map[string]string{
|
|
openAIWSTurnMetadataHeader: `{"session_id":"s","thread_id":"t-header"}`,
|
|
openAIWSWindowIDHeader: "t-window:0",
|
|
},
|
|
body: `{"client_metadata":{"thread_id":"t-body"}}`,
|
|
want: "t-header",
|
|
},
|
|
{
|
|
name: "window id fallback",
|
|
headers: map[string]string{openAIWSWindowIDHeader: " t-window:0 "},
|
|
body: `{"client_metadata":{"thread_id":"t-body"}}`,
|
|
want: "t-window",
|
|
},
|
|
{
|
|
name: "body client_metadata.thread_id",
|
|
body: `{"client_metadata":{"thread_id":"t-body"}}`,
|
|
want: "t-body",
|
|
},
|
|
{
|
|
name: "body embedded turn metadata",
|
|
body: `{"client_metadata":{"x-codex-turn-metadata":"{\"thread_id\":\"t-embedded\"}"}}`,
|
|
want: "t-embedded",
|
|
},
|
|
{
|
|
name: "malformed header ignored",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: "{not json"},
|
|
body: `{}`,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "none",
|
|
body: `{"input":"hi"}`,
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := resolveOpenAIWSClientThreadID(newOpenAIWSExecutionScopeTestContext(tc.headers), []byte(tc.body))
|
|
require.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
require.Equal(t, "t", resolveOpenAIWSClientThreadID(nil, []byte(`{"client_metadata":{"thread_id":"t"}}`)))
|
|
}
|
|
|
|
func TestResolveOpenAIWSExecutionLane(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
cases := []struct {
|
|
name string
|
|
headers map[string]string
|
|
body string
|
|
want string
|
|
}{
|
|
{
|
|
name: "turn is the main lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"turn"}`},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "prewarm shares the turn lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"prewarm","thread_source":"user"}`},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "compaction shares the turn lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"compaction"}`},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "missing request_kind is the main lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t"}`},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "memory consolidation gets its own lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"memory","thread_source":"memory_consolidation"}`},
|
|
want: "kind=memory",
|
|
},
|
|
{
|
|
name: "request_kind is normalized",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":" Memory "}`},
|
|
want: "kind=memory",
|
|
},
|
|
{
|
|
name: "unknown kind never joins the turn lane",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"future"}`},
|
|
want: "kind=future",
|
|
},
|
|
{
|
|
name: "header metadata wins over body metadata",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: `{"thread_id":"t","request_kind":"turn"}`},
|
|
body: `{"client_metadata":{"x-codex-turn-metadata":"{\"thread_id\":\"t\",\"request_kind\":\"memory\"}"}}`,
|
|
want: "",
|
|
},
|
|
{
|
|
name: "body embedded metadata used when header absent",
|
|
body: `{"client_metadata":{"x-codex-turn-metadata":"{\"thread_id\":\"t\",\"request_kind\":\"memory\"}"}}`,
|
|
want: "kind=memory",
|
|
},
|
|
{
|
|
name: "malformed header falls back to body",
|
|
headers: map[string]string{openAIWSTurnMetadataHeader: "{not json"},
|
|
body: `{"client_metadata":{"x-codex-turn-metadata":"{\"request_kind\":\"memory\"}"}}`,
|
|
want: "kind=memory",
|
|
},
|
|
{
|
|
name: "subagent without thread metadata gets its own lane",
|
|
headers: map[string]string{openAIWSThreadIDHeader: "t", openAISubagentHeader: "Guardian"},
|
|
want: "subagent=guardian",
|
|
},
|
|
{
|
|
name: "subagent thread with its own metadata stays in the turn lane",
|
|
headers: map[string]string{
|
|
openAIWSThreadIDHeader: "child",
|
|
openAISubagentHeader: "guardian",
|
|
openAIWSTurnMetadataHeader: `{"thread_id":"child","request_kind":"turn","subagent_kind":"guardian"}`,
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
name: "subagent declared in body client_metadata",
|
|
body: `{"client_metadata":{"x-openai-subagent":"guardian"}}`,
|
|
want: "subagent=guardian",
|
|
},
|
|
{
|
|
name: "none",
|
|
body: `{"input":"hi"}`,
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := resolveOpenAIWSExecutionLane(newOpenAIWSExecutionScopeTestContext(tc.headers), []byte(tc.body))
|
|
require.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
require.Equal(t, "kind=memory", resolveOpenAIWSExecutionLane(nil, []byte(`{"client_metadata":{"x-codex-turn-metadata":"{\"request_kind\":\"memory\"}"}}`)))
|
|
}
|
|
|
|
func TestResolveOpenAIWSExecutionScope(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
threadHeaders := map[string]string{
|
|
"session-id": "root-session",
|
|
openAIWSTurnMetadataHeader: `{"session_id":"root-session","thread_id":"thread-a"}`,
|
|
}
|
|
bodyFirst := []byte(`{"type":"response.create","model":"gpt-5.1","instructions":"sys","input":[{"role":"user","content":"first"}]}`)
|
|
bodyRetry := []byte(`{"type":"response.create","model":"gpt-5.1","instructions":"sys","input":[{"role":"user","content":"first"},{"role":"assistant","content":"x"},{"role":"user","content":"second"}]}`)
|
|
|
|
scopeOf := func(headers map[string]string, body []byte, apiKeyID int64) string {
|
|
scope, _ := resolveOpenAIWSExecutionScope(newOpenAIWSExecutionScopeTestContext(headers), body, apiKeyID)
|
|
return scope
|
|
}
|
|
|
|
byThread, threadID := resolveOpenAIWSExecutionScope(newOpenAIWSExecutionScopeTestContext(threadHeaders), bodyFirst, 11)
|
|
require.Len(t, byThread, 16)
|
|
require.Equal(t, "thread-a", threadID)
|
|
require.Equal(t, byThread, scopeOf(threadHeaders, bodyRetry, 11), "同线程重连的作用域不得随请求内容变化")
|
|
|
|
otherThread := map[string]string{
|
|
"session-id": "root-session",
|
|
openAIWSTurnMetadataHeader: `{"session_id":"root-session","thread_id":"thread-b"}`,
|
|
}
|
|
require.NotEqual(t, byThread, scopeOf(otherThread, bodyFirst, 11))
|
|
require.NotEqual(t, byThread, scopeOf(threadHeaders, bodyFirst, 12), "API key 必须参与隔离")
|
|
|
|
memoryHeaders := map[string]string{
|
|
"session-id": "root-session",
|
|
openAIWSTurnMetadataHeader: `{"session_id":"root-session","thread_id":"thread-a","request_kind":"memory","thread_source":"memory_consolidation"}`,
|
|
}
|
|
byMemory, memoryThread := resolveOpenAIWSExecutionScope(newOpenAIWSExecutionScopeTestContext(memoryHeaders), bodyFirst, 11)
|
|
require.Len(t, byMemory, 16)
|
|
require.Equal(t, "thread-a", memoryThread)
|
|
require.NotEqual(t, byThread, byMemory, "同线程的记忆整理请求不得与用户 turn 同键")
|
|
require.Equal(t, byMemory, scopeOf(memoryHeaders, bodyRetry, 11), "记忆整理重连仍命中同一键")
|
|
for _, kind := range []string{"turn", "prewarm", "compaction"} {
|
|
sameLane := map[string]string{
|
|
"session-id": "root-session",
|
|
openAIWSTurnMetadataHeader: `{"session_id":"root-session","thread_id":"thread-a","request_kind":"` + kind + `"}`,
|
|
}
|
|
require.Equal(t, byThread, scopeOf(sameLane, bodyFirst, 11), kind+" 必须与用户 turn 同键")
|
|
}
|
|
require.Equal(t, byThread, scopeOf(map[string]string{openAIWSThreadIDHeader: "thread-a"}, bodyFirst, 11), "只有 thread-id 头时仍是主道")
|
|
byScorer := scopeOf(map[string]string{openAIWSThreadIDHeader: "thread-a", openAISubagentHeader: "guardian"}, bodyFirst, 11)
|
|
require.Len(t, byScorer, 16)
|
|
require.NotEqual(t, byThread, byScorer, "无线程元数据的子智能体请求不得与用户 turn 同键")
|
|
require.NotEqual(t, byMemory, byScorer)
|
|
|
|
sessionOnly := map[string]string{"session-id": "root-session"}
|
|
bySession, threadID := resolveOpenAIWSExecutionScope(newOpenAIWSExecutionScopeTestContext(sessionOnly), bodyFirst, 11)
|
|
require.Len(t, bySession, 16)
|
|
require.Equal(t, "", threadID)
|
|
require.NotEqual(t, byThread, bySession, "线程标识优先于会话标识")
|
|
require.Equal(t, bySession, scopeOf(sessionOnly, bodyRetry, 11))
|
|
require.NotEqual(t, bySession, scopeOf(sessionOnly, bodyFirst, 12))
|
|
sessionMemory := []byte(`{"type":"response.create","input":"hi","client_metadata":{"x-codex-turn-metadata":"{\"request_kind\":\"memory\"}"}}`)
|
|
require.NotEqual(t, bySession, scopeOf(sessionOnly, sessionMemory, 11), "只有会话标识时记忆整理也单独成道")
|
|
|
|
byCacheKey := scopeOf(nil, []byte(`{"type":"response.create","prompt_cache_key":"cache-1","input":"hi"}`), 11)
|
|
require.Len(t, byCacheKey, 16)
|
|
|
|
require.Equal(t, "", scopeOf(nil, bodyFirst, 11), "只有请求内容时不得产生执行作用域")
|
|
nilScope, nilThread := resolveOpenAIWSExecutionScope(nil, bodyFirst, 11)
|
|
require.Equal(t, "", nilScope)
|
|
require.Equal(t, "", nilThread)
|
|
}
|