Skip to content

Commit 53498e7

Browse files
Copilotdata-douser
andauthored
Replace JavaScript client with Go binary and integration test runner
- Remove client/src/ directory with all JS files (ql-mcp-client.js and 14 library modules) - Remove client/package.json and client/eslint.config.mjs - Add Go module (go.mod, go.sum) with cobra and mcp-go dependencies - Add CLI entry point (main.go) and root Cobra command (cmd/root.go) - Add CLI helpers (cmd/helpers.go) and integration test command (cmd/integration_tests.go) - Add MCP client library (internal/mcp/client.go) with stdio and HTTP transport - Add integration test runner (internal/testing/runner.go, params.go) - Add comprehensive unit tests for all packages (16 tests, all passing) - Update Makefile to remove go.mod guards (Go source now available) - Update run-integration-tests.sh to build and use Go binary - Update test-config.json logDir paths for custom_log_directory tests Agent-Logs-Url: https://github.com/advanced-security/codeql-development-mcp-server/sessions/8c006672-cf7e-4045-9488-f6d97fafe2f2 Co-authored-by: data-douser <70299490+data-douser@users.noreply.github.com>
1 parent dbc2bdb commit 53498e7

38 files changed

+1723
-5733
lines changed

client/Makefile

Lines changed: 18 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
BINARY_NAME := gh-ql-mcp-client
22
MODULE := github.com/advanced-security/codeql-development-mcp-server/client
3+
VERSION := $(shell grep 'Version = ' cmd/root.go | head -1 | sed 's/.*"\(.*\)"/\1/')
34

45
# Disable CGO to avoid Xcode/C compiler dependency
56
export CGO_ENABLED = 0
@@ -13,59 +14,35 @@ all: lint test build
1314

1415
## build: Build the binary for the current platform
1516
build:
16-
@if [ -f go.mod ]; then \
17-
go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME) .; \
18-
else \
19-
echo "Go source not yet available — skipping build"; \
20-
fi
17+
go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME) .
2118

2219
## test: Run unit tests and integration tests
2320
test: test-unit test-integration
2421

2522
## test-unit: Run Go unit tests only
2623
test-unit:
27-
@if [ -f go.mod ]; then \
28-
go test ./...; \
29-
else \
30-
echo "Go source not yet available — skipping unit tests"; \
31-
fi
24+
go test ./...
3225

3326
## test-integration: Build binary and run integration tests via run-integration-tests.sh
3427
test-integration: build
35-
@if [ -f go.mod ]; then \
36-
ENABLE_ANNOTATION_TOOLS=true scripts/run-integration-tests.sh --no-install-packs; \
37-
else \
38-
echo "Go source not yet available — skipping integration tests"; \
39-
fi
28+
ENABLE_ANNOTATION_TOOLS=true scripts/run-integration-tests.sh --no-install-packs
4029

4130
## test-verbose: Run all unit tests with verbose output
4231
test-verbose:
43-
@if [ -f go.mod ]; then \
44-
go test -v ./...; \
45-
else \
46-
echo "Go source not yet available — skipping verbose tests"; \
47-
fi
32+
go test -v ./...
4833

4934
## test-coverage: Run tests with coverage
5035
test-coverage:
51-
@if [ -f go.mod ]; then \
52-
go test -coverprofile=coverage.out ./...; \
53-
go tool cover -html=coverage.out -o coverage.html; \
54-
else \
55-
echo "Go source not yet available — skipping coverage"; \
56-
fi
36+
go test -coverprofile=coverage.out ./...
37+
go tool cover -html=coverage.out -o coverage.html
5738

5839
## lint: Run linters
5940
lint:
60-
@if [ -f go.mod ]; then \
61-
if command -v golangci-lint > /dev/null 2>&1; then \
62-
golangci-lint run ./...; \
63-
else \
64-
echo "golangci-lint not found, running go vet only"; \
65-
go vet ./...; \
66-
fi; \
41+
@if command -v golangci-lint > /dev/null 2>&1; then \
42+
golangci-lint run ./...; \
6743
else \
68-
echo "Go source not yet available — skipping lint"; \
44+
echo "golangci-lint not found, running go vet only"; \
45+
go vet ./...; \
6946
fi
7047

7148
## clean: Remove build artifacts
@@ -74,31 +51,19 @@ clean:
7451

