Files
CLIProxyAPI/internal/runtime/executor/helps/claude_code_session_test.go
Luis Pater baffbe2cc7 feat(executor): implement agent-scoped cache key isolation and enhance replay handling
- 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
2026-07-18 02:18:09 +08:00

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)
}
}