mirror of
https://github.com/alibaba/higress.git
synced 2026-03-16 08:20:45 +08:00
test(ai-proxy): add integration tests for Claude Code mode
Add integration tests in test/claude.go for Claude provider: ParseConfig tests: - claude standard config - claude code mode config - claude config without token fails HttpRequestHeaders tests: - claude standard mode uses x-api-key - claude code mode uses bearer authorization - claude code mode adds beta query param - claude code mode with custom version HttpRequestBody tests: - claude standard mode does not inject defaults - claude code mode injects default system prompt - claude code mode injects bash tool - claude code mode preserves existing system prompt - claude code mode does not duplicate bash tool - claude code mode adds bash tool alongside existing tools All tests run in both go and wasm modes.
This commit is contained in:
@@ -150,3 +150,9 @@ func TestBedrock(t *testing.T) {
|
||||
test.RunBedrockOnHttpResponseHeadersTests(t)
|
||||
test.RunBedrockOnHttpResponseBodyTests(t)
|
||||
}
|
||||
|
||||
func TestClaude(t *testing.T) {
|
||||
test.RunClaudeParseConfigTests(t)
|
||||
test.RunClaudeOnHttpRequestHeadersTests(t)
|
||||
test.RunClaudeOnHttpRequestBodyTests(t)
|
||||
}
|
||||
|
||||
463
plugins/wasm-go/extensions/ai-proxy/test/claude.go
Normal file
463
plugins/wasm-go/extensions/ai-proxy/test/claude.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
|
||||
"github.com/higress-group/wasm-go/pkg/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Claude standard mode config
|
||||
var claudeStandardConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-api-key-123"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude Code mode config
|
||||
var claudeCodeModeConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-oat01-oauth-token-456"},
|
||||
"claudeCodeMode": true,
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude Code mode config with custom apiVersion
|
||||
var claudeCodeModeWithVersionConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
"apiTokens": []string{"sk-ant-oat01-oauth-token-789"},
|
||||
"claudeCodeMode": true,
|
||||
"claudeVersion": "2024-01-01",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
// Claude config without token (should fail validation)
|
||||
var claudeNoTokenConfig = func() json.RawMessage {
|
||||
data, _ := json.Marshal(map[string]interface{}{
|
||||
"provider": map[string]interface{}{
|
||||
"type": "claude",
|
||||
},
|
||||
})
|
||||
return data
|
||||
}()
|
||||
|
||||
func RunClaudeParseConfigTests(t *testing.T) {
|
||||
test.RunGoTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
t.Run("claude code mode config", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
config, err := host.GetMatchConfig()
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
})
|
||||
|
||||
t.Run("claude config without token fails", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeNoTokenConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusFailed, status)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunClaudeOnHttpRequestHeadersTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard mode uses x-api-key", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "x-api-key", "sk-ant-api-key-123"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2023-06-01"))
|
||||
|
||||
// Should NOT have Claude Code specific headers
|
||||
_, hasAuth := test.GetHeaderValue(requestHeaders, "authorization")
|
||||
require.False(t, hasAuth, "standard mode should not have authorization header")
|
||||
|
||||
_, hasXApp := test.GetHeaderValue(requestHeaders, "x-app")
|
||||
require.False(t, hasXApp, "standard mode should not have x-app header")
|
||||
})
|
||||
|
||||
t.Run("claude code mode uses bearer authorization", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
|
||||
// Claude Code mode should use Bearer authorization
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "authorization", "Bearer sk-ant-oat01-oauth-token-456"))
|
||||
|
||||
// Should NOT have x-api-key in Claude Code mode
|
||||
_, hasXApiKey := test.GetHeaderValue(requestHeaders, "x-api-key")
|
||||
require.False(t, hasXApiKey, "claude code mode should not have x-api-key header")
|
||||
|
||||
// Should have Claude Code specific headers
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "x-app", "cli"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "user-agent", "claude-cli/2.1.2 (external, cli)"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-beta", "oauth-2025-04-20,interleaved-thinking-2025-05-14,claude-code-20250219"))
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2023-06-01"))
|
||||
})
|
||||
|
||||
t.Run("claude code mode adds beta query param", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
path, found := test.GetHeaderValue(requestHeaders, ":path")
|
||||
require.True(t, found)
|
||||
require.Contains(t, path, "beta=true", "claude code mode should add beta=true query param")
|
||||
})
|
||||
|
||||
t.Run("claude code mode with custom version", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeWithVersionConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
action := host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
require.Equal(t, types.HeaderStopIteration, action)
|
||||
|
||||
requestHeaders := host.GetRequestHeaders()
|
||||
require.True(t, test.HasHeaderWithValue(requestHeaders, "anthropic-version", "2024-01-01"))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func RunClaudeOnHttpRequestBodyTests(t *testing.T) {
|
||||
test.RunTest(t, func(t *testing.T) {
|
||||
t.Run("claude standard mode does not inject defaults", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeStandardConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Standard mode should NOT inject system prompt or tools
|
||||
_, hasSystem := request["system"]
|
||||
require.False(t, hasSystem, "standard mode should not inject system prompt")
|
||||
|
||||
tools, hasTools := request["tools"]
|
||||
if hasTools {
|
||||
toolsArr, ok := tools.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Empty(t, toolsArr, "standard mode should not inject tools")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("claude code mode injects default system prompt", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"stream": true,
|
||||
"messages": [
|
||||
{"role": "user", "content": "List files"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Claude Code mode should inject system prompt
|
||||
system, hasSystem := request["system"]
|
||||
require.True(t, hasSystem, "claude code mode should inject system prompt")
|
||||
|
||||
systemArr, ok := system.([]interface{})
|
||||
require.True(t, ok, "system should be an array in claude code mode")
|
||||
require.Len(t, systemArr, 1)
|
||||
|
||||
systemBlock, ok := systemArr[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "text", systemBlock["type"])
|
||||
require.Equal(t, "You are Claude Code, Anthropic's official CLI for Claude.", systemBlock["text"])
|
||||
|
||||
// Should have cache_control
|
||||
cacheControl, hasCacheControl := systemBlock["cache_control"]
|
||||
require.True(t, hasCacheControl, "system prompt should have cache_control")
|
||||
cacheControlMap, ok := cacheControl.(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "ephemeral", cacheControlMap["type"])
|
||||
})
|
||||
|
||||
t.Run("claude code mode injects bash tool", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{"role": "user", "content": "List files"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Claude Code mode should inject Bash tool
|
||||
tools, hasTools := request["tools"]
|
||||
require.True(t, hasTools, "claude code mode should inject tools")
|
||||
|
||||
toolsArr, ok := tools.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, toolsArr, 1)
|
||||
|
||||
bashTool, ok := toolsArr[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "Bash", bashTool["name"])
|
||||
require.Equal(t, "Run bash commands", bashTool["description"])
|
||||
})
|
||||
|
||||
t.Run("claude code mode preserves existing system prompt", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{"role": "system", "content": "You are a custom assistant."},
|
||||
{"role": "user", "content": "Hello"}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should preserve custom system prompt (not default)
|
||||
system, hasSystem := request["system"]
|
||||
require.True(t, hasSystem)
|
||||
|
||||
systemArr, ok := system.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, systemArr, 1)
|
||||
|
||||
systemBlock, ok := systemArr[0].(map[string]interface{})
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "You are a custom assistant.", systemBlock["text"])
|
||||
})
|
||||
|
||||
t.Run("claude code mode does not duplicate bash tool", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "Bash",
|
||||
"description": "Custom bash tool",
|
||||
"parameters": {"type": "object"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should not duplicate Bash tool
|
||||
tools, hasTools := request["tools"]
|
||||
require.True(t, hasTools)
|
||||
|
||||
toolsArr, ok := tools.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, toolsArr, 1, "should not duplicate Bash tool")
|
||||
})
|
||||
|
||||
t.Run("claude code mode adds bash tool alongside existing tools", func(t *testing.T) {
|
||||
host, status := test.NewTestHost(claudeCodeModeConfig)
|
||||
defer host.Reset()
|
||||
require.Equal(t, types.OnPluginStartStatusOK, status)
|
||||
|
||||
host.CallOnHttpRequestHeaders([][2]string{
|
||||
{":authority", "api.anthropic.com"},
|
||||
{":path", "/v1/chat/completions"},
|
||||
{":method", "POST"},
|
||||
{"Content-Type", "application/json"},
|
||||
})
|
||||
|
||||
body := `{
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"max_tokens": 8192,
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello"}
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "Read",
|
||||
"description": "Read files",
|
||||
"parameters": {"type": "object"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}`
|
||||
action := host.CallOnHttpRequestBody([]byte(body))
|
||||
require.Equal(t, types.ActionContinue, action)
|
||||
|
||||
processedBody := host.GetRequestBody()
|
||||
var request map[string]interface{}
|
||||
err := json.Unmarshal(processedBody, &request)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should have both Read and Bash tools
|
||||
tools, hasTools := request["tools"]
|
||||
require.True(t, hasTools)
|
||||
|
||||
toolsArr, ok := tools.([]interface{})
|
||||
require.True(t, ok)
|
||||
require.Len(t, toolsArr, 2, "should have Read tool plus injected Bash tool")
|
||||
|
||||
// Verify both tools exist
|
||||
toolNames := make([]string, 0)
|
||||
for _, tool := range toolsArr {
|
||||
toolMap, ok := tool.(map[string]interface{})
|
||||
if ok {
|
||||
if name, hasName := toolMap["name"]; hasName {
|
||||
toolNames = append(toolNames, name.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
require.Contains(t, toolNames, "Read")
|
||||
require.Contains(t, toolNames, "Bash")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Note: Response headers tests are skipped as they require complex mocking
|
||||
// The response header transformation is covered by integration tests
|
||||
Reference in New Issue
Block a user