929 lines
38 KiB
Go
929 lines
38 KiB
Go
package service
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"net/http"
|
|||
|
|
"net/http/httptest"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
"github.com/gin-gonic/gin"
|
|||
|
|
"github.com/google/uuid"
|
|||
|
|
"github.com/stretchr/testify/assert"
|
|||
|
|
"github.com/stretchr/testify/require"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const testCodexFingerprintSeed = "11111111-1111-4111-8111-111111111111"
|
|||
|
|
|
|||
|
|
func newTestOAuthAccount(id int64, extra map[string]any) *Account {
|
|||
|
|
if codexFingerprintModeRequiresSeed(codexFingerprintModeFromExtra(extra)) {
|
|||
|
|
if extra == nil {
|
|||
|
|
extra = make(map[string]any)
|
|||
|
|
}
|
|||
|
|
if _, exists := extra[codexFingerprintSeedExtraKey]; !exists {
|
|||
|
|
extra[codexFingerprintSeedExtraKey] = testCodexFingerprintSeed
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return &Account{
|
|||
|
|
ID: id,
|
|||
|
|
Platform: PlatformOpenAI,
|
|||
|
|
Type: AccountTypeOAuth,
|
|||
|
|
Extra: extra,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- deriveStableUUIDv4 ---
|
|||
|
|
|
|||
|
|
func TestDeriveStableUUIDv4_Deterministic(t *testing.T) {
|
|||
|
|
a := deriveStableUUIDv4("test-seed-1")
|
|||
|
|
b := deriveStableUUIDv4("test-seed-1")
|
|||
|
|
assert.Equal(t, a, b, "同一种子应返回相同结果")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeriveStableUUIDv4_DifferentSeeds(t *testing.T) {
|
|||
|
|
a := deriveStableUUIDv4("seed-a")
|
|||
|
|
b := deriveStableUUIDv4("seed-b")
|
|||
|
|
assert.NotEqual(t, a, b, "不同种子应返回不同结果")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestDeriveStableUUIDv4_ValidFormat(t *testing.T) {
|
|||
|
|
result := deriveStableUUIDv4("test-seed")
|
|||
|
|
parsed, err := uuid.Parse(result)
|
|||
|
|
require.NoError(t, err, "应返回合法 UUID 格式")
|
|||
|
|
assert.Equal(t, uuid.Version(4), parsed.Version(), "应为 UUIDv4")
|
|||
|
|
assert.Equal(t, uuid.RFC4122, parsed.Variant(), "应为 RFC4122 变体")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- GetCodexFingerprintMode ---
|
|||
|
|
|
|||
|
|
func TestGetCodexFingerprintMode(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
account *Account
|
|||
|
|
expected codexFingerprintMode
|
|||
|
|
}{
|
|||
|
|
{"nil 账号", nil, codexFingerprintOff},
|
|||
|
|
{"非 OAuth 账号", &Account{Platform: PlatformOpenAI, Type: "api_key"}, codexFingerprintOff},
|
|||
|
|
// 收敛是显式 opt-in:缺省/空/非法一律 off(#5610)。存量账号普遍没有这个
|
|||
|
|
// extra 键,升级不得把它们静默切进收敛。
|
|||
|
|
{"无 extra 默认 off", newTestOAuthAccount(1, nil), codexFingerprintOff},
|
|||
|
|
{"空值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: ""}), codexFingerprintOff},
|
|||
|
|
{"非法值默认 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "invalid"}), codexFingerprintOff},
|
|||
|
|
{"显式 off", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"}), codexFingerprintOff},
|
|||
|
|
{"device", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "device"}), codexFingerprintDevice},
|
|||
|
|
{"session", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "session"}), codexFingerprintSession},
|
|||
|
|
{"full", newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "full"}), codexFingerprintFull},
|
|||
|
|
}
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
assert.Equal(t, tt.expected, tt.account.GetCodexFingerprintMode())
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- resolveConvergedInstallationID ---
|
|||
|
|
|
|||
|
|
func TestResolveConvergedInstallationID_UsesDeviceID(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{"openai_device_id": "real-device-id"})
|
|||
|
|
assert.Equal(t, "real-device-id", resolveConvergedInstallationID(account, testCodexFingerprintSeed))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestResolveConvergedInstallationID_DerivesFromSeed(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(42, nil)
|
|||
|
|
result := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
|
|||
|
|
_, err := uuid.Parse(result)
|
|||
|
|
require.NoError(t, err, "派生值应为合法 UUID")
|
|||
|
|
assert.Equal(t, result, resolveConvergedInstallationID(account, testCodexFingerprintSeed), "确定性")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestResolveConvergedInstallationID_DifferentSeeds(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, nil)
|
|||
|
|
a := resolveConvergedInstallationID(account, testCodexFingerprintSeed)
|
|||
|
|
b := resolveConvergedInstallationID(account, "22222222-2222-4222-8222-222222222222")
|
|||
|
|
assert.NotEqual(t, a, b)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- resolveConvergedThreadID ---
|
|||
|
|
|
|||
|
|
func TestResolveConvergedThreadID_PerClientSession(t *testing.T) {
|
|||
|
|
a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
|
|||
|
|
b := resolveConvergedThreadID(testCodexFingerprintSeed, "session-bbb")
|
|||
|
|
assert.NotEqual(t, a, b, "不同客户端 session 应得到不同 thread_id")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestResolveConvergedThreadID_Deterministic(t *testing.T) {
|
|||
|
|
a := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
|
|||
|
|
b := resolveConvergedThreadID(testCodexFingerprintSeed, "session-aaa")
|
|||
|
|
assert.Equal(t, a, b, "同一客户端 session 应得到相同 thread_id")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestResolveConvergedThreadID_EmptySession(t *testing.T) {
|
|||
|
|
assert.Equal(t, "", resolveConvergedThreadID(testCodexFingerprintSeed, ""))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- off 模式:resolveCodexFingerprintIDsFromRequest 返回 nil ---
|
|||
|
|
|
|||
|
|
func TestResolveCodexFingerprintIDsFromRequest_ExplicitOff(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: "off"})
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
|
|||
|
|
assert.Nil(t, ids, "显式 off 模式应返回 nil")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 未显式配置的存量账号不得被收敛(#5610):默认返回 nil,出站身份保持
|
|||
|
|
// v0.1.175 之前的客户端原值。
|
|||
|
|
func TestResolveCodexFingerprintIDsFromRequest_DefaultIsOff(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, nil)
|
|||
|
|
assert.Nil(t, resolveCodexFingerprintIDsFromRequest(account, nil), "无 extra 应视为 off")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 管理员显式 opt-in 的账号行为不变。
|
|||
|
|
func TestResolveCodexFingerprintIDsFromRequest_ExplicitOptInHonored(t *testing.T) {
|
|||
|
|
for _, mode := range []string{"device", "session", "full"} {
|
|||
|
|
t.Run(mode, func(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{codexFingerprintModeExtraKey: mode})
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
|
|||
|
|
require.NotNil(t, ids, "显式配置必须生效")
|
|||
|
|
assert.Equal(t, codexFingerprintMode(mode), ids.mode)
|
|||
|
|
assert.NotEmpty(t, ids.installationID)
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestResolveCodexFingerprintIDsFromRequest_EnabledModesRequireValidSeed(t *testing.T) {
|
|||
|
|
for _, tt := range []struct {
|
|||
|
|
name string
|
|||
|
|
extra map[string]any
|
|||
|
|
}{
|
|||
|
|
{name: "missing", extra: map[string]any{codexFingerprintModeExtraKey: "device"}},
|
|||
|
|
{name: "missing with device override", extra: map[string]any{codexFingerprintModeExtraKey: "device", "openai_device_id": "real-device"}},
|
|||
|
|
{name: "blank", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: ""}},
|
|||
|
|
{name: "uppercase", extra: map[string]any{codexFingerprintModeExtraKey: "full", codexFingerprintSeedExtraKey: "11111111-1111-4111-8111-AAAAAAAAAAAA"}},
|
|||
|
|
{name: "nil uuid", extra: map[string]any{codexFingerprintModeExtraKey: "device", codexFingerprintSeedExtraKey: "00000000-0000-0000-0000-000000000000"}},
|
|||
|
|
{name: "non string", extra: map[string]any{codexFingerprintModeExtraKey: "session", codexFingerprintSeedExtraKey: 123}},
|
|||
|
|
} {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
account := &Account{ID: 1, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: tt.extra}
|
|||
|
|
require.Nil(t, resolveCodexFingerprintIDsFromRequest(account, nil))
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- applyCodexFingerprintHeaders: off 模式 ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintHeaders_OffMode(t *testing.T) {
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("x-codex-installation-id", "original-install-id")
|
|||
|
|
h.Set("x-codex-window-id", "original-window-id")
|
|||
|
|
|
|||
|
|
applyCodexFingerprintHeaders(h, nil)
|
|||
|
|
|
|||
|
|
assert.Equal(t, "original-install-id", h.Get("x-codex-installation-id"), "nil ids 不改写")
|
|||
|
|
assert.Equal(t, "original-window-id", h.Get("x-codex-window-id"), "nil ids 不改写")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- applyCodexFingerprintHeaders: device 模式 ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintHeaders_DeviceMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "device",
|
|||
|
|
"openai_device_id": "converged-device",
|
|||
|
|
})
|
|||
|
|
turnMetadata := `{"installation_id":"user-install","session_id":"user-session","sandbox":"seccomp"}`
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("x-codex-installation-id", "user-install")
|
|||
|
|
h.Set("x-codex-window-id", "user-window:0")
|
|||
|
|
h.Set("x-codex-turn-metadata", turnMetadata)
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
|
|||
|
|
applyCodexFingerprintHeaders(h, ids)
|
|||
|
|
|
|||
|
|
assert.Equal(t, "converged-device", h.Get("x-codex-installation-id"), "installation_id 应收敛")
|
|||
|
|
assert.Equal(t, "user-window:0", h.Get("x-codex-window-id"), "device 模式不改写 window_id")
|
|||
|
|
|
|||
|
|
var meta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
|
|||
|
|
assert.Equal(t, "converged-device", meta["installation_id"])
|
|||
|
|
assert.Equal(t, "user-session", meta["session_id"], "device 模式不改写 session_id")
|
|||
|
|
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- applyCodexFingerprintHeaders: session 模式 ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintHeaders_SessionMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "session",
|
|||
|
|
})
|
|||
|
|
clientHeaders := http.Header{}
|
|||
|
|
clientHeaders.Set("session-id", "client-session-aaa")
|
|||
|
|
|
|||
|
|
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"}`
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("x-codex-installation-id", "user-install")
|
|||
|
|
h.Set("x-codex-window-id", "user-thread:0")
|
|||
|
|
h.Set("x-codex-turn-metadata", turnMetadata)
|
|||
|
|
h.Set("x-client-request-id", "user-thread")
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
|
|||
|
|
applyCodexFingerprintHeaders(h, ids)
|
|||
|
|
|
|||
|
|
seed, ok := codexFingerprintSeed(account.Extra)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
convergedInstall := resolveConvergedInstallationID(account, seed)
|
|||
|
|
convergedSession := resolveConvergedSessionID(seed)
|
|||
|
|
convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
|
|||
|
|
|
|||
|
|
assert.Equal(t, convergedInstall, h.Get("x-codex-installation-id"))
|
|||
|
|
assert.Equal(t, convergedSession, h.Get("session-id"))
|
|||
|
|
assert.Equal(t, convergedSession, h.Get("session_id"), "下划线形式也应被改写")
|
|||
|
|
assert.Equal(t, convergedThread, h.Get("thread-id"))
|
|||
|
|
assert.Equal(t, convergedThread, h.Get("x-client-request-id"))
|
|||
|
|
assert.Equal(t, convergedThread+":0", h.Get("x-codex-window-id"))
|
|||
|
|
|
|||
|
|
var meta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &meta))
|
|||
|
|
assert.Equal(t, convergedInstall, meta["installation_id"])
|
|||
|
|
assert.Equal(t, convergedSession, meta["session_id"])
|
|||
|
|
assert.Equal(t, convergedThread, meta["thread_id"])
|
|||
|
|
assert.NotEqual(t, "user-turn", meta["turn_id"], "turn_id 应被新生成的值替换")
|
|||
|
|
assert.Equal(t, "seccomp", meta["sandbox"], "sandbox 保留原样")
|
|||
|
|
assert.Equal(t, "user", meta["thread_source"], "thread_source 保留原样")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- session 模式:不同客户端得到不同 thread ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintHeaders_SessionMode_DifferentClients(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "session",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
makeTurnMeta := func() string {
|
|||
|
|
return `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
clientA := http.Header{}
|
|||
|
|
clientA.Set("session-id", "client-A")
|
|||
|
|
idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
|
|||
|
|
hA := http.Header{}
|
|||
|
|
hA.Set("x-codex-turn-metadata", makeTurnMeta())
|
|||
|
|
applyCodexFingerprintHeaders(hA, idsA)
|
|||
|
|
|
|||
|
|
clientB := http.Header{}
|
|||
|
|
clientB.Set("session-id", "client-B")
|
|||
|
|
idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
|
|||
|
|
hB := http.Header{}
|
|||
|
|
hB.Set("x-codex-turn-metadata", makeTurnMeta())
|
|||
|
|
applyCodexFingerprintHeaders(hB, idsB)
|
|||
|
|
|
|||
|
|
assert.Equal(t, hA.Get("session-id"), hB.Get("session-id"), "session_id 应相同")
|
|||
|
|
assert.NotEqual(t, hA.Get("thread-id"), hB.Get("thread-id"), "不同客户端 thread_id 应不同")
|
|||
|
|
assert.NotEqual(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "不同客户端 window_id 应不同")
|
|||
|
|
assert.Equal(t, hA.Get("x-codex-installation-id"), hB.Get("x-codex-installation-id"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- full 模式 ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintHeaders_FullMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "full",
|
|||
|
|
})
|
|||
|
|
seed, ok := codexFingerprintSeed(account.Extra)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
convergedSession := resolveConvergedSessionID(seed)
|
|||
|
|
|
|||
|
|
clientA := http.Header{}
|
|||
|
|
clientA.Set("session-id", "client-A")
|
|||
|
|
idsA := resolveCodexFingerprintIDsFromRequest(account, clientA)
|
|||
|
|
hA := http.Header{}
|
|||
|
|
hA.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
|
|||
|
|
applyCodexFingerprintHeaders(hA, idsA)
|
|||
|
|
|
|||
|
|
clientB := http.Header{}
|
|||
|
|
clientB.Set("session-id", "client-B")
|
|||
|
|
idsB := resolveCodexFingerprintIDsFromRequest(account, clientB)
|
|||
|
|
hB := http.Header{}
|
|||
|
|
hB.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
|
|||
|
|
applyCodexFingerprintHeaders(hB, idsB)
|
|||
|
|
|
|||
|
|
assert.Equal(t, hA.Get("thread-id"), hB.Get("thread-id"), "full 模式 thread_id 应相同")
|
|||
|
|
assert.Equal(t, convergedSession, hA.Get("thread-id"), "full 模式 thread_id 应等于 session_id")
|
|||
|
|
assert.Equal(t, hA.Get("x-codex-window-id"), hB.Get("x-codex-window-id"), "full 模式 window_id 应相同")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- H1 修复验证:头和体的 turn_id 一致性 ---
|
|||
|
|
|
|||
|
|
func TestFingerprintIDs_HeaderAndBody_TurnID_Consistent(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "session",
|
|||
|
|
})
|
|||
|
|
clientHeaders := http.Header{}
|
|||
|
|
clientHeaders.Set("session-id", "client-session-xyz")
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
// 头改写
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("x-codex-turn-metadata", `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`)
|
|||
|
|
applyCodexFingerprintHeaders(h, ids)
|
|||
|
|
|
|||
|
|
// 体改写(使用同一份 ids)
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"x-codex-installation-id": "x",
|
|||
|
|
"session_id": "x",
|
|||
|
|
"turn_id": "x",
|
|||
|
|
"x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
applyCodexFingerprintClientMetadata(reqBody, ids)
|
|||
|
|
|
|||
|
|
// 从头 turn-metadata JSON 提取 turn_id
|
|||
|
|
var headerMeta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
|
|||
|
|
headerTurnID, ok := headerMeta["turn_id"].(string)
|
|||
|
|
require.True(t, ok, "头 turn-metadata 应包含 string 类型的 turn_id")
|
|||
|
|
|
|||
|
|
// 从体 client_metadata 提取 turn_id
|
|||
|
|
cm, ok := reqBody["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok, "请求体应包含 client_metadata")
|
|||
|
|
bodyTurnID, ok := cm["turn_id"].(string)
|
|||
|
|
require.True(t, ok, "体 client_metadata 应包含 string 类型的 turn_id")
|
|||
|
|
|
|||
|
|
// 从体内嵌 turn-metadata JSON 提取 turn_id
|
|||
|
|
embeddedRaw, ok := cm["x-codex-turn-metadata"].(string)
|
|||
|
|
require.True(t, ok, "体 client_metadata 应包含 x-codex-turn-metadata 字符串")
|
|||
|
|
var bodyMeta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(embeddedRaw), &bodyMeta))
|
|||
|
|
bodyEmbeddedTurnID, ok := bodyMeta["turn_id"].(string)
|
|||
|
|
require.True(t, ok, "体内嵌 turn-metadata 应包含 string 类型的 turn_id")
|
|||
|
|
|
|||
|
|
assert.Equal(t, headerTurnID, bodyTurnID, "头和体的 turn_id 必须一致")
|
|||
|
|
assert.Equal(t, headerTurnID, bodyEmbeddedTurnID, "头和体内嵌 turn-metadata 的 turn_id 必须一致")
|
|||
|
|
assert.Equal(t, ids.turnID, headerTurnID, "所有 turn_id 都应来自同一份 ids")
|
|||
|
|
assert.Equal(t, headerMeta["turn_started_at_unix_ms"], bodyMeta["turn_started_at_unix_ms"], "头和体的 timestamp 必须一致")
|
|||
|
|
assert.Equal(t, float64(ids.turnStartedAtUnixMs), headerMeta["turn_started_at_unix_ms"])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestFingerprintIDs_MalformedEmbeddedMetadataRebuiltConsistently(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(2, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
clientHeaders := make(http.Header)
|
|||
|
|
clientHeaders.Set("session-id", "client-session-malformed")
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
h := make(http.Header)
|
|||
|
|
h.Set("x-codex-turn-metadata", "{malformed")
|
|||
|
|
applyCodexFingerprintHeaders(h, ids)
|
|||
|
|
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"session_id": "client-session-malformed",
|
|||
|
|
"x-codex-turn-metadata": "[malformed",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
require.True(t, applyCodexFingerprintClientMetadata(reqBody, ids))
|
|||
|
|
|
|||
|
|
var headerMeta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(h.Get("x-codex-turn-metadata")), &headerMeta))
|
|||
|
|
clientMetadata, ok := reqBody["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
bodyRaw, ok := clientMetadata["x-codex-turn-metadata"].(string)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
var bodyMeta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(bodyRaw), &bodyMeta))
|
|||
|
|
|
|||
|
|
for _, key := range []string{"installation_id", "session_id", "thread_id", "turn_id", "window_id", "turn_started_at_unix_ms"} {
|
|||
|
|
assert.Equal(t, headerMeta[key], bodyMeta[key], "rebuilt metadata field %s must match", key)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- applyCodexFingerprintClientMetadata ---
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadata_OffMode(t *testing.T) {
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"x-codex-installation-id": "original",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
modified := applyCodexFingerprintClientMetadata(reqBody, nil)
|
|||
|
|
assert.False(t, modified, "nil ids 不改写")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadata_DeviceMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "device",
|
|||
|
|
"openai_device_id": "converged-device",
|
|||
|
|
})
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, nil)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
embeddedMeta := `{"installation_id":"x","session_id":"user-session","sandbox":"seccomp"}`
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"x-codex-installation-id": "original-install",
|
|||
|
|
"session_id": "user-session",
|
|||
|
|
"x-codex-turn-metadata": embeddedMeta,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
|
|||
|
|
require.True(t, modified)
|
|||
|
|
|
|||
|
|
cm, ok := reqBody["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
assert.Equal(t, "converged-device", cm["x-codex-installation-id"])
|
|||
|
|
assert.Equal(t, "user-session", cm["session_id"], "device 模式不改 session_id")
|
|||
|
|
|
|||
|
|
turnMetaStr, ok := cm["x-codex-turn-metadata"].(string)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
var meta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(turnMetaStr), &meta))
|
|||
|
|
assert.Equal(t, "converged-device", meta["installation_id"])
|
|||
|
|
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadata_SessionMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "session",
|
|||
|
|
})
|
|||
|
|
clientHeaders := http.Header{}
|
|||
|
|
clientHeaders.Set("session-id", "client-session-aaa")
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
embeddedMeta := `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0","sandbox":"seccomp"}`
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"x-codex-installation-id": "original-install",
|
|||
|
|
"session_id": "original-session",
|
|||
|
|
"x-codex-turn-metadata": embeddedMeta,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
|
|||
|
|
require.True(t, modified)
|
|||
|
|
|
|||
|
|
cm, ok := reqBody["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
seed, ok := codexFingerprintSeed(account.Extra)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
convergedInstall := resolveConvergedInstallationID(account, seed)
|
|||
|
|
convergedSession := resolveConvergedSessionID(seed)
|
|||
|
|
convergedThread := resolveConvergedThreadID(seed, "client-session-aaa")
|
|||
|
|
|
|||
|
|
assert.Equal(t, convergedInstall, cm["x-codex-installation-id"])
|
|||
|
|
assert.Equal(t, convergedSession, cm["session_id"])
|
|||
|
|
assert.Equal(t, convergedThread, cm["thread_id"])
|
|||
|
|
assert.Equal(t, convergedThread+":0", cm["x-codex-window-id"])
|
|||
|
|
|
|||
|
|
turnMetaStr, ok := cm["x-codex-turn-metadata"].(string)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
var meta map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal([]byte(turnMetaStr), &meta))
|
|||
|
|
assert.Equal(t, convergedInstall, meta["installation_id"])
|
|||
|
|
assert.Equal(t, convergedSession, meta["session_id"])
|
|||
|
|
assert.Equal(t, "seccomp", meta["sandbox"], "非指纹字段保留原样")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadata_FullMode(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(1, map[string]any{
|
|||
|
|
codexFingerprintModeExtraKey: "full",
|
|||
|
|
})
|
|||
|
|
clientHeaders := http.Header{}
|
|||
|
|
clientHeaders.Set("session-id", "any-client")
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, clientHeaders)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
reqBody := map[string]any{
|
|||
|
|
"client_metadata": map[string]any{
|
|||
|
|
"session_id": "x",
|
|||
|
|
"thread_id": "x",
|
|||
|
|
"x-codex-turn-metadata": `{"installation_id":"x","session_id":"x","thread_id":"x","turn_id":"x","window_id":"x:0"}`,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
modified := applyCodexFingerprintClientMetadata(reqBody, ids)
|
|||
|
|
require.True(t, modified)
|
|||
|
|
|
|||
|
|
cm, ok := reqBody["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
seed, ok := codexFingerprintSeed(account.Extra)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
convergedSession := resolveConvergedSessionID(seed)
|
|||
|
|
|
|||
|
|
assert.Equal(t, convergedSession, cm["session_id"])
|
|||
|
|
assert.Equal(t, convergedSession, cm["thread_id"], "full 模式 thread_id 应等于 session_id")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- extractClientSessionID ---
|
|||
|
|
|
|||
|
|
func TestExtractClientSessionID(t *testing.T) {
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
headers http.Header
|
|||
|
|
expected string
|
|||
|
|
}{
|
|||
|
|
{"连字符形式优先", func() http.Header {
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("session-id", "hyphen-form")
|
|||
|
|
h.Set("session_id", "underscore-form")
|
|||
|
|
return h
|
|||
|
|
}(), "hyphen-form"},
|
|||
|
|
{"回退到下划线形式", func() http.Header {
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("session_id", "underscore-form")
|
|||
|
|
return h
|
|||
|
|
}(), "underscore-form"},
|
|||
|
|
{"都没有", http.Header{}, ""},
|
|||
|
|
}
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
assert.Equal(t, tt.expected, extractClientSessionID(tt.headers))
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- 透传路径:raw 字节版 client_metadata 改写 ---
|
|||
|
|
|
|||
|
|
// rawVsMapClientMetadata 用同一份 ids 分别跑 map 版与 raw 字节版,
|
|||
|
|
// 返回两侧最终的 client_metadata 解码结果。
|
|||
|
|
func rawVsMapClientMetadata(t *testing.T, body []byte, ids *codexFingerprintIDs) (map[string]any, map[string]any) {
|
|||
|
|
t.Helper()
|
|||
|
|
|
|||
|
|
var decoded map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(body, &decoded))
|
|||
|
|
applyCodexFingerprintClientMetadata(decoded, ids)
|
|||
|
|
mapCM, _ := decoded["client_metadata"].(map[string]any)
|
|||
|
|
|
|||
|
|
rawBody, changed, err := applyCodexFingerprintClientMetadataRaw(body, ids)
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
require.True(t, changed)
|
|||
|
|
var rawDecoded map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
|
|||
|
|
rawCM, _ := rawDecoded["client_metadata"].(map[string]any)
|
|||
|
|
return mapCM, rawCM
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func cloneCodexFingerprintIDsForTest(ids *codexFingerprintIDs) *codexFingerprintIDs {
|
|||
|
|
if ids == nil {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
cloned := *ids
|
|||
|
|
cloned.originalBodySessionID = ""
|
|||
|
|
cloned.originalBodySessionIDCaptured = false
|
|||
|
|
return &cloned
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func applyMapAndRawFingerprintBodiesForTest(t *testing.T, body []byte, ids *codexFingerprintIDs) (map[string]any, map[string]any) {
|
|||
|
|
t.Helper()
|
|||
|
|
|
|||
|
|
mapIDs := cloneCodexFingerprintIDsForTest(ids)
|
|||
|
|
rawIDs := cloneCodexFingerprintIDsForTest(ids)
|
|||
|
|
|
|||
|
|
var decoded map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(body, &decoded))
|
|||
|
|
applyCodexFingerprintClientMetadata(decoded, mapIDs)
|
|||
|
|
|
|||
|
|
rawBody, _, err := applyCodexFingerprintClientMetadataRaw(body, rawIDs)
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
var rawDecoded map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
|
|||
|
|
return decoded, rawDecoded
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintPromptCacheKey_MapRawEquivalence(t *testing.T) {
|
|||
|
|
for _, mode := range []codexFingerprintMode{codexFingerprintSession, codexFingerprintFull} {
|
|||
|
|
t.Run(string(mode)+"/default", func(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(4300, map[string]any{codexFingerprintModeExtraKey: string(mode)})
|
|||
|
|
ids := resolveCodexFingerprintIDs(account, "header-session", mode)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"body-session","client_metadata":{"session_id":" body-session ","trace":"keep"},"input":[]}`)
|
|||
|
|
mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
|
|||
|
|
|
|||
|
|
require.Equal(t, mapBody["prompt_cache_key"], rawBody["prompt_cache_key"])
|
|||
|
|
require.Equal(t, ids.sessionID, mapBody["prompt_cache_key"])
|
|||
|
|
mapCM, _ := mapBody["client_metadata"].(map[string]any)
|
|||
|
|
rawCM, _ := rawBody["client_metadata"].(map[string]any)
|
|||
|
|
require.Equal(t, ids.sessionID, mapCM["session_id"])
|
|||
|
|
require.Equal(t, mapCM["session_id"], rawCM["session_id"])
|
|||
|
|
require.Equal(t, "keep", rawCM["trace"])
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
t.Run("explicit override", func(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(4301, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
ids := resolveCodexFingerprintIDs(account, "header-session", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol","prompt_cache_key":"explicit-cache","client_metadata":{"session_id":"body-session"},"input":[]}`)
|
|||
|
|
mapBody, rawBody := applyMapAndRawFingerprintBodiesForTest(t, body, ids)
|
|||
|
|
|
|||
|
|
require.Equal(t, "explicit-cache", mapBody["prompt_cache_key"])
|
|||
|
|
require.Equal(t, "explicit-cache", rawBody["prompt_cache_key"])
|
|||
|
|
mapCM, _ := mapBody["client_metadata"].(map[string]any)
|
|||
|
|
rawCM, _ := rawBody["client_metadata"].(map[string]any)
|
|||
|
|
require.Equal(t, ids.sessionID, mapCM["session_id"])
|
|||
|
|
require.Equal(t, ids.sessionID, rawCM["session_id"])
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintPromptCacheKey_Negatives(t *testing.T) {
|
|||
|
|
sessionAccount := newTestOAuthAccount(4310, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
sessionIDs := resolveCodexFingerprintIDs(sessionAccount, "header-session", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, sessionIDs)
|
|||
|
|
deviceAccount := newTestOAuthAccount(4311, map[string]any{codexFingerprintModeExtraKey: "device"})
|
|||
|
|
deviceIDs := resolveCodexFingerprintIDs(deviceAccount, "header-session", codexFingerprintDevice)
|
|||
|
|
require.NotNil(t, deviceIDs)
|
|||
|
|
|
|||
|
|
tests := []struct {
|
|||
|
|
name string
|
|||
|
|
body []byte
|
|||
|
|
ids *codexFingerprintIDs
|
|||
|
|
wantExists bool
|
|||
|
|
wantCacheKey any
|
|||
|
|
wantRawString string
|
|||
|
|
}{
|
|||
|
|
{
|
|||
|
|
name: "missing key is not injected",
|
|||
|
|
body: []byte(`{"client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: false,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "empty key preserved",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"","client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "whitespace-different key is an explicit override",
|
|||
|
|
body: []byte(`{"prompt_cache_key":" body-session ","client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: " body-session ",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "non-string key preserved",
|
|||
|
|
body: []byte(`{"prompt_cache_key":123,"client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: float64(123),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "missing source metadata preserves key",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"body-session"}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "body-session",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "non-string source session preserves key",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"123","client_metadata":{"session_id":123}}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "123",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "non-object source metadata preserves key",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":"bad"}`),
|
|||
|
|
ids: sessionIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "body-session",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "device mode preserves key",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: deviceIDs,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "body-session",
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
name: "off mode preserves body",
|
|||
|
|
body: []byte(`{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`),
|
|||
|
|
ids: nil,
|
|||
|
|
wantExists: true,
|
|||
|
|
wantCacheKey: "body-session",
|
|||
|
|
wantRawString: `{"prompt_cache_key":"body-session","client_metadata":{"session_id":"body-session"}}`,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, tt := range tests {
|
|||
|
|
t.Run(tt.name, func(t *testing.T) {
|
|||
|
|
var mapBody map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(tt.body, &mapBody))
|
|||
|
|
changedMap := applyCodexFingerprintClientMetadata(mapBody, cloneCodexFingerprintIDsForTest(tt.ids))
|
|||
|
|
|
|||
|
|
rawBody, changedRaw, err := applyCodexFingerprintClientMetadataRaw(tt.body, cloneCodexFingerprintIDsForTest(tt.ids))
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
if tt.ids == nil {
|
|||
|
|
require.False(t, changedMap)
|
|||
|
|
require.False(t, changedRaw)
|
|||
|
|
require.JSONEq(t, tt.wantRawString, string(rawBody))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
require.True(t, changedMap)
|
|||
|
|
require.True(t, changedRaw)
|
|||
|
|
|
|||
|
|
rawDecoded := map[string]any{}
|
|||
|
|
require.NoError(t, json.Unmarshal(rawBody, &rawDecoded))
|
|||
|
|
_, mapExists := mapBody["prompt_cache_key"]
|
|||
|
|
_, rawExists := rawDecoded["prompt_cache_key"]
|
|||
|
|
require.Equal(t, tt.wantExists, mapExists)
|
|||
|
|
require.Equal(t, tt.wantExists, rawExists)
|
|||
|
|
if tt.wantExists {
|
|||
|
|
require.Equal(t, tt.wantCacheKey, mapBody["prompt_cache_key"])
|
|||
|
|
require.Equal(t, tt.wantCacheKey, rawDecoded["prompt_cache_key"])
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadataRaw_MatchesMapVariant(t *testing.T) {
|
|||
|
|
embedded := `{\"installation_id\":\"real-install\",\"session_id\":\"real-session\",\"sandbox\":\"seatbelt\"}`
|
|||
|
|
bodies := map[string]string{
|
|||
|
|
"no_client_metadata": `{"model":"gpt-5.6-sol","input":[],"stream":true}`,
|
|||
|
|
"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}`,
|
|||
|
|
"non_object_value": `{"model":"gpt-5.6-sol","client_metadata":"bogus","stream":true}`,
|
|||
|
|
}
|
|||
|
|
for _, mode := range []codexFingerprintMode{codexFingerprintDevice, codexFingerprintSession, codexFingerprintFull} {
|
|||
|
|
account := newTestOAuthAccount(4242, map[string]any{codexFingerprintModeExtraKey: string(mode)})
|
|||
|
|
ids := resolveCodexFingerprintIDs(account, "client-sess-raw", mode)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
for name, body := range bodies {
|
|||
|
|
t.Run(string(mode)+"/"+name, func(t *testing.T) {
|
|||
|
|
mapCM, rawCM := rawVsMapClientMetadata(t, []byte(body), ids)
|
|||
|
|
assert.Equal(t, mapCM, rawCM, "raw 字节版与 map 版的 client_metadata 结果必须逐点一致")
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadataRaw_PreservesUnrelatedFields(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(4243, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
ids := resolveCodexFingerprintIDs(account, "client-sess-preserve", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol","input":[{"type":"message","role":"user","content":"hi"}],"stream":true,"prompt_cache_key":"pck-1"}`)
|
|||
|
|
out, changed, err := applyCodexFingerprintClientMetadataRaw(body, ids)
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
require.True(t, changed)
|
|||
|
|
|
|||
|
|
var decoded map[string]any
|
|||
|
|
require.NoError(t, json.Unmarshal(out, &decoded))
|
|||
|
|
assert.Equal(t, "gpt-5.6-sol", decoded["model"])
|
|||
|
|
assert.Equal(t, "pck-1", decoded["prompt_cache_key"])
|
|||
|
|
assert.Equal(t, true, decoded["stream"])
|
|||
|
|
cm, _ := decoded["client_metadata"].(map[string]any)
|
|||
|
|
require.NotNil(t, cm)
|
|||
|
|
assert.Equal(t, ids.sessionID, cm["session_id"])
|
|||
|
|
assert.Equal(t, ids.turnID, cm["turn_id"])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadataRaw_Noop(t *testing.T) {
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol"}`)
|
|||
|
|
out, changed, err := applyCodexFingerprintClientMetadataRaw(body, nil)
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
assert.False(t, changed)
|
|||
|
|
assert.Equal(t, body, out)
|
|||
|
|
|
|||
|
|
out, changed, err = applyCodexFingerprintClientMetadataRaw(nil, &codexFingerprintIDs{mode: codexFingerprintSession, installationID: "x"})
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
assert.False(t, changed)
|
|||
|
|
assert.Nil(t, out)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- context 暂存与出站头应用(透传/非透传共用 seam)---
|
|||
|
|
|
|||
|
|
func newFingerprintStageTestContext(t *testing.T) *gin.Context {
|
|||
|
|
t.Helper()
|
|||
|
|
gin.SetMode(gin.TestMode)
|
|||
|
|
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
|||
|
|
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestStageCodexFingerprintIDs_NilOverwritesPreviousAccount(t *testing.T) {
|
|||
|
|
c := newFingerprintStageTestContext(t)
|
|||
|
|
accountA := newTestOAuthAccount(1001, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
idsA := resolveCodexFingerprintIDs(accountA, "sess-x", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, idsA)
|
|||
|
|
stageCodexFingerprintIDs(c, idsA)
|
|||
|
|
|
|||
|
|
// failover 切到 off 模式账号:无条件覆写为 nil,上一账号 IDs 不得残留
|
|||
|
|
stageCodexFingerprintIDs(c, nil)
|
|||
|
|
|
|||
|
|
h := http.Header{}
|
|||
|
|
h.Set("session_id", "isolated-session")
|
|||
|
|
accountB := newTestOAuthAccount(1002, map[string]any{"codex_fingerprint_mode": "off"})
|
|||
|
|
applyStagedCodexFingerprintHeaders(c, accountB, h)
|
|||
|
|
assert.Equal(t, "isolated-session", h.Get("session_id"), "off 账号不得应用上一账号的收敛 ID")
|
|||
|
|
assert.Empty(t, h.Get("x-codex-installation-id"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyStagedCodexFingerprintRejectsDifferentOAuthAccount(t *testing.T) {
|
|||
|
|
c := newFingerprintStageTestContext(t)
|
|||
|
|
accountA := newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
idsA := resolveCodexFingerprintIDs(accountA, "sess-a", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, idsA)
|
|||
|
|
stageCodexFingerprintIDs(c, idsA)
|
|||
|
|
|
|||
|
|
accountB := newTestOAuthAccount(1004, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
h := make(http.Header)
|
|||
|
|
h.Set("session-id", "account-b-session")
|
|||
|
|
applyStagedCodexFingerprintHeaders(c, accountB, h)
|
|||
|
|
assert.Equal(t, "account-b-session", h.Get("session-id"))
|
|||
|
|
assert.Empty(t, h.Get("x-codex-installation-id"))
|
|||
|
|
|
|||
|
|
body := map[string]any{"client_metadata": map[string]any{"session_id": "account-b-session"}}
|
|||
|
|
assert.False(t, applyStagedCodexFingerprintClientMetadata(c, accountB, body))
|
|||
|
|
clientMetadata, ok := body["client_metadata"].(map[string]any)
|
|||
|
|
require.True(t, ok)
|
|||
|
|
assert.Equal(t, "account-b-session", clientMetadata["session_id"])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyStagedCodexFingerprintHeaders_SkipsNonOAuthAccount(t *testing.T) {
|
|||
|
|
c := newFingerprintStageTestContext(t)
|
|||
|
|
oauthIDs := resolveCodexFingerprintIDs(newTestOAuthAccount(1003, map[string]any{codexFingerprintModeExtraKey: "session"}), "sess-y", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, oauthIDs)
|
|||
|
|
stageCodexFingerprintIDs(c, oauthIDs)
|
|||
|
|
|
|||
|
|
h := http.Header{}
|
|||
|
|
apiKeyAccount := &Account{ID: 1004, Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
|
|||
|
|
applyStagedCodexFingerprintHeaders(c, apiKeyAccount, h)
|
|||
|
|
assert.Empty(t, h.Get("x-codex-installation-id"), "stale 收敛 ID 不得应用到非 OAuth 账号")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestBuildUpstreamRequestOpenAIPassthrough_AppliesStagedFingerprint(t *testing.T) {
|
|||
|
|
svc := &OpenAIGatewayService{}
|
|||
|
|
// 收敛是显式 opt-in(#5610):显式开启后验证透传路径的出站头收敛。
|
|||
|
|
account := newTestOAuthAccount(2001, map[string]any{
|
|||
|
|
"openai_oauth_passthrough": true,
|
|||
|
|
"codex_fingerprint_mode": "session",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
c := newFingerprintStageTestContext(t)
|
|||
|
|
c.Request.Header.Set("session_id", "real-client-session")
|
|||
|
|
c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1 (Ubuntu 22.4.0; x86_64) xterm-256color")
|
|||
|
|
c.Request.Header.Set("originator", "codex_cli_rs")
|
|||
|
|
c.Request.Header.Set("x-codex-turn-metadata", `{"installation_id":"real-install","session_id":"real-session","sandbox":"seatbelt"}`)
|
|||
|
|
|
|||
|
|
// 复刻 forwardOpenAIPassthrough 的解析+暂存 seam(默认 session 模式)
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, c.Request.Header)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
stageCodexFingerprintIDs(c, ids)
|
|||
|
|
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol","input":[],"stream":true}`)
|
|||
|
|
req, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, account, body, "test-token")
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
|
|||
|
|
assert.Equal(t, ids.sessionID, req.Header.Get("session_id"), "session 模式下出站 session_id 应为账号级收敛值")
|
|||
|
|
assert.Equal(t, ids.installationID, req.Header.Get("x-codex-installation-id"))
|
|||
|
|
assert.Equal(t, ids.windowID, req.Header.Get("x-codex-window-id"))
|
|||
|
|
assert.Equal(t, ids.threadID, req.Header.Get("x-client-request-id"))
|
|||
|
|
turnMetadata := req.Header.Get("x-codex-turn-metadata")
|
|||
|
|
require.NotEmpty(t, turnMetadata)
|
|||
|
|
assert.Contains(t, turnMetadata, ids.sessionID, "turn-metadata JSON 中的 session_id 应被收敛")
|
|||
|
|
assert.Contains(t, turnMetadata, `"sandbox":"seatbelt"`, "turn-metadata 未指定字段应原样保留")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestBuildUpstreamRequestOpenAIPassthrough_OffModeKeepsIsolatedSession(t *testing.T) {
|
|||
|
|
svc := &OpenAIGatewayService{}
|
|||
|
|
account := newTestOAuthAccount(2002, map[string]any{
|
|||
|
|
"openai_oauth_passthrough": true,
|
|||
|
|
"codex_fingerprint_mode": "off",
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
c := newFingerprintStageTestContext(t)
|
|||
|
|
c.Request.Header.Set("session_id", "real-client-session")
|
|||
|
|
c.Request.Header.Set("originator", "codex_cli_rs")
|
|||
|
|
|
|||
|
|
ids := resolveCodexFingerprintIDsFromRequest(account, c.Request.Header)
|
|||
|
|
require.Nil(t, ids)
|
|||
|
|
stageCodexFingerprintIDs(c, ids)
|
|||
|
|
|
|||
|
|
body := []byte(`{"model":"gpt-5.6-sol","input":[],"stream":true}`)
|
|||
|
|
req, err := svc.buildUpstreamRequestOpenAIPassthrough(context.Background(), c, account, body, "test-token")
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
|
|||
|
|
assert.NotEmpty(t, req.Header.Get("session_id"))
|
|||
|
|
assert.NotEqual(t, resolveConvergedSessionID(testCodexFingerprintSeed), req.Header.Get("session_id"), "off 模式不得收敛 session_id")
|
|||
|
|
assert.Empty(t, req.Header.Get("x-codex-window-id"))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func TestApplyCodexFingerprintClientMetadataRaw_NonObjectBodyUntouched(t *testing.T) {
|
|||
|
|
account := newTestOAuthAccount(4244, map[string]any{codexFingerprintModeExtraKey: "session"})
|
|||
|
|
ids := resolveCodexFingerprintIDs(account, "client-sess-nonobj", codexFingerprintSession)
|
|||
|
|
require.NotNil(t, ids)
|
|||
|
|
|
|||
|
|
for _, body := range []string{`[1,2,3]`, `"plain string"`, `not json at all`} {
|
|||
|
|
out, changed, err := applyCodexFingerprintClientMetadataRaw([]byte(body), ids)
|
|||
|
|
require.NoError(t, err)
|
|||
|
|
assert.False(t, changed, "非 JSON 对象 body 不应被改写: %s", body)
|
|||
|
|
assert.Equal(t, []byte(body), out)
|
|||
|
|
}
|
|||
|
|
}
|