package claude import ( "bytes" "context" "strings" "testing" "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" ) // ============================================================================ // Signature Caching Tests // ============================================================================ func TestConvertAntigravityResponseToClaude_ParamsInitialized(t *testing.T) { cache.ClearSignatureCache("") // Request with user message - should initialize params 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) params := param.(*Params) if !params.HasFirstResponse { t.Error("HasFirstResponse should be set after first chunk") } if params.CurrentThinkingText.Len() == 0 { t.Error("Thinking text should be accumulated") } } 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(`{ "model": "claude-sonnet-4-5-thinking", "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) thinkingText := params.CurrentThinkingText.String() 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("claude-sonnet-4-5-thinking", 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(`{ "model": "claude-sonnet-4-5-thinking", "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) firstThinkingText := params.CurrentThinkingText.String() ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, block1Sig, ¶m) // Verify first signature cached if cache.GetCachedSignature("claude-sonnet-4-5-thinking", 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("claude-sonnet-4-5-thinking", secondThinkingText) != validSig2 { t.Error("Second thinking block signature should be cached") } } func TestConvertAntigravityResponseToClaude_TextAndSignatureInSameChunk(t *testing.T) { cache.ClearSignatureCache("") requestJSON := []byte(`{ "model": "claude-sonnet-4-5-thinking", "messages": [{"role": "user", "content": [{"type": "text", "text": "Test"}]}] }`) validSignature := "RtestSig1234567890123456789012345678901234567890123456789" // Chunk 1: thinking text only (no signature) chunk1 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "First part.", "thought": true}] } }] } }`) // Chunk 2: thinking text AND signature in the same part chunk2 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": " Second part.", "thought": true, "thoughtSignature": "` + validSignature + `"}] } }] } }`) var param any ctx := context.Background() result1 := ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk1, ¶m) result2 := ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk2, ¶m) allOutput := string(bytes.Join(result1, nil)) + string(bytes.Join(result2, nil)) // The text " Second part." must appear as a thinking_delta, not be silently dropped if !strings.Contains(allOutput, "Second part.") { t.Error("Text co-located with signature must be emitted as thinking_delta before the signature") } // The signature must also be emitted if !strings.Contains(allOutput, "signature_delta") { t.Error("Signature delta must still be emitted") } // Verify the cached signature covers the FULL text (both parts) fullText := "First part. Second part." cachedSig := cache.GetCachedSignature("claude-sonnet-4-5-thinking", fullText) if cachedSig != validSignature { t.Errorf("Cached signature should cover full text %q, got sig=%q", fullText, cachedSig) } } func TestConvertAntigravityResponseToClaude_SignatureOnlyChunk(t *testing.T) { cache.ClearSignatureCache("") requestJSON := []byte(`{ "model": "claude-sonnet-4-5-thinking", "messages": [{"role": "user", "content": [{"type": "text", "text": "Test"}]}] }`) validSignature := "RtestSig1234567890123456789012345678901234567890123456789" // Chunk 1: thinking text chunk1 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "Full thinking text.", "thought": true}] } }] } }`) // Chunk 2: signature only (empty text) — the normal case chunk2 := []byte(`{ "response": { "candidates": [{ "content": { "parts": [{"text": "", "thought": true, "thoughtSignature": "` + validSignature + `"}] } }] } }`) var param any ctx := context.Background() ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk1, ¶m) ConvertAntigravityResponseToClaude(ctx, "claude-sonnet-4-5-thinking", requestJSON, requestJSON, chunk2, ¶m) cachedSig := cache.GetCachedSignature("claude-sonnet-4-5-thinking", "Full thinking text.") if cachedSig != validSignature { t.Errorf("Signature-only chunk should still cache correctly, got %q", cachedSig) } }