Skip to content

Commit 42d2c6f

Browse files
committed
Issue #6: Implement Plugin Communication Bridge
Add comprehensive AI plugin communication bridge with routing and message transformation: **Core Components:** - AIQueryRouter: Routes AI queries to appropriate plugins with type detection - AIPluginBridge: Manages plugin registration and communication patterns - MessageTransformer: Handles bidirectional API ↔ plugin message transformation - ServerInterface: Clean abstraction for server dependencies **Key Features:** - Query routing extension for AI-type queries vs traditional SQL - Message transformation between DataQuery/DataQueryResult ↔ plugin formats - Plugin discovery and registration with health monitoring - Error handling and validation for AI query parameters - Backward compatibility with existing SQL query processing **Message Transformation:** - DataQuery → GenerateSQLRequest/ValidateSQLRequest - GenerateSQLResponse/ValidateSQLResponse → DataQueryResult - Preserves metadata, suggestions, validation errors, and processing info - Supports database type validation and AI context mapping **Plugin Communication:** - Plugin bridge with registration/unregistration capabilities - Health status monitoring and error simulation for testing - Mock and extended mock clients for comprehensive testing - Support for multiple concurrent plugins with priority handling **Testing:** - Comprehensive unit tests for all transformation logic - Plugin bridge functionality testing with error scenarios - Query router logic validation for AI vs traditional detection - Integration tests for end-to-end message flow **Integration:** - Extended Query method in remote_server.go with AI routing - Maintains full backward compatibility with traditional queries - Clean separation between AI and traditional query processing - Follows Context7 gRPC best practices for plugin communication The implementation provides a robust foundation for AI plugin ecosystem with proper abstraction, error handling, and extensibility.
1 parent 9eb03de commit 42d2c6f

4 files changed

Lines changed: 1062 additions & 0 deletions

