| package amp |
|
|
| import ( |
| "bytes" |
| "net/http" |
| "net/http/httptest" |
| "strings" |
| "testing" |
|
|
| "github.com/gin-gonic/gin" |
| ) |
|
|
| |
| type mockResponseWriter struct { |
| gin.ResponseWriter |
| body *bytes.Buffer |
| headers http.Header |
| statusCode int |
| flushed bool |
| } |
|
|
| func newMockResponseWriter() *mockResponseWriter { |
| return &mockResponseWriter{ |
| body: &bytes.Buffer{}, |
| headers: make(http.Header), |
| } |
| } |
|
|
| func (m *mockResponseWriter) Write(data []byte) (int, error) { |
| return m.body.Write(data) |
| } |
|
|
| func (m *mockResponseWriter) WriteString(s string) (int, error) { |
| return m.body.WriteString(s) |
| } |
|
|
| func (m *mockResponseWriter) Header() http.Header { |
| return m.headers |
| } |
|
|
| func (m *mockResponseWriter) WriteHeader(code int) { |
| m.statusCode = code |
| } |
|
|
| func (m *mockResponseWriter) Status() int { |
| return m.statusCode |
| } |
|
|
| |
| func (m *mockResponseWriter) Flush() { |
| m.flushed = true |
| } |
|
|
| |
| func TestResponseRewriter_SplitJSONTokensAcrossChunks(t *testing.T) { |
| tests := []struct { |
| name string |
| originalModel string |
| chunks []string |
| expected string |
| }{ |
| { |
| name: "model field split across chunks", |
| originalModel: "original-model", |
| chunks: []string{ |
| `{"mod`, |
| `el": "mapped-model", "content": "test"}`, |
| }, |
| expected: `{"model": "original-model", "content": "test"}`, |
| }, |
| { |
| name: "modelVersion split across chunks", |
| originalModel: "original-model", |
| chunks: []string{ |
| `{"modelVers`, |
| `ion": "mapped-version"}`, |
| }, |
| expected: `{"modelVersion": "original-model"}`, |
| }, |
| { |
| name: "multiple fields with splits", |
| originalModel: "original-model", |
| chunks: []string{ |
| `{"model": "mapped`, |
| `-model", "modelVersion": "mapped`, |
| `-version"}`, |
| }, |
| expected: `{"model": "original-model", "modelVersion": "original-model"}`, |
| }, |
| { |
| name: "empty chunks between data", |
| originalModel: "original-model", |
| chunks: []string{ |
| `{"model": "`, |
| ``, |
| `mapped-model"}`, |
| }, |
| expected: `{"model": "original-model"}`, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| |
| for _, chunk := range tt.chunks { |
| rw.WriteString(chunk) |
| } |
| rw.Flush() |
|
|
| result := mock.body.String() |
| if result != tt.expected { |
| t.Errorf("got %q, want %q", result, tt.expected) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_InvalidMalformedJSON(t *testing.T) { |
| tests := []struct { |
| name string |
| originalModel string |
| input string |
| shouldPass bool |
| }{ |
| { |
| name: "incomplete JSON", |
| originalModel: "original", |
| input: `{"model": "mapped`, |
| shouldPass: true, |
| }, |
| { |
| name: "invalid JSON structure", |
| originalModel: "original", |
| input: `{"model": "mapped", invalid}`, |
| shouldPass: true, |
| }, |
| { |
| name: "unclosed string", |
| originalModel: "original", |
| input: `{"model": "mapped`, |
| shouldPass: true, |
| }, |
| { |
| name: "binary garbage", |
| originalModel: "original", |
| input: string([]byte{0x00, 0x01, 0x02, 0xFF}), |
| shouldPass: true, |
| }, |
| { |
| name: "null bytes in JSON", |
| originalModel: "original", |
| input: `{"model": "mapped\u0000model"}`, |
| shouldPass: true, |
| }, |
| { |
| name: "deeply nested valid JSON", |
| originalModel: "original", |
| input: `{"a": {"b": {"c": {"d": {"model": "mapped"}}}}}`, |
| shouldPass: true, |
| }, |
| { |
| name: "valid JSON without model field", |
| originalModel: "original", |
| input: `{"content": "test", "other": "data"}`, |
| shouldPass: true, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| |
| rw.WriteString(tt.input) |
| rw.Flush() |
|
|
| result := mock.body.String() |
|
|
| |
| if result == "" && tt.shouldPass { |
| t.Error("expected output but got empty string") |
| } |
|
|
| |
| if strings.Contains(tt.input, `"model"`) && strings.Contains(tt.input, "mapped") { |
| if strings.Contains(result, "mapped") && !strings.Contains(result, `"model": "original"`) { |
| t.Logf("Model replacement may not have worked for: %s", tt.name) |
| } |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_MixedContentTypes(t *testing.T) { |
| tests := []struct { |
| name string |
| contentType string |
| originalModel string |
| input string |
| isStreaming bool |
| }{ |
| { |
| name: "SSE stream", |
| contentType: "text/event-stream", |
| originalModel: "original-model", |
| input: `data: {"model": "mapped-model", "content": "hello"}`, |
| isStreaming: true, |
| }, |
| { |
| name: "JSON response", |
| contentType: "application/json", |
| originalModel: "original-model", |
| input: `{"model": "mapped-model", "content": "hello"}`, |
| isStreaming: false, |
| }, |
| { |
| name: "plain text", |
| contentType: "text/plain", |
| originalModel: "original-model", |
| input: `model: mapped-model`, |
| isStreaming: false, |
| }, |
| { |
| name: "octet stream", |
| contentType: "application/octet-stream", |
| originalModel: "original-model", |
| input: `binary data`, |
| isStreaming: false, |
| }, |
| { |
| name: "multipart form", |
| contentType: "multipart/form-data", |
| originalModel: "original-model", |
| input: `--boundary\nContent-Type: application/json\n\n{"model": "mapped-model"}`, |
| isStreaming: false, |
| }, |
| { |
| name: "JSON with stream in body", |
| contentType: "application/json", |
| originalModel: "original-model", |
| input: `{"stream": true, "model": "mapped-model"}`, |
| isStreaming: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| mock.headers.Set("Content-Type", tt.contentType) |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| |
| rw.WriteString(tt.input) |
|
|
| if rw.isStreaming != tt.isStreaming { |
| t.Errorf("isStreaming = %v, want %v", rw.isStreaming, tt.isStreaming) |
| } |
|
|
| rw.Flush() |
|
|
| |
| result := mock.body.String() |
| if result == "" { |
| t.Error("expected output but got empty string") |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_FallbackStrategy(t *testing.T) { |
| tests := []struct { |
| name string |
| originalModel string |
| input []string |
| expectError bool |
| }{ |
| { |
| name: "empty original model - pass through", |
| originalModel: "", |
| input: []string{`{"model": "mapped-model"}`}, |
| expectError: false, |
| }, |
| { |
| name: "single chunk rewrite success", |
| originalModel: "original-model", |
| input: []string{`{"model": "mapped-model"}`}, |
| expectError: false, |
| }, |
| { |
| name: "multiple chunks with partial data", |
| originalModel: "original-model", |
| input: []string{ |
| `{"model": "mapped`, |
| `-model", "content": "test"}`, |
| }, |
| expectError: false, |
| }, |
| { |
| name: "chunk with only whitespace", |
| originalModel: "original-model", |
| input: []string{ |
| ` `, |
| `{"model": "mapped-model"}`, |
| }, |
| expectError: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| |
| for _, chunk := range tt.input { |
| _, err := rw.WriteString(chunk) |
| if err != nil && !tt.expectError { |
| t.Errorf("unexpected error: %v", err) |
| } |
| } |
|
|
| rw.Flush() |
|
|
| |
| result := mock.body.String() |
| if result == "" && len(tt.input) > 0 { |
| t.Error("expected output but got empty string") |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_SSEEdgeCases(t *testing.T) { |
| tests := []struct { |
| name string |
| originalModel string |
| chunks []string |
| expected string |
| }{ |
| { |
| name: "SSE with data prefix", |
| originalModel: "original-model", |
| chunks: []string{ |
| `data: {"model": "mapped-model"}`, |
| }, |
| expected: `data: {"model": "original-model"}`, |
| }, |
| { |
| name: "SSE multiple data lines", |
| originalModel: "original-model", |
| chunks: []string{ |
| "data: {\"model\": \"mapped-model\"}\n\ndata: {\"model\": \"mapped-model2\"}", |
| }, |
| expected: "data: {\"model\": \"original-model\"}\n\ndata: {\"model\": \"original-model\"}", |
| }, |
| { |
| name: "SSE with event and id", |
| originalModel: "original-model", |
| chunks: []string{ |
| "id: 1\nevent: message\ndata: {\"model\": \"mapped-model\"}", |
| }, |
| expected: "id: 1\nevent: message\ndata: {\"model\": \"original-model\"}", |
| }, |
| { |
| name: "SSE data split across chunks", |
| originalModel: "original-model", |
| chunks: []string{ |
| `data: {"mod`, |
| `el": "mapped-model"}`, |
| }, |
| expected: `data: {"model": "original-model"}`, |
| }, |
| { |
| name: "SSE non-JSON data", |
| originalModel: "original-model", |
| chunks: []string{ |
| `data: plain text message`, |
| }, |
| expected: `data: plain text message`, |
| }, |
| { |
| name: "SSE empty data line", |
| originalModel: "original-model", |
| chunks: []string{ |
| `data:`, |
| }, |
| expected: `data:`, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| mock.headers.Set("Content-Type", "text/event-stream") |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| for _, chunk := range tt.chunks { |
| rw.WriteString(chunk) |
| } |
|
|
| result := mock.body.String() |
| if result != tt.expected { |
| t.Errorf("got %q, want %q", result, tt.expected) |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_ThinkingBlockSuppression(t *testing.T) { |
| tests := []struct { |
| name string |
| originalModel string |
| input string |
| shouldFilter bool |
| }{ |
| { |
| name: "tool_use with thinking - should filter", |
| originalModel: "original-model", |
| input: `{ |
| "content": [ |
| {"type": "thinking", "thinking": "secret"}, |
| {"type": "tool_use", "name": "calculator"} |
| ] |
| }`, |
| shouldFilter: true, |
| }, |
| { |
| name: "thinking without tool_use - should not filter", |
| originalModel: "original-model", |
| input: `{ |
| "content": [ |
| {"type": "thinking", "thinking": "not secret"} |
| ] |
| }`, |
| shouldFilter: false, |
| }, |
| { |
| name: "no content field", |
| originalModel: "original-model", |
| input: `{"model": "mapped-model"}`, |
| shouldFilter: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, tt.originalModel) |
|
|
| rw.WriteString(tt.input) |
| rw.Flush() |
|
|
| result := mock.body.String() |
|
|
| |
| hasThinking := strings.Contains(result, `"type": "thinking"`) |
| hasToolUse := strings.Contains(tt.input, `"type": "tool_use"`) |
|
|
| if tt.shouldFilter && hasThinking { |
| t.Error("thinking blocks should have been filtered but are present") |
| } |
|
|
| if !tt.shouldFilter && hasToolUse && !hasThinking { |
| |
| t.Logf("Note: thinking blocks were removed even though no tool_use detected") |
| } |
| }) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_ConcurrentWrites(t *testing.T) { |
| gin.SetMode(gin.TestMode) |
|
|
| mock := newMockResponseWriter() |
| mock.headers.Set("Content-Type", "application/json") |
| rw := NewResponseRewriter(mock, "original-model") |
|
|
| |
| chunks := []string{ |
| `{"model": "`, |
| `mapped`, |
| `-model", "`, |
| `content": "`, |
| `test"}`, |
| } |
|
|
| for _, chunk := range chunks { |
| rw.WriteString(chunk) |
| } |
| rw.Flush() |
|
|
| result := mock.body.String() |
| expected := `{"model": "original-model", "content": "test"}` |
| if result != expected { |
| t.Errorf("got %q, want %q", result, expected) |
| } |
| } |
|
|
| |
| func TestResponseRewriter_LargeResponse(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, "original-model") |
|
|
| |
| largeContent := strings.Repeat("a", 100000) |
| input := `{"model": "mapped-model", "content": "` + largeContent + `"}` |
|
|
| rw.WriteString(input) |
| rw.Flush() |
|
|
| result := mock.body.String() |
|
|
| |
| if !strings.Contains(result, `"model": "original-model"`) { |
| t.Error("model replacement failed for large response") |
| } |
|
|
| |
| if !strings.Contains(result, largeContent) { |
| t.Error("large content was lost") |
| } |
| } |
|
|
| |
| func TestResponseRewriter_EmptyAndNilInputs(t *testing.T) { |
| tests := []struct { |
| name string |
| input []byte |
| }{ |
| { |
| name: "nil input", |
| input: nil, |
| }, |
| { |
| name: "empty input", |
| input: []byte{}, |
| }, |
| { |
| name: "whitespace only", |
| input: []byte(" \n\t "), |
| }, |
| { |
| name: "single brace", |
| input: []byte("{"), |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| rw := NewResponseRewriter(mock, "original-model") |
|
|
| |
| rw.Write(tt.input) |
| rw.Flush() |
|
|
| |
| _ = mock.body.String() |
| }) |
| } |
| } |
|
|
| |
| func TestNewResponseRewriter(t *testing.T) { |
| gin.SetMode(gin.TestMode) |
| w := httptest.NewRecorder() |
| c, _ := gin.CreateTestContext(w) |
|
|
| rw := NewResponseRewriter(c.Writer, "test-model") |
|
|
| if rw == nil { |
| t.Fatal("NewResponseRewriter returned nil") |
| } |
|
|
| if rw.originalModel != "test-model" { |
| t.Errorf("originalModel = %q, want %q", rw.originalModel, "test-model") |
| } |
|
|
| if rw.body == nil { |
| t.Error("body buffer is nil") |
| } |
| } |
|
|
| |
| func TestResponseRewriter_FlushBehavior(t *testing.T) { |
| tests := []struct { |
| name string |
| isStreaming bool |
| writeData string |
| expectFlush bool |
| }{ |
| { |
| name: "flush non-streaming", |
| isStreaming: false, |
| writeData: `{"model": "mapped"}`, |
| expectFlush: true, |
| }, |
| { |
| name: "flush streaming", |
| isStreaming: true, |
| writeData: `data: {"model": "mapped"}`, |
| expectFlush: true, |
| }, |
| { |
| name: "flush empty body", |
| isStreaming: false, |
| writeData: "", |
| expectFlush: false, |
| }, |
| } |
|
|
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| mock := newMockResponseWriter() |
| if tt.isStreaming { |
| mock.headers.Set("Content-Type", "text/event-stream") |
| } |
| rw := NewResponseRewriter(mock, "original") |
|
|
| if tt.writeData != "" { |
| rw.WriteString(tt.writeData) |
| } |
|
|
| |
| mock.flushed = false |
|
|
| rw.Flush() |
|
|
| if tt.expectFlush && !mock.flushed { |
| t.Error("expected Flush to be called on underlying writer") |
| } |
| }) |
| } |
| } |
|
|