| package executor |
|
|
| import ( |
| "bytes" |
| "context" |
| "io" |
| "testing" |
| "time" |
|
|
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" |
| cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" |
| sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" |
| "github.com/tidwall/gjson" |
| ) |
|
|
| func resetCodexCacheForTest() { |
| codexCacheMu.Lock() |
| codexCacheMap = make(map[string]codexCache) |
| codexCacheMu.Unlock() |
| } |
|
|
| func readRequestBodyForTest(t *testing.T, reqBody io.Reader) []byte { |
| t.Helper() |
| data, err := io.ReadAll(reqBody) |
| if err != nil { |
| t.Fatalf("read request body: %v", err) |
| } |
| return data |
| } |
|
|
| func TestApplyCodexPromptCache_ClaudeCachesByModelAndUserID(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| reqPayload := []byte(`{"metadata":{"user_id":"user-123"}}`) |
| rawJSON := []byte(`{"input":[]}`) |
|
|
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("claude"), reqPayload, "gpt-5", rawJSON, false) |
| if cacheID == "" { |
| t.Fatal("expected non-empty cache ID") |
| } |
| if got := gjson.GetBytes(out, "prompt_cache_key").String(); got != cacheID { |
| t.Fatalf("prompt_cache_key = %q, want %q", got, cacheID) |
| } |
| if got := headers.Get("Conversation_id"); got != cacheID { |
| t.Fatalf("Conversation_id = %q, want %q", got, cacheID) |
| } |
| if got := headers.Get("Session_id"); got != cacheID { |
| t.Fatalf("Session_id = %q, want %q", got, cacheID) |
| } |
|
|
| _, cacheID2, _ := applyCodexPromptCache(nil, sdktranslator.FromString("claude"), reqPayload, "gpt-5", []byte(`{"input":[1]}`), false) |
| if cacheID2 != cacheID { |
| t.Fatalf("expected stable cache ID for same model+user: first=%q second=%q", cacheID, cacheID2) |
| } |
|
|
| _, cacheIDOtherModel, _ := applyCodexPromptCache(nil, sdktranslator.FromString("claude"), reqPayload, "gpt-4.1", []byte(`{"input":[]}`), false) |
| if cacheIDOtherModel == cacheID { |
| t.Fatalf("expected different cache ID for different model, got %q", cacheIDOtherModel) |
| } |
| } |
|
|
| func TestApplyCodexPromptCache_OpenAIResponseUsesPromptCacheKey(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| reqPayload := []byte(`{"prompt_cache_key":"pck-123"}`) |
| rawJSON := []byte(`{"input":[]}`) |
|
|
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("openai-response"), reqPayload, "gpt-5", rawJSON, false) |
| if cacheID != "pck-123" { |
| t.Fatalf("cacheID = %q, want %q", cacheID, "pck-123") |
| } |
| if got := gjson.GetBytes(out, "prompt_cache_key").String(); got != "pck-123" { |
| t.Fatalf("prompt_cache_key = %q, want %q", got, "pck-123") |
| } |
| if got := headers.Get("Conversation_id"); got != "pck-123" { |
| t.Fatalf("Conversation_id = %q, want %q", got, "pck-123") |
| } |
| if got := headers.Get("Session_id"); got != "pck-123" { |
| t.Fatalf("Session_id = %q, want %q", got, "pck-123") |
| } |
| } |
|
|
| func TestApplyCodexPromptCache_OpenAIResponseRetentionDoesNotDisableExplicitKey(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| reqPayload := []byte(`{"prompt_cache_key":"pck-retained","prompt_cache_retention":"off"}`) |
| rawJSON := []byte(`{"input":[]}`) |
|
|
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("openai-response"), reqPayload, "gpt-5", rawJSON, false) |
| if cacheID != "pck-retained" { |
| t.Fatalf("cacheID = %q, want %q", cacheID, "pck-retained") |
| } |
| if got := gjson.GetBytes(out, "prompt_cache_key").String(); got != "pck-retained" { |
| t.Fatalf("prompt_cache_key = %q, want %q", got, "pck-retained") |
| } |
| if got := headers.Get("Conversation_id"); got != "pck-retained" { |
| t.Fatalf("Conversation_id = %q, want %q", got, "pck-retained") |
| } |
| if got := headers.Get("Session_id"); got != "pck-retained" { |
| t.Fatalf("Session_id = %q, want %q", got, "pck-retained") |
| } |
| } |
|
|
| func TestApplyCodexPromptCache_NoCacheInputLeavesPayloadUnchanged(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| rawJSON := []byte(`{"input":[]}`) |
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("codex"), []byte(`{"input":[]}`), "gpt-5", rawJSON, false) |
| if cacheID != "" { |
| t.Fatalf("cacheID = %q, want empty", cacheID) |
| } |
| if len(headers) != 0 { |
| t.Fatalf("headers = %#v, want empty", headers) |
| } |
| if !bytes.Equal(out, rawJSON) { |
| t.Fatalf("payload changed unexpectedly: got=%s want=%s", string(out), string(rawJSON)) |
| } |
| if gjson.GetBytes(out, "prompt_cache_key").Exists() { |
| t.Fatalf("unexpected prompt_cache_key in payload: %s", string(out)) |
| } |
| } |
|
|
| func TestApplyCodexPromptCacheHeaders_SkipsWhenBodyEmpty(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| out, headers := applyCodexPromptCacheHeaders( |
| nil, |
| sdktranslator.FromString("claude"), |
| []byte(`{"metadata":{"user_id":"user-123"}}`), |
| "gpt-5", |
| []byte{}, |
| ) |
| if len(out) != 0 { |
| t.Fatalf("expected empty output body, got: %s", string(out)) |
| } |
| if len(headers) != 0 { |
| t.Fatalf("expected empty headers, got: %#v", headers) |
| } |
| } |
|
|
| func TestApplyCodexPromptCache_CodexRetentionAndUserAutoCache(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| reqPayload := []byte(`{"user":"user-xyz","prompt_cache_retention":"24h"}`) |
| rawJSON := []byte(`{"input":[]}`) |
|
|
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("codex"), reqPayload, "gpt-5", rawJSON, false) |
| if cacheID == "" { |
| t.Fatal("expected non-empty cache ID") |
| } |
| if got := gjson.GetBytes(out, "prompt_cache_key").String(); got != cacheID { |
| t.Fatalf("prompt_cache_key = %q, want %q", got, cacheID) |
| } |
| if got := headers.Get("Conversation_id"); got != cacheID { |
| t.Fatalf("Conversation_id = %q, want %q", got, cacheID) |
| } |
| if got := headers.Get("Session_id"); got != cacheID { |
| t.Fatalf("Session_id = %q, want %q", got, cacheID) |
| } |
| } |
|
|
| func TestApplyCodexPromptCache_RetentionDisabledSkipsAutoCache(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| reqPayload := []byte(`{"user":"user-xyz","prompt_cache_retention":"off"}`) |
| rawJSON := []byte(`{"input":[]}`) |
|
|
| out, cacheID, headers := applyCodexPromptCache(nil, sdktranslator.FromString("codex"), reqPayload, "gpt-5", rawJSON, false) |
| if cacheID != "" { |
| t.Fatalf("cacheID = %q, want empty", cacheID) |
| } |
| if len(headers) != 0 { |
| t.Fatalf("headers = %#v, want empty", headers) |
| } |
| if gjson.GetBytes(out, "prompt_cache_key").Exists() { |
| t.Fatalf("prompt_cache_key should not be set when retention disables cache") |
| } |
| } |
|
|
| func TestCodexPromptCacheEffectiveTTL_PreservesSubMinuteConfiguredTTL(t *testing.T) { |
| ttl := codexPromptCacheEffectiveTTL(&config.Config{CodexPromptCache: config.CodexPromptCacheConfig{TTLSeconds: 7}}, nil) |
| if ttl != 7*time.Second { |
| t.Fatalf("ttl = %v, want %v", ttl, 7*time.Second) |
| } |
| } |
|
|
| func TestCodexPromptCacheEffectiveTTL_PreservesSubMinuteRetentionTTL(t *testing.T) { |
| reqPayload := []byte(`{"prompt_cache_retention":5}`) |
| ttl := codexPromptCacheEffectiveTTL(nil, reqPayload) |
| if ttl != 5*time.Second { |
| t.Fatalf("ttl = %v, want %v", ttl, 5*time.Second) |
| } |
| } |
|
|
| func TestCodexExecutorCacheHelper_AppliesUnifiedCacheLogic(t *testing.T) { |
| resetCodexCacheForTest() |
|
|
| exec := &CodexExecutorRefactored{} |
| httpReq, err := exec.cacheHelper( |
| context.Background(), |
| sdktranslator.FromString("openai-response"), |
| "https://example.com/responses", |
| cliproxyexecutor.Request{ |
| Model: "gpt-5", |
| Payload: []byte(`{"prompt_cache_key":"pck-456"}`), |
| }, |
| []byte(`{"input":[]}`), |
| ) |
| if err != nil { |
| t.Fatalf("cacheHelper error: %v", err) |
| } |
|
|
| if got := httpReq.Header.Get("Conversation_id"); got != "pck-456" { |
| t.Fatalf("Conversation_id = %q, want %q", got, "pck-456") |
| } |
| if got := httpReq.Header.Get("Session_id"); got != "pck-456" { |
| t.Fatalf("Session_id = %q, want %q", got, "pck-456") |
| } |
|
|
| body := readRequestBodyForTest(t, httpReq.Body) |
| if got := gjson.GetBytes(body, "prompt_cache_key").String(); got != "pck-456" { |
| t.Fatalf("prompt_cache_key = %q, want %q", got, "pck-456") |
| } |
| } |
|
|