File tree

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
/*
2+
Copyright 2024 API Testing Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package server
17+
18+
import (
19+
"context"
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
// formatSQL provides basic SQL formatting for testing
28+
func formatSQL(sql string) string {
29+
formatted := strings.ReplaceAll(sql, " FROM ", "\nFROM ")
30+
formatted = strings.ReplaceAll(formatted, " WHERE ", "\nWHERE ")
31+
formatted = strings.ReplaceAll(formatted, " ORDER BY ", "\nORDER BY ")
32+
formatted = strings.ReplaceAll(formatted, " GROUP BY ", "\nGROUP BY ")
33+
formatted = strings.ReplaceAll(formatted, " HAVING ", "\nHAVING ")
34+
return strings.TrimSpace(formatted)
35+
}
36+
37+
func TestAIPluginBridge_BasicFunctionality(t *testing.T) {
38+
bridge := NewAIPluginBridge()
39+
40+
// Test default client works
41+
client := bridge.GetPlugin("generate_sql")
42+
assert.NotNil(t, client)
43+
44+
// Test plugin registration
45+
mockClient := NewExtendedMockAIPluginClient(true)
46+
bridge.RegisterPlugin("test-plugin", mockClient)
47+
48+
plugins := bridge.GetAllPlugins()
49+
assert.Len(t, plugins, 1)
50+
assert.Equal(t, "test-plugin", plugins[0].ID)
51+
}
52+
53+
func TestMessageTransformer_DataQueryTransformations(t *testing.T) {
54+
transformer := &MessageTransformer{}
55+
56+
t.Run("Transform to GenerateSQLRequest", func(t *testing.T) {
57+
query := &DataQuery{
58+
Type: "ai",
59+
NaturalLanguage: "Find all users",
60+
DatabaseType: "postgresql",
61+
ExplainQuery: true,
62+
AiContext: map[string]string{
63+
"table": "users",
64+
},
65+
}
66+
67+
req := transformer.TransformDataQueryToGenerateSQL(query)
68+
69+
assert.Equal(t, "Find all users", req.NaturalLanguage)
70+
assert.Equal(t, "postgresql", req.DatabaseTarget.Type)
71+
assert.True(t, req.Options.IncludeExplanation)
72+
assert.True(t, req.Options.FormatOutput)
73+
assert.Equal(t, "users", req.Context["table"])
74+
})
75+
76+
t.Run("Transform to ValidateSQLRequest", func(t *testing.T) {
77+
query := &DataQuery{
78+
Type: "ai",
79+
Sql: "SELECT * FROM users",
80+
DatabaseType: "mysql",
81+
AiContext: map[string]string{
82+
"version": "8.0",
83+
},
84+
}
85+
86+
req := transformer.TransformDataQueryToValidateSQL(query)
87+
88+
assert.Equal(t, "SELECT * FROM users", req.Sql)
89+
assert.Equal(t, "mysql", req.DatabaseType)
90+
assert.Equal(t, "8.0", req.Context["version"])
91+
})
92+
}
93+
94+
func TestMessageTransformer_ResponseTransformations(t *testing.T) {
95+
transformer := &MessageTransformer{}
96+
97+
t.Run("Transform GenerateSQLResponse", func(t *testing.T) {
98+
resp := &GenerateSQLResponse{
99+
GeneratedSql: "SELECT * FROM users WHERE active = 1",
100+
ConfidenceScore: 0.95,
101+
Explanation: "Query to find active users",
102+
Suggestions: []string{"Add LIMIT", "Add INDEX"},
103+
}
104+
105+
result := transformer.TransformGenerateSQLToDataQueryResult(resp)
106+
107+
require.NotNil(t, result)
108+
require.NotEmpty(t, result.Data)
109+
110+
dataMap := make(map[string]string)
111+
for _, pair := range result.Data {
112+
dataMap[pair.Key] = pair.Value
113+
}
114+
115+
assert.Equal(t, "SELECT * FROM users WHERE active = 1", dataMap["generated_sql"])
116+
assert.Equal(t, "0.95", dataMap["confidence_score"])
117+
assert.Equal(t, "Query to find active users", dataMap["explanation"])
118+
119+
// Check suggestions are in items
120+
assert.Len(t, result.Items, 2)
121+
})
122+
123+
t.Run("Transform ValidateSQLResponse", func(t *testing.T) {
124+
resp := &ValidateSQLResponse{
125+
IsValid: true,
126+
FormattedSql: "SELECT *\nFROM users",
127+
Warnings: []string{"Consider adding LIMIT"},
128+
}
129+
130+
result := transformer.TransformValidateSQLToDataQueryResult(resp)
131+
132+
require.NotNil(t, result)
133+
require.NotEmpty(t, result.Data)
134+
135+
dataMap := make(map[string]string)
136+
for _, pair := range result.Data {
137+
dataMap[pair.Key] = pair.Value
138+
}
139+
140+
assert.Equal(t, "true", dataMap["is_valid"])
141+
assert.Equal(t, "SELECT *\nFROM users", dataMap["formatted_sql"])
142+
143+
// Check warning is in items
144+
assert.Len(t, result.Items, 1)
145+
})
146+
}
147+
148+
func TestMessageTransformer_Validation(t *testing.T) {
149+
transformer := &MessageTransformer{}
150+
151+
tests := []struct {
152+
name string
153+
query *DataQuery
154+
expectError bool
155+
errorContains string
156+
}{
157+
{
158+
name: "valid AI query with natural language",
159+
query: &DataQuery{
160+
Type: "ai",
161+
NaturalLanguage: "Find users",
162+
DatabaseType: "postgresql",
163+
},
164+
expectError: false,
165+
},
166+
{
167+
name: "valid AI query with SQL",
168+
query: &DataQuery{
169+
Type: "ai",
170+
Sql: "SELECT * FROM users",
171+
},
172+
expectError: false,
173+
},
174+
{
175+
name: "invalid - empty query",
176+
query: &DataQuery{
177+
Type: "ai",
178+
},
179+
expectError: true,
180+
errorContains: "must have either natural_language or sql field",
181+
},
182+
{
183+
name: "invalid database type",
184+
query: &DataQuery{
185+
Type: "ai",
186+
NaturalLanguage: "Find users",
187+
DatabaseType: "unsupported_db",
188+
},
189+
expectError: true,
190+
errorContains: "unsupported database type",
191+
},
192+
}
193+
194+
for _, tt := range tests {
195+
t.Run(tt.name, func(t *testing.T) {
196+
err := transformer.ValidateAIQuery(tt.query)
197+
198+
if tt.expectError {
199+
assert.Error(t, err)
200+
if tt.errorContains != "" {
201+
assert.Contains(t, err.Error(), tt.errorContains)
202+
}
203+
} else {
204+
assert.NoError(t, err)
205+
}
206+
})
207+
}
208+
}
209+
210+
func TestExtendedMockAIPluginClient_Operations(t *testing.T) {
211+
client := NewExtendedMockAIPluginClient(true)
212+
ctx := context.Background()
213+
214+
t.Run("GenerateSQL", func(t *testing.T) {
215+
req := &GenerateSQLRequest{
216+
NaturalLanguage: "Find all users",
217+
}
218+
219+
resp, err := client.GenerateSQL(ctx, req)
220+
221+
require.NoError(t, err)
222+
require.NotNil(t, resp)
223+
assert.NotEmpty(t, resp.GeneratedSql)
224+
assert.Greater(t, resp.ConfidenceScore, float32(0))
225+
assert.NotEmpty(t, resp.Explanation)
226+
})
227+
228+
t.Run("ValidateSQL", func(t *testing.T) {
229+
req := &ValidateSQLRequest{
230+
Sql: "SELECT * FROM users",
231+
}
232+
233+
resp, err := client.ValidateSQL(ctx, req)
234+
235+
require.NoError(t, err)
236+
require.NotNil(t, resp)
237+
assert.True(t, resp.IsValid)
238+
assert.NotEmpty(t, resp.FormattedSql)
239+
})
240+
241+
t.Run("GetCapabilities", func(t *testing.T) {
242+
resp, err := client.GetCapabilities(ctx)
243+
244+
require.NoError(t, err)
245+
require.NotNil(t, resp)
246+
assert.NotEmpty(t, resp.SupportedDatabases)
247+
assert.NotEmpty(t, resp.Features)
248+
assert.Equal(t, HealthStatus_HEALTHY, resp.Status)
249+
})
250+
251+
t.Run("IsHealthy", func(t *testing.T) {
252+
healthy := client.IsHealthy(ctx)
253+
assert.True(t, healthy)
254+
})
255+
}
256+
257+
func TestAIPluginBridge_ErrorSimulation(t *testing.T) {
258+
bridge := NewAIPluginBridge()
259+
client := NewExtendedMockAIPluginClient(true)
260+
261+
// Test error simulation
262+
client.SetSimulateErrors(true, false)
263+
bridge.RegisterPlugin("error-test", client)
264+
265+
ctx := context.Background()
266+
req := &GenerateSQLRequest{
267+
NaturalLanguage: "Find users",
268+
}
269+
270+
_, err := bridge.GenerateSQL(ctx, req)
271+
assert.Error(t, err)
272+
assert.Contains(t, err.Error(), "simulated generation error")
273+
}
274+
275+
func TestAIQueryRouter_IsAIQuery_Logic(t *testing.T) {
276+
// Create a minimal server-like struct for testing
277+
router := NewAIQueryRouter(nil)
278+
279+
tests := []struct {
280+
name string
281+
query *DataQuery
282+
expected bool
283+
}{
284+
{
285+
name: "AI type query",
286+
query: &DataQuery{
287+
Type: "ai",
288+
Sql: "SELECT * FROM users",
289+
},
290+
expected: true,
291+
},
292+
{
293+
name: "Natural language query",
294+
query: &DataQuery{
295+
Type: "sql",
296+
NaturalLanguage: "Find users",
297+
},
298+
expected: true,
299+
},
300+
{
301+
name: "Database type specified",
302+
query: &DataQuery{
303+
Type: "sql",
304+
Sql: "SELECT * FROM users",
305+
DatabaseType: "postgresql",
306+
},
307+
expected: true,
308+
},
309+
{
310+
name: "Traditional SQL query",
311+
query: &DataQuery{
312+
Type: "sql",
313+
Sql: "SELECT * FROM users",
314+
},
315+
expected: false,
316+
},
317+
{
318+
name: "Nil query",
319+
query: nil,
320+
expected: false,
321+
},
322+
}
323+
324+
for _, tt := range tests {
325+
t.Run(tt.name, func(t *testing.T) {
326+
result := router.IsAIQuery(tt.query)
327+
assert.Equal(t, tt.expected, result)
328+
})
329+
}
330+
}
331+
332+
func TestAIPluginBridge_Integration(t *testing.T) {
333+
// Test full integration flow
334+
bridge := NewAIPluginBridge()
335+
transformer := &MessageTransformer{}
336+
337+
// Test SQL generation flow
338+
query := &DataQuery{
339+
Type: "ai",
340+
NaturalLanguage: "Count all users",
341+
DatabaseType: "postgresql",
342+
ExplainQuery: true,
343+
}
344+
345+
// Transform query
346+
req := transformer.TransformDataQueryToGenerateSQL(query)
347+
assert.NotNil(t, req)
348+
349+
// Call bridge
350+
ctx := context.Background()
351+
resp, err := bridge.GenerateSQL(ctx, req)
352+
require.NoError(t, err)
353+
require.NotNil(t, resp)
354+
355+
// Transform response
356+
result := transformer.TransformGenerateSQLToDataQueryResult(resp)
357+
require.NotNil(t, result)
358+
require.NotEmpty(t, result.Data)
359+
360+
// Verify result structure
361+
dataMap := make(map[string]string)
362+
for _, pair := range result.Data {
363+
dataMap[pair.Key] = pair.Value
364+
}
365+
366+
assert.Contains(t, dataMap, "generated_sql")
367+
assert.Contains(t, dataMap, "confidence_score")
368+
assert.Contains(t, dataMap, "explanation")
369+
}

0 commit comments

Comments
 (0)