Skip to content

Commit 91af4eb

Browse files
Copilotlpcox
andcommitted
Add integration test for tools.json logging
Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 3859e33 commit 91af4eb

2 files changed

Lines changed: 257 additions & 1 deletion

File tree

internal/logger/tools_logger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func InitToolsLogger(logDir, fileName string) error {
5050
if file != nil {
5151
file.Close()
5252
}
53-
53+
5454
tl := &ToolsLogger{
5555
logDir: logDir,
5656
fileName: fileName,
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package integration
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/http/httptest"
11+
"os"
12+
"os/exec"
13+
"path/filepath"
14+
"testing"
15+
"time"
16+
17+
"github.com/stretchr/testify/assert"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
// startGatewayWithJSONConfigAndLogDir starts the gateway with JSON config and custom log directory
22+
func startGatewayWithJSONConfigAndLogDir(ctx context.Context, t *testing.T, jsonConfig string, logDir string) *exec.Cmd {
23+
t.Helper()
24+
25+
// Find the binary
26+
binaryPath := findBinary(t)
27+
t.Logf("Using binary: %s", binaryPath)
28+
29+
// Extract port from config if possible, otherwise use default
30+
port := "13120" // Default port
31+
var configMap map[string]interface{}
32+
if err := json.Unmarshal([]byte(jsonConfig), &configMap); err == nil {
33+
if gateway, ok := configMap["gateway"].(map[string]interface{}); ok {
34+
if portNum, ok := gateway["port"].(float64); ok {
35+
port = fmt.Sprintf("%d", int(portNum))
36+
}
37+
}
38+
}
39+
40+
cmd := exec.CommandContext(ctx, binaryPath,
41+
"--config-stdin",
42+
"--listen", "127.0.0.1:"+port,
43+
"--log-dir", logDir,
44+
"--routed",
45+
)
46+
47+
// Set stdin to the JSON config
48+
cmd.Stdin = bytes.NewBufferString(jsonConfig)
49+
50+
// Capture output for debugging
51+
var stdout, stderr bytes.Buffer
52+
cmd.Stdout = &stdout
53+
cmd.Stderr = &stderr
54+
55+
if err := cmd.Start(); err != nil {
56+
t.Fatalf("Failed to start gateway: %v\nSTDOUT: %s\nSTDERR: %s", err, stdout.String(), stderr.String())
57+
}
58+
59+
// Start a goroutine to log output if test fails
60+
go func() {
61+
<-ctx.Done()
62+
if t.Failed() {
63+
t.Logf("Gateway STDOUT: %s", stdout.String())
64+
t.Logf("Gateway STDERR: %s", stderr.String())
65+
}
66+
}()
67+
68+
return cmd
69+
}
70+
71+
// TestToolsJSONLogging tests that tools.json is created and populated correctly
72+
func TestToolsJSONLogging(t *testing.T) {
73+
if testing.Short() {
74+
t.Skip("Skipping tools.json integration test in short mode")
75+
}
76+
77+
assert := assert.New(t)
78+
require := require.New(t)
79+
80+
// Create a mock MCP backend that returns tools
81+
mockBackend := createMockToolsServer(t)
82+
defer mockBackend.Close()
83+
84+
t.Logf("✓ Mock MCP backend started at %s", mockBackend.URL)
85+
86+
// Create a temporary directory for logs
87+
tmpDir := t.TempDir()
88+
89+
// Create JSON config for the gateway
90+
configContent := fmt.Sprintf(`{
91+
"mcpServers": {
92+
"test-server": {
93+
"type": "http",
94+
"url": "%s"
95+
},
96+
"another-server": {
97+
"type": "http",
98+
"url": "%s"
99+
}
100+
},
101+
"gateway": {
102+
"port": 13120,
103+
"domain": "localhost",
104+
"apiKey": "test-tools-key"
105+
}
106+
}`, mockBackend.URL, mockBackend.URL)
107+
108+
t.Logf("✓ Created config with log directory: %s", tmpDir)
109+
110+
// Start the gateway with the config via stdin
111+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
112+
defer cancel()
113+
114+
// Start gateway with JSON config via stdin and custom log directory
115+
gatewayCmd := startGatewayWithJSONConfigAndLogDir(ctx, t, configContent, tmpDir)
116+
defer gatewayCmd.Process.Kill()
117+
118+
// Wait for gateway to start
119+
gatewayURL := "http://127.0.0.1:13120"
120+
if !waitForServer(t, gatewayURL+"/health", 15*time.Second) {
121+
t.Fatal("Gateway did not start in time")
122+
}
123+
t.Logf("✓ Gateway started at %s", gatewayURL)
124+
125+
// Give the gateway time to initialize and register tools
126+
time.Sleep(2 * time.Second)
127+
128+
// Check that tools.json was created
129+
toolsPath := filepath.Join(tmpDir, "tools.json")
130+
toolsData, err := os.ReadFile(toolsPath)
131+
require.NoError(err, "tools.json should exist")
132+
t.Logf("✓ tools.json found at %s", toolsPath)
133+
134+
// Parse the tools.json file
135+
var tools struct {
136+
Servers map[string][]struct {
137+
Name string `json:"name"`
138+
Description string `json:"description"`
139+
} `json:"servers"`
140+
}
141+
err = json.Unmarshal(toolsData, &tools)
142+
require.NoError(err, "tools.json should be valid JSON")
143+
144+
// Verify both servers are present
145+
assert.Contains(tools.Servers, "test-server", "test-server should be in tools.json")
146+
assert.Contains(tools.Servers, "another-server", "another-server should be in tools.json")
147+
148+
// Verify test-server has the expected tools
149+
testServerTools := tools.Servers["test-server"]
150+
require.Len(testServerTools, 3, "test-server should have 3 tools")
151+
152+
toolNames := make(map[string]string)
153+
for _, tool := range testServerTools {
154+
toolNames[tool.Name] = tool.Description
155+
}
156+
157+
assert.Contains(toolNames, "tool1", "tool1 should be present")
158+
assert.Contains(toolNames, "tool2", "tool2 should be present")
159+
assert.Contains(toolNames, "tool3", "tool3 should be present")
160+
assert.Equal("First test tool", toolNames["tool1"])
161+
assert.Equal("Second test tool", toolNames["tool2"])
162+
assert.Equal("Third test tool", toolNames["tool3"])
163+
164+
t.Logf("✓ tools.json contains correct data")
165+
}
166+
167+
// createMockToolsServer creates a mock HTTP MCP server that returns tools
168+
func createMockToolsServer(t *testing.T) *httptest.Server {
169+
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
170+
// Read the request body
171+
body, err := io.ReadAll(r.Body)
172+
if err != nil {
173+
t.Logf("Failed to read request body: %v", err)
174+
w.WriteHeader(http.StatusInternalServerError)
175+
return
176+
}
177+
178+
// Parse the JSON-RPC request
179+
var req struct {
180+
Method string `json:"method"`
181+
ID json.RawMessage `json:"id"`
182+
}
183+
if err := json.Unmarshal(body, &req); err != nil {
184+
t.Logf("Failed to parse request: %v", err)
185+
w.WriteHeader(http.StatusBadRequest)
186+
return
187+
}
188+
189+
// Handle different methods
190+
switch req.Method {
191+
case "initialize":
192+
response := map[string]interface{}{
193+
"jsonrpc": "2.0",
194+
"id": req.ID,
195+
"result": map[string]interface{}{
196+
"protocolVersion": "2024-11-05",
197+
"serverInfo": map[string]interface{}{
198+
"name": "mock-server",
199+
"version": "1.0.0",
200+
},
201+
"capabilities": map[string]interface{}{
202+
"tools": map[string]interface{}{},
203+
},
204+
},
205+
}
206+
w.Header().Set("Content-Type", "application/json")
207+
json.NewEncoder(w).Encode(response)
208+
209+
case "tools/list":
210+
response := map[string]interface{}{
211+
"jsonrpc": "2.0",
212+
"id": req.ID,
213+
"result": map[string]interface{}{
214+
"tools": []map[string]interface{}{
215+
{
216+
"name": "tool1",
217+
"description": "First test tool",
218+
"inputSchema": map[string]interface{}{
219+
"type": "object",
220+
"properties": map[string]interface{}{},
221+
},
222+
},
223+
{
224+
"name": "tool2",
225+
"description": "Second test tool",
226+
"inputSchema": map[string]interface{}{
227+
"type": "object",
228+
"properties": map[string]interface{}{},
229+
},
230+
},
231+
{
232+
"name": "tool3",
233+
"description": "Third test tool",
234+
"inputSchema": map[string]interface{}{
235+
"type": "object",
236+
"properties": map[string]interface{}{},
237+
},
238+
},
239+
},
240+
},
241+
}
242+
w.Header().Set("Content-Type", "application/json")
243+
json.NewEncoder(w).Encode(response)
244+
245+
default:
246+
// Return success for any other method
247+
response := map[string]interface{}{
248+
"jsonrpc": "2.0",
249+
"id": req.ID,
250+
"result": map[string]interface{}{},
251+
}
252+
w.Header().Set("Content-Type", "application/json")
253+
json.NewEncoder(w).Encode(response)
254+
}
255+
}))
256+
}

0 commit comments

Comments
 (0)