7552
## install: Install the binary to GOPATH/bin
7653
install:
77-
@if [ -f go.mod ]; then \
78-
go install -ldflags "$(LDFLAGS)" .; \
79-
else \
80-
echo "Go source not yet available — skipping install"; \
81-
fi
54+
go install -ldflags "$(LDFLAGS)" .
8255

8356
## build-all: Cross-compile for all supported platforms
8457
build-all:
85-
@if [ -f go.mod ]; then \
86-
GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-darwin-amd64 .; \
87-
GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-darwin-arm64 .; \
88-
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-linux-amd64 .; \
89-
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-linux-arm64 .; \
90-
GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-windows-amd64.exe .; \
91-
else \
92-
echo "Go source not yet available — skipping cross-compile"; \
93-
fi
58+
GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-darwin-amd64 .
59+
GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-darwin-arm64 .
60+
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-linux-amd64 .
61+
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-linux-arm64 .
62+
GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o $(BINARY_NAME)-windows-amd64.exe .
9463

9564
## tidy: Tidy go.mod
9665
tidy:
97-
@if [ -f go.mod ]; then \
98-
go mod tidy; \
99-
else \
100-
echo "Go source not yet available — skipping tidy"; \
101-
fi
66+
go mod tidy
10267

10368
## help: Show this help
10469
help:

client/cmd/helpers.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
)
7+
8+
// parseRepo splits an "owner/repo" string into owner and repo components.
9+
func parseRepo(nwo string) (string, string, error) {
10+
parts := strings.SplitN(nwo, "/", 2)
11+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
12+
return "", "", fmt.Errorf("invalid repo format %q: expected owner/repo", nwo)
13+
}
14+
return parts[0], parts[1], nil
15+
}

client/cmd/helpers_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestParseRepo_Valid(t *testing.T) {
6+
owner, repo, err := parseRepo("has-ghas/dubbo")
7+
if err != nil {
8+
t.Fatalf("unexpected error: %v", err)
9+
}
10+
if owner != "has-ghas" {
11+
t.Errorf("owner = %q, want %q", owner, "has-ghas")
12+
}
13+
if repo != "dubbo" {
14+
t.Errorf("repo = %q, want %q", repo, "dubbo")
15+
}
16+
}
17+
18+
func TestParseRepo_Invalid(t *testing.T) {
19+
tests := []string{
20+
"",
21+
"noslash",
22+
"/norepo",
23+
"noowner/",
24+
}
25+
for _, input := range tests {
26+
_, _, err := parseRepo(input)
27+
if err == nil {
28+
t.Errorf("parseRepo(%q) should return error", input)
29+
}
30+
}
31+
}

