package claude import ( "context" "strings" "testing" "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" ) // ============================================================================ // Signature Caching Tests // ============================================================================ func TestConvertAntigravityResponseToClaude_SessionIDDerived(t *testing.T) { cache.ClearSignatureCache("") // Request with user message - should derive session ID requestJSON := []byte(`{ "messages": [ {"role": "user", "content": [{"type": "text", "text": "Hello world"}]} ] }`) // First response chunk with thinking responseJSON := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "Let me think...", "thought": true}] } }] } }`) var param any ctx := context.Background() ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, responseJSON, ¶m) // Verify session ID was set params := param.(*Params) if params.SessionID == "" { t.Error("SessionID should be derived from request") } } func TestConvertAntigravityResponseToClaude_ThinkingTextAccumulated(t *testing.T) { cache.ClearSignatureCache("") requestJSON := []byte(`{ "messages": [{"role": "user", "content": [{"type": "text", "text": "Test"}]}] }`) // First thinking chunk chunk1 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "First part of thinking...", "thought": true}] } }] } }`) // Second thinking chunk (continuation) chunk2 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": " Second part of thinking...", "thought": true}] } }] } }`) var param any ctx := context.Background() // Process first chunk - starts new thinking block ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk1, ¶m) params := param.(*Params) if params.CurrentThinkingText.Len() == 0 { t.Error("Thinking text should be accumulated after first chunk") } // Process second chunk - continues thinking block ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk2, ¶m) text := params.CurrentThinkingText.String() if !strings.Contains(text, "First part") || !strings.Contains(text, "Second part") { t.Errorf("Thinking text should accumulate both parts, got: %s", text) } } func TestConvertAntigravityResponseToClaude_SignatureCached(t *testing.T) { cache.ClearSignatureCache("") requestJSON := []byte(`{ "messages": [{"role": "user", "content": [{"type": "text", "text": "Cache test"}]}] }`) // Thinking chunk thinkingChunk := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "My thinking process here", "thought": true}] } }] } }`) // Signature chunk validSignature := "abc123validSignature1234567890123456789012345678901234567890" signatureChunk := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSignature + `"}] } }] } }`) var param any ctx := context.Background() // Process thinking chunk ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, thinkingChunk, ¶m) params := param.(*Params) sessionID := params.SessionID thinkingText := params.CurrentThinkingText.String() if sessionID == "" { t.Fatal("SessionID should be set") } if thinkingText == "" { t.Fatal("Thinking text should be accumulated") } // Process signature chunk - should cache the signature ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, signatureChunk, ¶m) // Verify signature was cached cachedSig := cache.GetCachedSignature(sessionID, thinkingText) if cachedSig != validSignature { t.Errorf("Expected cached signature '%s', got '%s'", validSignature, cachedSig) } // Verify thinking text was reset after caching if params.CurrentThinkingText.Len() != 0 { t.Error("Thinking text should be reset after signature is cached") } } func TestConvertAntigravityResponseToClaude_MultipleThinkingBlocks(t *testing.T) { cache.ClearSignatureCache("") requestJSON := []byte(`{ "messages": [{"role": "user", "content": [{"type": "text", "text": "Multi block test"}]}] }`) validSig1 := "signature1_12345678901234567890123456789012345678901234567" validSig2 := "signature2_12345678901234567890123456789012345678901234567" // First thinking block with signature block1Thinking := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "First thinking block", "thought": true}] } }] } }`) block1Sig := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSig1 + `"}] } }] } }`) // Text content (breaks thinking) textBlock := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "Regular text output"}] } }] } }`) // Second thinking block with signature block2Thinking := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "Second thinking block", "thought": true}] } }] } }`) block2Sig := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSig2 + `"}] } }] } }`) var param any ctx := context.Background() // Process first thinking block ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block1Thinking, ¶m) params := param.(*Params) sessionID := params.SessionID firstThinkingText := params.CurrentThinkingText.String() ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block1Sig, ¶m) // Verify first signature cached if cache.GetCachedSignature(sessionID, firstThinkingText) != validSig1 { t.Error("First thinking block signature should be cached") } // Process text (transitions out of thinking) ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, textBlock, ¶m) // Process second thinking block ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block2Thinking, ¶m) secondThinkingText := params.CurrentThinkingText.String() ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block2Sig, ¶m) // Verify second signature cached if cache.GetCachedSignature(sessionID, secondThinkingText) != validSig2 { t.Error("Second thinking block signature should be cached") } } func TestDeriveSessionIDFromRequest(t *testing.T) { tests := []struct { name string input []byte wantEmpty bool }{ { name: "valid user message", input: []byte(`{"messages": [{"role": "user", "content": "Hello"}]}`), wantEmpty: false, }, { name: "user message with content array", input: []byte(`{"messages": [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}]}`), wantEmpty: false, }, { name: "no user message", input: []byte(`{"messages": [{"role": "assistant", "content": "Hi"}]}`), wantEmpty: true, }, { name: "empty messages", input: []byte(`{"messages": []}`), wantEmpty: true, }, { name: "no messages field", input: []byte(`{}`), wantEmpty: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := deriveSessionID(tt.input) if tt.wantEmpty && result != "" { t.Errorf("Expected empty session ID, got '%s'", result) } if !tt.wantEmpty && result == "" { t.Error("Expected non-empty session ID") } }) } } func TestDeriveSessionIDFromRequest_Deterministic(t *testing.T) { input := []byte(`{"messages": [{"role": "user", "content": "Same message"}]}`) id1 := deriveSessionID(input) id2 := deriveSessionID(input) if id1 != id2 { t.Errorf("Session ID should be deterministic: '%s' != '%s'", id1, id2) } } func TestDeriveSessionIDFromRequest_DifferentMessages(t *testing.T) { input1 := []byte(`{"messages": [{"role": "user", "content": "Message A"}]}`) input2 := []byte(`{"messages": [{"role": "user", "content": "Message B"}]}`) id1 := deriveSessionID(input1) id2 := deriveSessionID(input2) if id1 == id2 { t.Error("Different messages should produce different session IDs") } }