mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-03 06:35:00 +08:00
- Introduced agent-specific cache key isolation to ensure distinct prompt caching across agents. - Improved reasoning replay handling for Claude models, including cumulative tool turn restoration and validation. - Enhanced HTTP and WebSocket session consistency during execution scope and cache replay synchronization. - Added new unit tests and extended coverage for prompt caching, agent isolation, and replay cache functionality. Closes: #4352
123 lines
4.9 KiB
Go
123 lines
4.9 KiB
Go
package helps
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func TestExtractClaudeCodeSessionIDFromPayloadJSON(t *testing.T) {
|
|
payload := []byte(`{"metadata":{"user_id":"{\"device_id\":\"d\",\"session_id\":\"cache-session-1\"}"}}`)
|
|
got := ExtractClaudeCodeSessionID(context.Background(), payload, nil)
|
|
if got != "cache-session-1" {
|
|
t.Fatalf("ExtractClaudeCodeSessionID() = %q, want cache-session-1", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractClaudeCodeSessionIDFromHeader(t *testing.T) {
|
|
recorder := httptest.NewRecorder()
|
|
ginCtx, _ := gin.CreateTestContext(recorder)
|
|
ginCtx.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
|
ginCtx.Request.Header.Set(ClaudeCodeSessionHeader, "header-session-1")
|
|
ctx := context.WithValue(context.Background(), "gin", ginCtx)
|
|
|
|
got := ExtractClaudeCodeSessionID(ctx, []byte(`{"model":"gpt-5.4"}`), nil)
|
|
if got != "header-session-1" {
|
|
t.Fatalf("ExtractClaudeCodeSessionID() = %q, want header-session-1", got)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodePromptCacheStableAcrossRequests(t *testing.T) {
|
|
ctx := context.Background()
|
|
payload := []byte(`{"metadata":{"user_id":"{\"session_id\":\"cache-session-2\"}"}}`)
|
|
first, ok, err := ClaudeCodePromptCache(ctx, "grok-composer-2.5-fast", payload, nil)
|
|
if err != nil {
|
|
t.Fatalf("ClaudeCodePromptCache first error: %v", err)
|
|
}
|
|
if !ok || first.ID == "" {
|
|
t.Fatalf("ClaudeCodePromptCache first = %#v, ok=%v, want cached id", first, ok)
|
|
}
|
|
second, ok, err := ClaudeCodePromptCache(ctx, "grok-composer-2.5-fast", payload, nil)
|
|
if err != nil {
|
|
t.Fatalf("ClaudeCodePromptCache second error: %v", err)
|
|
}
|
|
if !ok || second.ID != first.ID {
|
|
t.Fatalf("second cache id = %q, want %q", second.ID, first.ID)
|
|
}
|
|
}
|
|
|
|
func TestExtractClaudeCodeSessionIDPrefersHeaderOverPayload(t *testing.T) {
|
|
payload := []byte(`{"metadata":{"user_id":"{"session_id":"payload-session"}"}}`)
|
|
headers := http.Header{}
|
|
headers.Set(ClaudeCodeSessionHeader, "header-session")
|
|
|
|
got := ExtractClaudeCodeSessionID(context.Background(), payload, headers)
|
|
if got != "header-session" {
|
|
t.Fatalf("ExtractClaudeCodeSessionID() = %q, want header-session", got)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeExecutionScopeAcceptsLowercaseHeaderMapKeys(t *testing.T) {
|
|
headers := http.Header{
|
|
"x-claude-code-session-id": []string{"lower-session"},
|
|
"x-claude-code-agent-id": []string{"lower-agent"},
|
|
}
|
|
|
|
scope, ok := ClaudeCodeExecutionScope(context.Background(), nil, headers)
|
|
if !ok || scope != "claude:lower-session:agent:lower-agent" {
|
|
t.Fatalf("lowercase header scope = %q, %v", scope, ok)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodeExecutionScopeIsolatesAgents(t *testing.T) {
|
|
rootHeaders := http.Header{}
|
|
rootHeaders.Set(ClaudeCodeSessionHeader, "session-agents")
|
|
childAHeaders := rootHeaders.Clone()
|
|
childAHeaders.Set(ClaudeCodeAgentHeader, "agent-a")
|
|
childBHeaders := rootHeaders.Clone()
|
|
childBHeaders.Set(ClaudeCodeAgentHeader, "agent-b")
|
|
|
|
rootScope, ok := ClaudeCodeExecutionScope(context.Background(), nil, rootHeaders)
|
|
if !ok || rootScope != "claude:session-agents:agent:main" {
|
|
t.Fatalf("root scope = %q, %v", rootScope, ok)
|
|
}
|
|
childAScope, ok := ClaudeCodeExecutionScope(context.Background(), nil, childAHeaders)
|
|
if !ok || childAScope != "claude:session-agents:agent:agent-a" {
|
|
t.Fatalf("child A scope = %q, %v", childAScope, ok)
|
|
}
|
|
childBScope, ok := ClaudeCodeExecutionScope(context.Background(), nil, childBHeaders)
|
|
if !ok || childBScope != "claude:session-agents:agent:agent-b" {
|
|
t.Fatalf("child B scope = %q, %v", childBScope, ok)
|
|
}
|
|
if rootScope == childAScope || childAScope == childBScope || rootScope == childBScope {
|
|
t.Fatalf("agent scopes are not isolated: root=%q a=%q b=%q", rootScope, childAScope, childBScope)
|
|
}
|
|
}
|
|
|
|
func TestClaudeCodePromptCacheDeterministicAndAgentScoped(t *testing.T) {
|
|
rootHeaders := http.Header{}
|
|
rootHeaders.Set(ClaudeCodeSessionHeader, "session-cache-agents")
|
|
childHeaders := rootHeaders.Clone()
|
|
childHeaders.Set(ClaudeCodeAgentHeader, "agent-a")
|
|
|
|
rootFirst, ok, errFirst := ClaudeCodePromptCache(context.Background(), "gpt-5.4", nil, rootHeaders)
|
|
if errFirst != nil || !ok {
|
|
t.Fatalf("root first cache = %#v, %v, %v", rootFirst, ok, errFirst)
|
|
}
|
|
rootSecond, ok, errSecond := ClaudeCodePromptCache(context.Background(), "gpt-5.4", nil, rootHeaders)
|
|
if errSecond != nil || !ok || rootSecond.ID != rootFirst.ID {
|
|
t.Fatalf("root second cache = %#v, %v, %v; want ID %q", rootSecond, ok, errSecond, rootFirst.ID)
|
|
}
|
|
child, ok, errChild := ClaudeCodePromptCache(context.Background(), "gpt-5.4", nil, childHeaders)
|
|
if errChild != nil || !ok || child.ID == rootFirst.ID {
|
|
t.Fatalf("child cache = %#v, %v, %v; root ID %q", child, ok, errChild, rootFirst.ID)
|
|
}
|
|
otherModel, ok, errModel := ClaudeCodePromptCache(context.Background(), "gpt-5.5", nil, rootHeaders)
|
|
if errModel != nil || !ok || otherModel.ID == rootFirst.ID {
|
|
t.Fatalf("other model cache = %#v, %v, %v; root ID %q", otherModel, ok, errModel, rootFirst.ID)
|
|
}
|
|
}
|