client/cmd/integration_tests.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
11+
itesting "github.com/advanced-security/codeql-development-mcp-server/client/internal/testing"
12+
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/spf13/cobra"
14+
)
15+
16+
var integrationTestsCmd = &cobra.Command{
17+
Use: "integration-tests",
18+
Short: "Run MCP server integration tests from client/integration-tests/",
19+
Long: `Discovers and runs integration test fixtures against a connected MCP server.
20+
21+
Test fixtures live in client/integration-tests/primitives/tools/<tool>/<test>/
22+
and use test-config.json or monitoring-state.json to define tool parameters.`,
23+
RunE: runIntegrationTests,
24+
}
25+
26+
var integrationTestsFlags struct {
27+
tools string
28+
tests string
29+
noInstall bool
30+
timeout int
31+
}
32+
33+
func init() {
34+
rootCmd.AddCommand(integrationTestsCmd)
35+
36+
f := integrationTestsCmd.Flags()
37+
f.StringVar(&integrationTestsFlags.tools, "tools", "", "Comma-separated list of tool names to test")
38+
f.StringVar(&integrationTestsFlags.tests, "tests", "", "Comma-separated list of test case names to run")
39+
f.BoolVar(&integrationTestsFlags.noInstall, "no-install-packs", false, "Skip CodeQL pack installation")
40+
f.IntVar(&integrationTestsFlags.timeout, "timeout", 30, "Per-tool-call timeout in seconds")
41+
}
42+
43+
// mcpToolCaller adapts the MCP client to the ToolCaller interface.
44+
type mcpToolCaller struct {
45+
client *mcpclient.Client
46+
}
47+
48+
func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]itesting.ContentBlock, bool, error) {
49+
result, err := c.client.CallTool(context.Background(), name, params)
50+
if err != nil {
51+
return nil, false, err
52+
}
53+
54+
var blocks []itesting.ContentBlock
55+
for _, item := range result.Content {
56+
if textContent, ok := item.(mcp.TextContent); ok {
57+
blocks = append(blocks, itesting.ContentBlock{
58+
Type: "text",
59+
Text: textContent.Text,
60+
})
61+
}
62+
}
63+
64+
return blocks, result.IsError, nil
65+
}
66+
67+
func (c *mcpToolCaller) ListToolNames() ([]string, error) {
68+
tools, err := c.client.ListTools(context.Background())
69+
if err != nil {
70+
return nil, err
71+
}
72+
names := make([]string, len(tools))
73+
for i, t := range tools {
74+
names[i] = t.Name
75+
}
76+
return names, nil
77+
}
78+
79+
func runIntegrationTests(cmd *cobra.Command, _ []string) error {
80+
// Determine repo root
81+
repoRoot, err := findRepoRoot()
82+
if err != nil {
83+
return fmt.Errorf("cannot determine repo root: %w", err)
84+
}
85+
86+
// Change CWD to repo root so the MCP server subprocess resolves
87+
// relative paths (from test-config.json, monitoring-state.json)
88+
// correctly. The codeql CLI also resolves paths from CWD.
89+
if err := os.Chdir(repoRoot); err != nil {
90+
return fmt.Errorf("chdir to repo root: %w", err)
91+
}
92+
fmt.Printf("Working directory: %s\n", repoRoot)
93+
94+
// Connect to MCP server
95+
client := mcpclient.NewClient(mcpclient.Config{
96+
Mode: MCPMode(),
97+
Host: MCPHost(),
98+
Port: MCPPort(),
99+
})
100+
101+
fmt.Println("🔌 Connecting to MCP server...")
102+
ctx := context.Background()
103+
if err := client.Connect(ctx); err != nil {
104+
return fmt.Errorf("connect to MCP server: %w", err)
105+
}
106+
fmt.Println("✅ Connected to MCP server")
107+
108+
// Parse filters
109+
var filterTools, filterTests []string
110+
if integrationTestsFlags.tools != "" {
111+
filterTools = strings.Split(integrationTestsFlags.tools, ",")
112+
}
113+
if integrationTestsFlags.tests != "" {
114+
filterTests = strings.Split(integrationTestsFlags.tests, ",")
115+
}
116+
117+
// Create and run the test runner
118+
runner := itesting.NewRunner(&mcpToolCaller{client: client}, itesting.RunnerOptions{
119+
RepoRoot: repoRoot,
120+
FilterTools: filterTools,
121+
FilterTests: filterTests,
122+
})
123+
124+
allPassed, _ := runner.Run()
125+
126+
// Close the MCP client (and its stdio subprocess) before returning.
127+
client.Close()
128+
129+
if !allPassed {
130+
return fmt.Errorf("some integration tests failed")
131+
}
132+
return nil
133+
}
134+
135+
// findRepoRoot walks up from the current directory to find the repo root
136+
// (identified by the presence of codeql-workspace.yml).
137+
func findRepoRoot() (string, error) {
138+
// Try from current working directory
139+
dir, err := os.Getwd()
140+
if err != nil {
141+
return "", err
142+
}
143+
144+
for {
145+
if _, err := os.Stat(filepath.Join(dir, "codeql-workspace.yml")); err == nil {
146+
return dir, nil
147+
}
148+
parent := filepath.Dir(dir)
149+
if parent == dir {
150+
break
151+
}
152+
dir = parent
153+
}
154+
155+
// Fallback: try relative to the binary
156+
exe, err := os.Executable()
157+
if err == nil {
158+
dir = filepath.Dir(exe)
159+
for i := 0; i < 5; i++ {
160+
if _, err := os.Stat(filepath.Join(dir, "codeql-workspace.yml")); err == nil {
161+
return dir, nil
162+
}
163+
dir = filepath.Dir(dir)
164+
}
165+
}
166+
167+
return "", fmt.Errorf("could not find repo root (codeql-workspace.yml)")
168+
}

0 commit comments

Comments
 (0)