openai_codex_fingerprint_test.go 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "net/http/httptest"
  7. "testing"
  8. "github.com/gin-gonic/gin"
  9. "github.com/google/uuid"
  10. "github.com/stretchr/testify/assert"
  11. "github.com/stretchr/testify/require"
  12. )
  13. const testCodexFingerprintSeed = "11111111-1111-4111-8111-111111111111"
  14. func newTestOAuthAccount(id int64, extra map[string]any) *Account {
  15. if codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(extra)) {
  16. if extra == nil {
  17. extra = make(map[string]any)
  18. }
  19. if _, exists := extra[codexFingerprintSeedExtraKey]; !exists {
  20. extra[codexFingerprintSeedExtraKey] = testCodexFingerprintSeed
  21. }
  22. }
  23. return &Account{
  24. ID: id,
  25. Platform: PlatformOpenAI,
  26. Type: AccountTypeOAuth,
  27. Extra: extra,
  28. }
  29. }
  30. // --- deriveStableUUIDv4 ---
  31. func TestDeriveStableUUIDv4_Deterministic(t *testing.T) {
  32. a := deriveStableUUIDv4("test-seed-1")
  33. b := deriveStableUUIDv4("test-seed-1")
  34. assert.Equal(t, a, b, "同一种子应返回相同结果")
  35. }
  36. func TestDeriveStableUUIDv4_DifferentSeeds(t *testing.T) {
  37. a := deriveStableUUIDv4("seed-a")
  38. b := deriveStableUUIDv4("seed-b")
  39. assert.NotEqual(t, a, b, "不同种子应返回不同结果")
  40. }
  41. func TestDeriveStableUUIDv4_ValidFormat(t *testing.T) {
  42. result := deriveStableUUIDv4("test-seed")
  43. parsed, err := uuid.Parse(result)
  44. require.NoError(t, err, "应返回合法 UUID 格式")
  45. assert.Equal(t, uuid.Version(4), parsed.Version(), "应为 UUIDv4")
  46. assert.Equal(t, uuid.RFC4122, parsed.Variant(), "应为 RFC4122 变体")
  47. }
  48. // --- GetCodexFingerprintMode ---
  49. func TestGetCodexFingerprintMode(t *testing.T) {
  50. tests := []struct {
  51. name string
  52. account *Account
  53. expected codexFingerprintMode
  54. }{
  55. {"nil 账号", nil, codexFingerprintOff},
  56. {"非 OAuth 账号", &Account{Platform: PlatformOpenAI, Type: "api_key"}, codexFingerprintOff},
  57. {"OpenAI setup token", &Account{Platform: PlatformOpenAI, Type: AccountTypeSetupToken, Extra: map[string]any{codexFingerprintModeExtraKey: "session"}}, codexFingerprintSession},
  58. {"Anthropic setup token", &Account{Platform: PlatformAnthropic, Type: AccountTypeSetupToken, Extra: map[string]any{codexFingerprintModeExtraKey: "session"}}, codexFingerprintOff},
  59. // 收敛是显式 opt-in:缺省/空/非法一律 off(#5610)。存量账号普遍没有这个
  60. // extra 键,升级不得把它们静默切进收敛。
  61. {"无 extra 默认 off", newTestOAuthAccount(1, nil), codexFingerprintOff},
  62. {"空值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: ""}), codexFingerprintOff},
  63. {"非法值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "invalid"}), codexFingerprintOff},
  64. {"显式 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"}), codexFingerprintOff},
  65. {"device", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "device"}), codexFingerprintDevice},
  66. {"session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "session"}), codexFingerprintSession},
  67. {"full", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "full"}), codexFingerprintFull},
  68. }
  69. for _, tt := range tests {
  70. t.Run(tt.name, func(t *testing.T) {
  71. assert.Equal(t, tt.expected, tt.account.GetCodexFingerprintMode())
  72. })
  73. }
  74. }
  75. // --- resolveConvergedInstallationID ---
  76. func TestResolveConvergedInstallationID_UsesDeviceID(t *testing.T) {
  77. account := newTestOAuthAccount(1, map[string]any{"openai_device_id": "real-device-id"})
  78. assert.Equal(t, "real-device-id", resolveConvergedInstallationID(account, testCodexFingerprintSeed))
  79. }
  80. func TestResolveConvergedInstallationID_DerivesFromSeed(t *testing.T) {
  81. account := newTestOAuthAccount(42, nil)
  82. result := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
  83. _, err := uuid.Parse(result)
  84. require.NoError(t, err, "派生值应为合法 UUID")
  85. assert.Equal(t, result, resolveConvergedInstallationID(account, testCodexFingerprintSeed), "确定性")
  86. }
  87. func TestResolveConvergedInstallationID_DifferentSeeds(t *testing.T) {
  88. account := newTestOAuthAccount(1, nil)
  89. a := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
  90. b := resolveConvergedInstallationID(account, "22222222-2222-4222-8222-222222222222")
  91. assert.NotEqual(t, a, b)
  92. }
  93. // --- resolveConvergedThreadID ---
  94. func TestResolveConvergedThreadID_PerClientSession(t *testing.T) {
  95. a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
  96. b := resolveConvergedThreadID(testCodexFingerprintSeed, "session-bbb")
  97. assert.NotEqual(t, a, b, "不同客户端 session 应得到不同 thread_id")
  98. }
  99. func TestResolveConvergedThreadID_Deterministic(t *testing.T) {
  100. a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
  101. b := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
  102. assert.Equal(t, a, b, "同一客户端 session 应得到相同 thread_id")
  103. }
  104. func TestResolveConvergedThreadID_EmptySession(t *testing.T) {
  105. assert.Equal(t, "", resolveConvergedThreadID(testCodexFingerprintSeed, ""))
  106. }
  107. // --- off 模式:resolveCodexFingerprintIDsFromRequest 返回 nil ---
  108. func TestResolveCodexFingerprintIDsFromRequest_ExplicitOff(t *testing.T) {
  109. account := newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"})
  110. ids := resolveCodexFingerprintIDsFromRequest(account, nil)
  111. assert.Nil(t, ids, "显式 off 模式应返回 nil")
  112. }
  113. // 未显式配置的存量账号不得被收敛(#5610):默认返回 nil,出站身份保持
  114. // v0.1.175 之前的客户端原值。
  115. func TestResolveCodexFingerprintIDsFromRequest_DefaultIsOff(t *testing.T) {
  116. account := newTestOAuthAccount(1, nil)
  117. assert.Nil(t, resolveCodexFingerprintIDsFromRequest(account, nil), "无 extra 应视为 off")
  118. }
  119. // 管理员显式 opt-in 的账号行为不变。
  120. func TestResolveCodexFingerprintIDsFromRequest_ExplicitOptInHonored(t *testing.T) {
  121. for _, mode := range []string{"device", "session", "full"} {
  122. t.Run(mode, func(t *testing.T) {
  123. account := newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: mode})
  124. ids := resolveCodexFingerprintIDsFromRequest(account, nil)
  125. require.NotNil(t, ids, "显式配置必须生效")
  126. assert.Equal(t, codexFingerprintMode(mode), ids.mode)
  127. assert.NotEmpty(t, ids.installationID)
  128. })
  129. }
  130. }
  131. func TestResolveCodexFingerprintIDsFromRequest_EnabledModesRequireValidSeed(t *testing.T) {
  132. for _, tt := range []struct {
  133. name string
  134. extra map[string]any
  135. }{
  136. {name: "missing", extra: map[string]any{codexFingerprintModeExtraKey: "device"}},
  137. {name: "missing with device override", extra: map[string]any{codexFingerprintModeExtraKey: "device", "openai_device_id": "real-device"}},
  138. {name: "blank", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: ""}},
  139. {name: "uppercase", extra: map[string]any{codexFingerprintModeExtraKey: "full", codexFingerprintSeedExtraKey: "11111111-1111-4111-8111-AAAAAAAAAAAA"}},
  140. {name: "nil uuid", extra: map[string]any{codexFingerprintModeExtraKey: "device", codexFingerprintSeedExtraKey: "00000000-0000-0000-0000-000000000000"}},
  141. {name: "non string", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: 123}},
  142. } {
  143. t.Run(tt.name, func(t *testing.T) {
  144. account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: tt.extra}
  145. require.Nil(t, resolveCodexFingerprintIDsFromRequest(account, nil))
  146. })
  147. }
  148. }
  149. // --- applyCodexFingerprintHeaders: off 模式 ---
  150. func TestApplyCodexFingerprintHeaders_OffMode(t *testing.T) {
  151. h := http.Header{}
  152. h.Set("x-codex-installation-id", "original-install-id")
  153. h.Set("x-codex-window-id", "original-window-id")
  154. applyCodexFingerprintHeaders(h, nil)
  155. assert.Equal(t, "original-install-id", h.Get("x-codex-installation-id"), "nil ids 不改写")
  156. assert.Equal(t, "original-window-id", h.Get("x-codex-window-id"), "nil ids 不改写")
  157. }
  158. // --- applyCodexFingerprintHeaders: device 模式 ---
  159. func TestApplyCodexFingerprintHeaders_DeviceMode(t *testing.T) {
  160. account := newTestOAuthAccount(1, map[string]any{
  161. codexFingerprintModeExtraKey: "device",
  162. "openai_device_id": "converged-device",
  163. })
  164. turnMetadata := `{"installation_id":"user-install","session_id":"user-session","sandbox":"seccomp"}`
  165. h := http.Header{}
  166. h.Set("x-codex-installation-id", "user-install")
  167. h.Set("x-codex-window-id", "user-window:0")
  168. h.Set("x-codex-turn-metadata", turnMetadata)
  169. ids := resolveCodexFingerprintIDsFromRequest(account, nil)
  170. applyCodexFingerprintHeaders(h, ids)
  171. assert.Equal(t, "converged-device", h.Get("x-codex-installation-id"), "installation_id 应收敛")
  172. assert.Equal(t, "user-window:0", h.Get("x-codex-window-id"), "device 模式不改写 window_id")
  173. var meta map[string]any
  174. require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
  175. assert.Equal(t, "converged-device", meta["installation_id"])
  176. assert.Equal(t, "user-session", meta["session_id"], "device 模式不改写 session_id")
  177. assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
  178. }
  179. // --- applyCodexFingerprintHeaders: session 模式 ---
  180. func TestApplyCodexFingerprintHeaders_SessionMode(t *testing.T) {
  181. account := newTestOAuthAccount(1, map[string]any{
  182. codexFingerprintModeExtraKey: "session",
  183. })
  184. clientHeaders := http.Header{}
  185. clientHeaders.Set("session-id", "client-session-aaa")
  186. turnMetadata := `{"installation_id":"user-install","session_id":"user-session","thread_id":"user-thread","turn_id":"user-turn","window_id":"user-thread:0","sandbox":"seccomp","thread_source":"user"}`
  187. h := http.Header{}
  188. h.Set("x-codex-installation-id", "user-install")
  189. h.Set("x-codex-window-id", "user-thread:0")
  190. h.Set("x-codex-turn-metadata", turnMetadata)
  191. h.Set("x-client-request-id", "user-thread")
  192. ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
  193. applyCodexFingerprintHeaders(h, ids)
  194. seed, ok := codexFingerprintSeed(account.Extra)
  195. require.True(t, ok)
  196. convergedInstall := resolveConvergedInstallationID(account, seed)
  197. convergedSession := resolveConvergedSessionID(seed)
  198. convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
  199. assert.Equal(t, convergedInstall, h.Get("x-codex-installation-id"))
  200. assert.Equal(t, convergedSession, h.Get("session-id"))
  201. assert.Equal(t, convergedSession, h.Get("session_id"), "下划线形式也应被改写")
  202. assert.Equal(t, convergedThread, h.Get("thread-id"))
  203. assert.Equal(t, convergedThread, h.Get("x-client-request-id"))
  204. assert.Equal(t, convergedThread+":0", h.Get("x-codex-window-id"))
  205. var meta map[string]any
  206. require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
  207. assert.Equal(t, convergedInstall, meta["installation_id"])
  208. assert.Equal(t, convergedSession, meta["session_id"])
  209. assert.Equal(t, convergedThread, meta["thread_id"])
  210. assert.NotEqual(t, "user-turn", meta["turn_id"], "turn_id 应被新生成的值替换")
  211. assert.Equal(t, "seccomp", meta["sandbox"], "sandbox 保留原样")
  212. assert.Equal(t, "user", meta["thread_source"], "thread_source 保留原样")
  213. }
  214. // --- session 模式:不同客户端得到不同 thread ---
  215. func TestApplyCodexFingerprintHeaders_SessionMode_DifferentClients(t *testing.T) {
  216. account := newTestOAuthAccount(1, map[string]any{
  217. codexFingerprintModeExtraKey: "session",
  218. })
  219. makeTurnMeta := func() string {
  220. return `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`
  221. }
  222. clientA := http.Header{}
  223. clientA.Set("session-id", "client-A")
  224. idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
  225. hA := http.Header{}
  226. hA.Set("x-codex-turn-metadata", makeTurnMeta())
  227. applyCodexFingerprintHeaders(hA, idsA)
  228. clientB := http.Header{}
  229. clientB.Set("session-id", "client-B")
  230. idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
  231. hB := http.Header{}
  232. hB.Set("x-codex-turn-metadata", makeTurnMeta())
  233. applyCodexFingerprintHeaders(hB, idsB)
  234. assert.Equal(t, hA.Get("session-id"), hB.Get("session-id"), "session_id 应相同")
  235. assert.NotEqual(t, hA.Get("thread-id"), hB.Get("thread-id"), "不同客户端 thread_id 应不同")
  236. assert.NotEqual(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "不同客户端 window_id 应不同")
  237. assert.Equal(t, hA.Get("x-codex-installation-id"), hB.Get("x-codex-installation-id"))
  238. }
  239. // --- full 模式 ---
  240. func TestApplyCodexFingerprintHeaders_FullMode(t *testing.T) {
  241. account := newTestOAuthAccount(1, map[string]any{
  242. codexFingerprintModeExtraKey: "full",
  243. })
  244. seed, ok := codexFingerprintSeed(account.Extra)
  245. require.True(t, ok)
  246. convergedSession := resolveConvergedSessionID(seed)
  247. clientA := http.Header{}
  248. clientA.Set("session-id", "client-A")
  249. idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
  250. hA := http.Header{}
  251. hA.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
  252. applyCodexFingerprintHeaders(hA, idsA)
  253. clientB := http.Header{}
  254. clientB.Set("session-id", "client-B")
  255. idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
  256. hB := http.Header{}
  257. hB.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
  258. applyCodexFingerprintHeaders(hB, idsB)
  259. assert.Equal(t, hA.Get("thread-id"), hB.Get("thread-id"), "full 模式 thread_id 应相同")
  260. assert.Equal(t, convergedSession, hA.Get("thread-id"), "full 模式 thread_id 应等于 session_id")
  261. assert.Equal(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "full 模式 window_id 应相同")
  262. }
  263. // --- H1 修复验证:头和体的 turn_id 一致性 ---
  264. func TestFingerprintIDs_HeaderAndBody_TurnID_Consistent(t *testing.T) {
  265. account := newTestOAuthAccount(1, map[string]any{
  266. codexFingerprintModeExtraKey: "session",
  267. })
  268. clientHeaders := http.Header{}
  269. clientHeaders.Set("session-id", "client-session-xyz")
  270. ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
  271. require.NotNil(t, ids)
  272. // 头改写
  273. h := http.Header{}
  274. h.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
  275. applyCodexFingerprintHeaders(h, ids)
  276. // 体改写(使用同一份 ids)
  277. reqBody := map[string]any{
  278. "client_metadata": map[string]any{
  279. "x-codex-installation-id": "x",
  280. "session_id": "x",
  281. "turn_id": "x",
  282. "x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
  283. },
  284. }
  285. applyCodexFingerprintClientMetadata(reqBody, ids)
  286. // 从头 turn-metadata JSON 提取 turn_id
  287. var headerMeta map[string]any
  288. require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
  289. headerTurnID, ok := headerMeta["turn_id"].(string)
  290. require.True(t, ok, "头 turn-metadata 应包含 string 类型的 turn_id")
  291. // 从体 client_metadata 提取 turn_id
  292. cm, ok := reqBody["client_metadata"].(map[string]any)
  293. require.True(t, ok, "请求体应包含 client_metadata")
  294. bodyTurnID, ok := cm["turn_id"].(string)
  295. require.True(t, ok, "体 client_metadata 应包含 string 类型的 turn_id")
  296. // 从体内嵌 turn-metadata JSON 提取 turn_id
  297. embeddedRaw, ok := cm["x-codex-turn-metadata"].(string)
  298. require.True(t, ok, "体 client_metadata 应包含 x-codex-turn-metadata 字符串")
  299. var bodyMeta map[string]any
  300. require.NoError(t, json.Unmarshal([]byte(embeddedRaw), &bodyMeta))
  301. bodyEmbeddedTurnID, ok := bodyMeta["turn_id"].(string)
  302. require.True(t, ok, "体内嵌 turn-metadata 应包含 string 类型的 turn_id")
  303. assert.Equal(t, headerTurnID, bodyTurnID, "头和体的 turn_id 必须一致")
  304. assert.Equal(t, headerTurnID, bodyEmbeddedTurnID, "头和体内嵌 turn-metadata 的 turn_id 必须一致")
  305. assert.Equal(t, ids.turnID, headerTurnID, "所有 turn_id 都应来自同一份 ids")
  306. assert.Equal(t, headerMeta["turn_started_at_unix_ms"], bodyMeta["turn_started_at_unix_ms"], "头和体的 timestamp 必须一致")
  307. assert.Equal(t, float64(ids.turnStartedAtUnixMs), headerMeta["turn_started_at_unix_ms"])
  308. }
  309. func TestFingerprintIDs_MalformedEmbeddedMetadataRebuiltConsistently(t *testing.T) {
  310. account := newTestOAuthAccount(2, map[string]any{codexFingerprintModeExtraKey: "session"})
  311. clientHeaders := make(http.Header)
  312. clientHeaders.Set("session-id", "client-session-malformed")
  313. ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
  314. require.NotNil(t, ids)
  315. h := make(http.Header)
  316. h.Set("x-codex-turn-metadata", "{malformed")
  317. applyCodexFingerprintHeaders(h, ids)
  318. reqBody := map[string]any{
  319. "client_metadata": map[string]any{
  320. "session_id": "client-session-malformed",
  321. "x-codex-turn-metadata": "[malformed",
  322. },
  323. }
  324. require.True(t, applyCodexFingerprintClientMetadata(reqBody, ids))
  325. var headerMeta map[string]any
  326. require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
  327. clientMetadata, ok := reqBody["client_metadata"].(map[string]any)
  328. require.True(t, ok)
  329. bodyRaw, ok := clientMetadata["x-codex-turn-metadata"].(string)
  330. require.True(t, ok)
  331. var bodyMeta map[string]any
  332. require.NoError(t, json.Unmarshal([]byte(bodyRaw), &bodyMeta))
  333. for _, key := range []string{"installation_id", "session_id", "thread_id", "turn_id", "window_id", "turn_started_at_unix_ms"} {
  334. assert.Equal(t, headerMeta[key], bodyMeta[key], "rebuilt metadata field %s must match", key)
  335. }
  336. }
  337. // --- applyCodexFingerprintClientMetadata ---
  338. func TestApplyCodexFingerprintClientMetadata_OffMode(t *testing.T) {
  339. reqBody := map[string]any{
  340. "client_metadata": map[string]any{
  341. "x-codex-installation-id": "original",
  342. },
  343. }
  344. modified := applyCodexFingerprintClientMetadata(reqBody, nil)
  345. assert.False(t, modified, "nil ids 不改写")
  346. }
  347. func TestApplyCodexFingerprintClientMetadata_DeviceMode(t *testing.T) {
  348. account := newTestOAuthAccount(1, map[string]any{
  349. codexFingerprintModeExtraKey: "device",
  350. "openai_device_id": "converged-device",
  351. })
  352. ids := resolveCodexFingerprintIDsFromRequest(account, nil)
  353. require.NotNil(t, ids)
  354. embeddedMeta := `{"installation_id":"x","session_id":"user-session","sandbox":"seccomp"}`
  355. reqBody := map[string]any{
  356. "client_metadata": map[string]any{
  357. "x-codex-installation-id": "original-install",
  358. "session_id": "user-session",
  359. "x-codex-turn-metadata": embeddedMeta,
  360. },
  361. }
  362. modified := applyCodexFingerprintClientMetadata(reqBody, ids)
  363. require.True(t, modified)
  364. cm, ok := reqBody["client_metadata"].(map[string]any)
  365. require.True(t, ok)
  366. assert.Equal(t, "converged-device", cm["x-codex-installation-id"])
  367. assert.Equal(t, "user-session", cm["session_id"], "device 模式不改 session_id")
  368. turnMetaStr, ok := cm["x-codex-turn-metadata"].(string)
  369. require.True(t, ok)
  370. var meta map[string]any
  371. require.NoError(t, json.Unmarshal([]byte(turnMetaStr), &meta))
  372. assert.Equal(t, "converged-device", meta["installation_id"])
  373. assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
  374. }
  375. func TestApplyCodexFingerprintClientMetadata_SessionMode(t *testing.T) {
  376. account := newTestOAuthAccount(1, map[string]any{
  377. codexFingerprintModeExtraKey: "session",
  378. })
  379. clientHeaders := http.Header{}
  380. clientHeaders.Set("session-id", "client-session-aaa")
  381. ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
  382. require.NotNil(t, ids)
  383. embeddedMeta := `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0","sandbox":"seccomp"}`
  384. reqBody := map[string]any{
  385. "client_metadata": map[string]any{
  386. "x-codex-installation-id": "original-install",
  387. "session_id": "original-session",
  388. "x-codex-turn-metadata": embeddedMeta,
  389. },
  390. }
  391. modified := applyCodexFingerprintClientMetadata(reqBody, ids)
  392. require.True(t, modified)
  393. cm, ok := reqBody["client_metadata"].(map[string]any)
  394. require.True(t, ok)
  395. seed, ok := codexFingerprintSeed(account.Extra)
  396. require.True(t, ok)
  397. convergedInstall := resolveConvergedInstallationID(account, seed)
  398. convergedSession := resolveConvergedSessionID(seed)
  399. convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
  400. assert.Equal(t, convergedInstall, cm["x-codex-installation-id"])
  401. assert.Equal(t, convergedSession, cm["session_id"])
  402. assert.Equal(t, convergedThread, cm["thread_id"])
  403. assert.Equal(t, convergedThread+":0", cm["x-codex-window-id"])
  404. turnMetaStr, ok := cm["x-codex-turn-metadata"].(string)
  405. require.True(t, ok)
  406. var meta map[string]any
  407. require.NoError(t, json.Unmarshal([]byte(turnMetaStr), &meta))
  408. assert.Equal(t, convergedInstall, meta["installation_id"])
  409. assert.Equal(t, convergedSession, meta["session_id"])
  410. assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
  411. }
  412. func TestApplyCodexFingerprintClientMetadata_FullMode(t *testing.T) {
  413. account := newTestOAuthAccount(1, map[string]any{
  414. codexFingerprintModeExtraKey: "full",
  415. })
  416. clientHeaders := http.Header{}
  417. clientHeaders.Set("session-id", "any-client")
  418. ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
  419. require.NotNil(t, ids)
  420. reqBody := map[string]any{
  421. "client_metadata": map[string]any{
  422. "session_id": "x",
  423. "thread_id": "x",
  424. "x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
  425. },
  426. }
  427. modified := applyCodexFingerprintClientMetadata(reqBody, ids)
  428. require.True(t, modified)
  429. cm, ok := reqBody["client_metadata"].(map[string]any)
  430. require.True(t, ok)
  431. seed, ok := codexFingerprintSeed(account.Extra)
  432. require.True(t, ok)
  433. convergedSession := resolveConvergedSessionID(seed)
  434. assert.Equal(t, convergedSession, cm["session_id"])
  435. assert.Equal(t, convergedSession, cm["thread_id"], "full 模式 thread_id 应等于 session_id")
  436. }
  437. // --- extractClientSessionID ---
  438. func TestExtractClientSessionID(t *testing.T) {
  439. tests := []struct {
  440. name string
  441. headers http.Header
  442. expected string
  443. }{
  444. {"连字符形式优先", func() http.Header {
  445. h := http.Header{}
  446. h.Set("session-id", "hyphen-form")
  447. h.Set("session_id", "underscore-form")
  448. return h
  449. }(), "hyphen-form"},
  450. {"回退到下划线形式", func() http.Header {
  451. h := http.Header{}
  452. h.Set("session_id", "underscore-form")
  453. return h
  454. }(), "underscore-form"},
  455. {"都没有", http.Header{}, ""},
  456. }
  457. for _, tt := range tests {
  458. t.Run(tt.name, func(t *testing.T) {
  459. assert.Equal(t, tt.expected, extractClientSessionID(tt.headers))
  460. })
  461. }
  462. }
  463. // --- 透传路径:raw 字节版 client_metadata 改写 ---
  464. // rawVsMapClientMetadata 用同一份 ids 分别跑 map 版与 raw 字节版,
  465. // 返回两侧最终的 client_metadata 解码结果。
  466. func rawVsMapClientMetadata(t *testing.T, body []byte, ids *codexFingerprintIDs) (map[string]any, map[string]any) {
  467. t.Helper()
  468. var decoded map[string]any
  469. require.NoError(t, json.Unmarshal(body, &decoded))
  470. applyCodexFingerprintClientMetadata(decoded, ids)
  471. mapCM, _ := decoded["client_metadata"].(map[string]any)
  472. rawBody, changed, err := applyCodexFingerprintClientMetadataRaw(body, ids)
  473. require.NoError(t, err)
  474. require.True(t, changed)
  475. var rawDecoded map[string]any
  476. require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
  477. rawCM, _ := rawDecoded["client_metadata"].(map[string]any)
  478. return mapCM, rawCM
  479. }
  480. func cloneCodexFingerprintIDsForTest(ids *codexFingerprintIDs) *codexFingerprintIDs {
  481. if ids == nil {
  482. return nil
  483. }
  484. cloned := *ids
  485. cloned.originalBodySessionID = ""
  486. cloned.originalBodySessionIDCaptured = false
  487. return &cloned
  488. }
  489. func applyMapAndRawFingerprintBodiesForTest(t *testing.T, body []byte, ids *codexFingerprintIDs) (map[string]any, map[string]any) {
  490. t.Helper()
  491. mapIDs := cloneCodexFingerprintIDsForTest(ids)
  492. rawIDs := cloneCodexFingerprintIDsForTest(ids)
  493. var decoded map[string]any
  494. require.NoError(t, json.Unmarshal(body, &decoded))
  495. applyCodexFingerprintClientMetadata(decoded, mapIDs)
  496. rawBody, _, err := applyCodexFingerprintClientMetadataRaw(body, rawIDs)
  497. require.NoError(t, err)
  498. var rawDecoded map[string]any
  499. require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
  500. return decoded, rawDecoded
  501. }
  502. func TestApplyCodexFingerprintPromptCacheKey_MapRawEquivalence(t *testing.T) {
  503. for _, mode := range []codexFingerprintMode{codexFingerprintSession, codexFingerprintFull} {
  504. t.Run(string(mode)+"/default", func(t *testing.T) {
  505. account := newTestOAuthAccount(4300, map[string]any{codexFingerprintModeExtraKey: string(mode)})
  506. ids := resolveCodexFingerprintIDs(account, "header-session", mode)
  507. require.NotNil(t, ids)
  508. body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"body-session","client_metadata":{"session_id":" body-session ","trace":"keep"},"input":[]}`)
  509. mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
  510. require.Equal(t, mapBody["prompt_cache_key"], rawBody["prompt_cache_key"])
  511. require.Equal(t, ids.sessionID, mapBody["prompt_cache_key"])
  512. mapCM, _ := mapBody["client_metadata"].(map[string]any)
  513. rawCM, _ := rawBody["client_metadata"].(map[string]any)
  514. require.Equal(t, ids.sessionID, mapCM["session_id"])
  515. require.Equal(t, mapCM["session_id"], rawCM["session_id"])
  516. require.Equal(t, "keep", rawCM["trace"])
  517. })
  518. }
  519. t.Run("explicit override", func(t *testing.T) {
  520. account := newTestOAuthAccount(4301, map[string]any{codexFingerprintModeExtraKey: "session"})
  521. ids := resolveCodexFingerprintIDs(account, "header-session", codexFingerprintSession)
  522. require.NotNil(t, ids)
  523. body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"explicit-cache","client_metadata":{"session_id":"body-session"},"input":[]}`)
  524. mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
  525. require.Equal(t, "explicit-cache", mapBody["prompt_cache_key"])
  526. require.Equal(t, "explicit-cache", rawBody["prompt_cache_key"])
  527. mapCM, _ := mapBody["client_metadata"].(map[string]any)
  528. rawCM, _ := rawBody["client_metadata"].(map[string]any)
  529. require.Equal(t, ids.sessionID, mapCM["session_id"])
  530. require.Equal(t, ids.sessionID, rawCM["session_id"])
  531. })
  532. }
  533. func TestApplyCodexFingerprintPromptCacheKey_Negatives(t *testing.T) {
  534. sessionAccount := newTestOAuthAccount(4310, map[string]any{codexFingerprintModeExtraKey: "session"})
  535. sessionIDs := resolveCodexFingerprintIDs(sessionAccount, "header-session", codexFingerprintSession)
  536. require.NotNil(t, sessionIDs)
  537. deviceAccount := newTestOAuthAccount(4311, map[string]any{codexFingerprintModeExtraKey: "device"})
  538. deviceIDs := resolveCodexFingerprintIDs(deviceAccount, "header-session", codexFingerprintDevice)
  539. require.NotNil(t, deviceIDs)
  540. tests := []struct {
  541. name string
  542. body []byte
  543. ids *codexFingerprintIDs
  544. wantExists bool
  545. wantCacheKey any
  546. wantRawString string
  547. }{
  548. {
  549. name: "missing key is not injected",
  550. body: []byte(`{"client_metadata":{"session_id":"body-session"}}`),
  551. ids: sessionIDs,
  552. wantExists: false,
  553. },
  554. {
  555. name: "empty key preserved",
  556. body: []byte(`{"prompt_cache_key":"","client_metadata":{"session_id":"body-session"}}`),
  557. ids: sessionIDs,
  558. wantExists: true,
  559. wantCacheKey: "",
  560. },
  561. {
  562. name: "whitespace-different key is an explicit override",
  563. body: []byte(`{"prompt_cache_key":" body-session ","client_metadata":{"session_id":"body-session"}}`),
  564. ids: sessionIDs,
  565. wantExists: true,
  566. wantCacheKey: " body-session ",
  567. },
  568. {
  569. name: "non-string key preserved",
  570. body: []byte(`{"prompt_cache_key":123,"client_metadata":{"session_id":"body-session"}}`),
  571. ids: sessionIDs,
  572. wantExists: true,
  573. wantCacheKey: float64(123),
  574. },
  575. {
  576. name: "missing source metadata preserves key",
  577. body: []byte(`{"prompt_cache_key":"body-session"}`),
  578. ids: sessionIDs,
  579. wantExists: true,
  580. wantCacheKey: "body-session",
  581. },
  582. {
  583. name: "non-string source session preserves key",
  584. body: []byte(`{"prompt_cache_key":"123","client_metadata":{"session_id":123}}`),
  585. ids: sessionIDs,
  586. wantExists: true,
  587. wantCacheKey: "123",
  588. },
  589. {
  590. name: "non-object source metadata preserves key",
  591. body: []byte(`{"prompt_cache_key":"body-session","client_metadata":"bad"}`),
  592. ids: sessionIDs,
  593. wantExists: true,
  594. wantCacheKey: "body-session",
  595. },
  596. {
  597. name: "device mode preserves key",
  598. body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
  599. ids: deviceIDs,
  600. wantExists: true,
  601. wantCacheKey: "body-session",
  602. },
  603. {
  604. name: "off mode preserves body",
  605. body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
  606. ids: nil,
  607. wantExists: true,
  608. wantCacheKey: "body-session",
  609. wantRawString: `{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`,
  610. },
  611. }
  612. for _, tt := range tests {
  613. t.Run(tt.name, func(t *testing.T) {
  614. var mapBody map[string]any
  615. require.NoError(t, json.Unmarshal(tt.body, &mapBody))
  616. changedMap := applyCodexFingerprintClientMetadata(mapBody, cloneCodexFingerprintIDsForTest(tt.ids))
  617. rawBody, changedRaw, err := applyCodexFingerprintClientMetadataRaw(tt.body, cloneCodexFingerprintIDsForTest(tt.ids))
  618. require.NoError(t, err)
  619. if tt.ids == nil {
  620. require.False(t, changedMap)
  621. require.False(t, changedRaw)
  622. require.JSONEq(t, tt.wantRawString, string(rawBody))
  623. return
  624. }
  625. require.True(t, changedMap)
  626. require.True(t, changedRaw)
  627. rawDecoded := map[string]any{}
  628. require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
  629. _, mapExists := mapBody["prompt_cache_key"]
  630. _, rawExists := rawDecoded["prompt_cache_key"]
  631. require.Equal(t, tt.wantExists, mapExists)
  632. require.Equal(t, tt.wantExists, rawExists)
  633. if tt.wantExists {
  634. require.Equal(t, tt.wantCacheKey, mapBody["prompt_cache_key"])
  635. require.Equal(t, tt.wantCacheKey, rawDecoded["prompt_cache_key"])
  636. }
  637. })
  638. }
  639. }
  640. func TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant(t *testing.T) {
  641. embedded := `{\"installation_id\":\"real-install\",\"session_id\":\"real-session\",\"sandbox\":\"seatbelt\"}`
  642. bodies := map[string]string{
  643. "no_client_metadata": `{"model":"gpt-5.6-sol","input":[],"stream":true}`,
  644. "object_with_extras": `{"model":"gpt-5.6-sol","client_metadata":{"session_id":"client-session","traceparent":"00-abc-def-01","x-codex-turn-metadata":"` + embedded + `"},"stream":true}`,
  645. "non_object_value": `{"model":"gpt-5.6-sol","client_metadata":"bogus","stream":true}`,
  646. }
  647. for _, mode := range []codexFingerprintMode{codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull} {
  648. account := newTestOAuthAccount(4242, map[string]any{codexFingerprintModeExtraKey: string(mode)})
  649. ids := resolveCodexFingerprintIDs(account, "client-sess-raw", mode)
  650. require.NotNil(t, ids)
  651. for name, body := range bodies {
  652. t.Run(string(mode)+"/"+name, func(t *testing.T) {
  653. mapCM, rawCM := rawVsMapClientMetadata(t, []byte(body), ids)
  654. assert.Equal(t, mapCM, rawCM, "raw 字节版与 map 版的 client_metadata 结果必须逐点一致")
  655. })
  656. }
  657. }
  658. }
  659. func TestApplyCodexFingerprintClientMetadataRaw_PreservesUnrelatedFields(t *testing.T) {
  660. account := newTestOAuthAccount(4243, map[string]any{codexFingerprintModeExtraKey: "session"})
  661. ids := resolveCodexFingerprintIDs(account, "client-sess-preserve", codexFingerprintSession)
  662. require.NotNil(t, ids)
  663. body := []byte(`{"model":"gpt-5.6-sol","input":[{"type":"message","role":"user","content":"hi"}],"stream":true,"prompt_cache_key":"pck-1"}`)
  664. out, changed, err := applyCodexFingerprintClientMetadataRaw(body, ids)
  665. require.NoError(t, err)
  666. require.True(t, changed)
  667. var decoded map[string]any
  668. require.NoError(t, json.Unmarshal(out, &decoded))
  669. assert.Equal(t, "gpt-5.6-sol", decoded["model"])
  670. assert.Equal(t, "pck-1", decoded["prompt_cache_key"])
  671. assert.Equal(t, true, decoded["stream"])
  672. cm, _ := decoded["client_metadata"].(map[string]any)
  673. require.NotNil(t, cm)
  674. assert.Equal(t, ids.sessionID, cm["session_id"])
  675. assert.Equal(t, ids.turnID, cm["turn_id"])
  676. }
  677. func TestApplyCodexFingerprintClientMetadataRaw_Noop(t *testing.T) {
  678. body := []byte(`{"model":"gpt-5.6-sol"}`)
  679. out, changed, err := applyCodexFingerprintClientMetadataRaw(body, nil)
  680. require.NoError(t, err)
  681. assert.False(t, changed)
  682. assert.Equal(t, body, out)
  683. out, changed, err = applyCodexFingerprintClientMetadataRaw(nil, &codexFingerprintIDs{mode: codexFingerprintSession, installationID: "x"})
  684. require.NoError(t, err)
  685. assert.False(t, changed)
  686. assert.Nil(t, out)
  687. }
  688. // --- context 暂存与出站头应用(透传/非透传共用 seam)---
  689. func newFingerprintStageTestContext(t *testing.T) *gin.Context {
  690. t.Helper()
  691. gin.SetMode(gin.TestMode)
  692. c, _ := gin.CreateTestContext(httptest.NewRecorder())
  693. c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  694. return c
  695. }
  696. func TestStageCodexFingerprintIDs_NilOverwritesPreviousAccount(t *testing.T) {
  697. c := newFingerprintStageTestContext(t)
  698. accountA := newTestOAuthAccount(1001, map[string]any{codexFingerprintModeExtraKey: "session"})
  699. idsA := resolveCodexFingerprintIDs(accountA, "sess-x", codexFingerprintSession)
  700. require.NotNil(t, idsA)
  701. stageCodexFingerprintIDs(c, idsA)
  702. // failover 切到 off 模式账号:无条件覆写为 nil,上一账号 IDs 不得残留
  703. stageCodexFingerprintIDs(c, nil)
  704. h := http.Header{}
  705. h.Set("session_id", "isolated-session")
  706. accountB := newTestOAuthAccount(1002, map[string]any{"codex_fingerprint_mode": "off"})
  707. applyStagedCodexFingerprintHeaders(c, accountB, h)
  708. assert.Equal(t, "isolated-session", h.Get("session_id"), "off 账号不得应用上一账号的收敛 ID")
  709. assert.Empty(t, h.Get("x-codex-installation-id"))
  710. }
  711. func TestApplyStagedCodexFingerprintRejectsDifferentOAuthAccount(t *testing.T) {
  712. c := newFingerprintStageTestContext(t)
  713. accountA := newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"})
  714. idsA := resolveCodexFingerprintIDs(accountA, "sess-a", codexFingerprintSession)
  715. require.NotNil(t, idsA)
  716. stageCodexFingerprintIDs(c, idsA)
  717. accountB := newTestOAuthAccount(1004, map[string]any{codexFingerprintModeExtraKey: "session"})
  718. h := make(http.Header)
  719. h.Set("session-id", "account-b-session")
  720. applyStagedCodexFingerprintHeaders(c, accountB, h)
  721. assert.Equal(t, "account-b-session", h.Get("session-id"))
  722. assert.Empty(t, h.Get("x-codex-installation-id"))
  723. body := map[string]any{"client_metadata": map[string]any{"session_id": "account-b-session"}}
  724. assert.False(t, applyStagedCodexFingerprintClientMetadata(c, accountB, body))
  725. clientMetadata, ok := body["client_metadata"].(map[string]any)
  726. require.True(t, ok)
  727. assert.Equal(t, "account-b-session", clientMetadata["session_id"])
  728. }
  729. func TestApplyStagedCodexFingerprintHeaders_SkipsNonOAuthAccount(t *testing.T) {
  730. c := newFingerprintStageTestContext(t)
  731. oauthIDs := resolveCodexFingerprintIDs(newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"}), "sess-y", codexFingerprintSession)
  732. require.NotNil(t, oauthIDs)
  733. stageCodexFingerprintIDs(c, oauthIDs)
  734. h := http.Header{}
  735. apiKeyAccount := &Account{ID: 1004, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
  736. applyStagedCodexFingerprintHeaders(c, apiKeyAccount, h)
  737. assert.Empty(t, h.Get("x-codex-installation-id"), "stale 收敛 ID 不得应用到非 OAuth 账号")
  738. }
  739. func TestBuildUpstreamRequestOpenAIPassthrough_AppliesStagedFingerprint(t *testing.T) {
  740. svc := &OpenAIGatewayService{}
  741. // 收敛是显式 opt-in(#5610):显式开启后验证透传路径的出站头收敛。
  742. account := newTestOAuthAccount(2001, map[string]any{
  743. "openai_oauth_passthrough": true,
  744. "codex_fingerprint_mode": "session",
  745. })
  746. c := newFingerprintStageTestContext(t)
  747. c.Request.Header.Set("session_id", "real-client-session")
  748. c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color")
  749. c.Request.Header.Set("originator", "codex_cli_rs")
  750. c.Request.Header.Set("x-codex-turn-metadata", `{"installation_id":"real-install","session_id":"real-session","sandbox":"seatbelt"}`)
  751. // 复刻 forwardOpenAIPassthrough 的解析+暂存 seam(默认 session 模式)
  752. ids := resolveCodexFingerprintIDsFromRequest(account, c.Request.Header)
  753. require.NotNil(t, ids)
  754. stageCodexFingerprintIDs(c, ids)
  755. body := []byte(`{"model":"gpt-5.6-sol","input":[],"stream":true}`)
  756. req, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, account, body, "test-token")
  757. require.NoError(t, err)
  758. assert.Equal(t, ids.sessionID, req.Header.Get("session_id"), "session 模式下出站 session_id 应为账号级收敛值")
  759. assert.Equal(t, ids.installationID, req.Header.Get("x-codex-installation-id"))
  760. assert.Equal(t, ids.windowID, req.Header.Get("x-codex-window-id"))
  761. assert.Equal(t, ids.threadID, req.Header.Get("x-client-request-id"))
  762. turnMetadata := req.Header.Get("x-codex-turn-metadata")
  763. require.NotEmpty(t, turnMetadata)
  764. assert.Contains(t, turnMetadata, ids.sessionID, "turn-metadata JSON 中的 session_id 应被收敛")
  765. assert.Contains(t, turnMetadata, `"sandbox":"seatbelt"`, "turn-metadata 未指定字段应原样保留")
  766. }
  767. func TestBuildUpstreamRequestOpenAIPassthrough_OffModeKeepsIsolatedSession(t *testing.T) {
  768. svc := &OpenAIGatewayService{}
  769. account := newTestOAuthAccount(2002, map[string]any{
  770. "openai_oauth_passthrough": true,
  771. "codex_fingerprint_mode": "off",
  772. })
  773. c := newFingerprintStageTestContext(t)
  774. c.Request.Header.Set("session_id", "real-client-session")
  775. c.Request.Header.Set("originator", "codex_cli_rs")
  776. ids := resolveCodexFingerprintIDsFromRequest(account, c.Request.Header)
  777. require.Nil(t, ids)
  778. stageCodexFingerprintIDs(c, ids)
  779. body := []byte(`{"model":"gpt-5.6-sol","input":[],"stream":true}`)
  780. req, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, account, body, "test-token")
  781. require.NoError(t, err)
  782. assert.NotEmpty(t, req.Header.Get("session_id"))
  783. assert.NotEqual(t, resolveConvergedSessionID(testCodexFingerprintSeed), req.Header.Get("session_id"), "off 模式不得收敛 session_id")
  784. assert.Empty(t, req.Header.Get("x-codex-window-id"))
  785. }
  786. func TestApplyCodexFingerprintClientMetadataRaw_NonObjectBodyUntouched(t *testing.T) {
  787. account := newTestOAuthAccount(4244, map[string]any{codexFingerprintModeExtraKey: "session"})
  788. ids := resolveCodexFingerprintIDs(account, "client-sess-nonobj", codexFingerprintSession)
  789. require.NotNil(t, ids)
  790. for _, body := range []string{`[1,2,3]`, `"plain string"`, `not json at all`} {
  791. out, changed, err := applyCodexFingerprintClientMetadataRaw([]byte(body), ids)
  792. require.NoError(t, err)
  793. assert.False(t, changed, "非 JSON 对象 body 不应被改写: %s", body)
  794. assert.Equal(t, []byte(body), out)
  795. }
  796. }