Skip to content

Commit 13855d4

Browse files
committed
Address PR review feedback
1 parent 23e3430 commit 13855d4

File tree

6 files changed

+213
-12
lines changed

6 files changed

+213
-12
lines changed

client/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ gh ql-mcp-client <command> [flags]
3939
| `--format` | `text` | Output format (`text`/`json`) |
4040

4141
Transport is configured via CLI flags. The CLI does not currently read `MCP_MODE`.
42+
4243
### Commands
4344

4445
#### `code-scanning` (alias: `cs`)

client/cmd/integration_tests.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"path/filepath"
88
"strings"
9+
"time"
910

1011
mcpclient "github.com/advanced-security/codeql-development-mcp-server/client/internal/mcp"
1112
itesting "github.com/advanced-security/codeql-development-mcp-server/client/internal/testing"
@@ -37,16 +38,23 @@ func init() {
3738
f.StringVar(&integrationTestsFlags.tools, "tools", "", "Comma-separated list of tool names to test")
3839
f.StringVar(&integrationTestsFlags.tests, "tests", "", "Comma-separated list of test case names to run")
3940
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+
f.IntVar(&integrationTestsFlags.timeout, "timeout", 0, "Per-tool-call timeout in seconds (0 = use server defaults)")
4142
}
4243

4344
// mcpToolCaller adapts the MCP client to the ToolCaller interface.
4445
type mcpToolCaller struct {
45-
client *mcpclient.Client
46+
client *mcpclient.Client
47+
timeout time.Duration
4648
}
4749

4850
func (c *mcpToolCaller) CallToolRaw(name string, params map[string]any) ([]itesting.ContentBlock, bool, error) {
49-
result, err := c.client.CallTool(context.Background(), name, params)
51+
ctx := context.Background()
52+
if c.timeout > 0 {
53+
var cancel context.CancelFunc
54+
ctx, cancel = context.WithTimeout(ctx, c.timeout)
55+
defer cancel()
56+
}
57+
result, err := c.client.CallTool(ctx, name, params)
5058
if err != nil {
5159
return nil, false, err
5260
}
@@ -115,10 +123,14 @@ func runIntegrationTests(cmd *cobra.Command, _ []string) error {
115123
}
116124

117125
// Create and run the test runner
118-
runner := itesting.NewRunner(&mcpToolCaller{client: client}, itesting.RunnerOptions{
119-
RepoRoot: repoRoot,
120-
FilterTools: filterTools,
121-
FilterTests: filterTests,
126+
runner := itesting.NewRunner(&mcpToolCaller{
127+
client: client,
128+
timeout: time.Duration(integrationTestsFlags.timeout) * time.Second,
129+
}, itesting.RunnerOptions{
130+
RepoRoot: repoRoot,
131+
FilterTools: filterTools,
132+
FilterTests: filterTests,
133+
NoInstallPacks: integrationTestsFlags.noInstall,
122134
})
123135

124136
allPassed, _ := runner.Run()

client/internal/testing/params.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ func buildToolParams(repoRoot, toolName, testCase, testDir string) (map[string]a
2828
if err := json.Unmarshal(configData, &config); err != nil {
2929
return nil, fmt.Errorf("parse test-config.json: %w", err)
3030
}
31-
return config.Arguments, nil
31+
// SARIF tools need derived path injection from before/ even when
32+
// test-config.json provides base params — continue to tool-specific logic.
33+
if !isSARIFTool(toolName) {
34+
return config.Arguments, nil
35+
}
3236
}
3337

3438
// Check monitoring-state.json for embedded parameters
@@ -317,6 +321,12 @@ func buildToolParams(repoRoot, toolName, testCase, testDir string) (map[string]a
317321
return params, nil
318322
}
319323

324+
// isSARIFTool returns true for tools that need SARIF file path injection
325+
// from the before/ directory even when test-config.json provides base params.
326+
func isSARIFTool(name string) bool {
327+
return strings.HasPrefix(name, "sarif_")
328+
}
329+
320330
// findFilesByExt returns filenames in dir matching the given extension.
321331
func findFilesByExt(dir, ext string) []string {
322332
entries, err := os.ReadDir(dir)

client/internal/testing/params_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,95 @@ func TestFindFilesByExt(t *testing.T) {
116116
t.Errorf("found %d .txt files, want 1", len(txtFiles))
117117
}
118118
}
119+
120+
func TestIsSARIFTool(t *testing.T) {
121+
tests := []struct {
122+
name string
123+
want bool
124+
}{
125+
{"sarif_extract_rule", true},
126+
{"sarif_list_rules", true},
127+
{"sarif_compare_alerts", true},
128+
{"sarif_diff_runs", true},
129+
{"sarif_rule_to_markdown", true},
130+
{"codeql_query_run", false},
131+
{"validate_codeql_query", false},
132+
{"codeql_pack_install", false},
133+
}
134+
for _, tt := range tests {
135+
if got := isSARIFTool(tt.name); got != tt.want {
136+
t.Errorf("isSARIFTool(%q) = %v, want %v", tt.name, got, tt.want)
137+
}
138+
}
139+
}
140+
141+
func TestBuildToolParams_SARIFToolWithConfig(t *testing.T) {
142+
dir := t.TempDir()
143+
testDir := filepath.Join(dir, "tools", "sarif_extract_rule", "extract_sql_injection")
144+
beforeDir := filepath.Join(testDir, "before")
145+
os.MkdirAll(beforeDir, 0o755)
146+
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
147+
148+
// Write test-config.json with ruleId but no sarifPath
149+
os.WriteFile(filepath.Join(testDir, "test-config.json"),
150+
[]byte(`{"toolName":"sarif_extract_rule","arguments":{"ruleId":"js/sql-injection"}}`), 0o600)
151+
152+
// Write a SARIF file in before/
153+
os.WriteFile(filepath.Join(beforeDir, "test-input.sarif"),
154+
[]byte(`{"version":"2.1.0"}`), 0o600)
155+
156+
params, err := buildToolParams(dir, "sarif_extract_rule", "extract_sql_injection", testDir)
157+
if err != nil {
158+
t.Fatalf("unexpected error: %v", err)
159+
}
160+
161+
// Should have sarifPath injected from before/
162+
sarifPath, ok := params["sarifPath"].(string)
163+
if !ok || sarifPath == "" {
164+
t.Error("expected sarifPath to be injected from before/ directory")
165+
}
166+
167+
// Should still have ruleId from config
168+
if params["ruleId"] != "js/sql-injection" {
169+
t.Errorf("params[ruleId] = %v, want js/sql-injection", params["ruleId"])
170+
}
171+
}
172+
173+
func TestBuildToolParams_SARIFCompareAlertsWithConfig(t *testing.T) {
174+
dir := t.TempDir()
175+
testDir := filepath.Join(dir, "tools", "sarif_compare_alerts", "sink_overlap")
176+
beforeDir := filepath.Join(testDir, "before")
177+
os.MkdirAll(beforeDir, 0o755)
178+
os.MkdirAll(filepath.Join(testDir, "after"), 0o755)
179+
180+
// Write test-config.json with alertA/alertB but no sarifPath
181+
os.WriteFile(filepath.Join(testDir, "test-config.json"),
182+
[]byte(`{"toolName":"sarif_compare_alerts","arguments":{"alertA":{"ruleId":"r1","resultIndex":0},"alertB":{"ruleId":"r2","resultIndex":0},"overlapMode":"sink"}}`), 0o600)
183+
184+
// Write a SARIF file in before/
185+
os.WriteFile(filepath.Join(beforeDir, "test-input.sarif"),
186+
[]byte(`{"version":"2.1.0"}`), 0o600)
187+
188+
params, err := buildToolParams(dir, "sarif_compare_alerts", "sink_overlap", testDir)
189+
if err != nil {
190+
t.Fatalf("unexpected error: %v", err)
191+
}
192+
193+
// alertA should have sarifPath injected
194+
alertA, ok := params["alertA"].(map[string]any)
195+
if !ok {
196+
t.Fatal("expected alertA in params")
197+
}
198+
if alertA["sarifPath"] == nil || alertA["sarifPath"] == "" {
199+
t.Error("expected sarifPath injected into alertA")
200+
}
201+
202+
// alertB should have sarifPath injected
203+
alertB, ok := params["alertB"].(map[string]any)
204+
if !ok {
205+
t.Fatal("expected alertB in params")
206+
}
207+
if alertB["sarifPath"] == nil || alertB["sarifPath"] == "" {
208+
t.Error("expected sarifPath injected into alertB")
209+
}
210+
}

client/internal/testing/runner.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ type TestResult struct {
3838

3939
// RunnerOptions configures the integration test runner.
4040
type RunnerOptions struct {
41-
FilterTests []string
42-
FilterTools []string
43-
RepoRoot string
41+
FilterTests []string
42+
FilterTools []string
43+
NoInstallPacks bool
44+
RepoRoot string
4445
}
4546

4647
// Runner discovers and executes integration tests.
@@ -54,7 +55,7 @@ type Runner struct {
5455

5556
// NewRunner creates a new integration test runner.
5657
func NewRunner(caller ToolCaller, opts RunnerOptions) *Runner {
57-
tmpBase := filepath.Join(opts.RepoRoot, ".tmp")
58+
tmpBase := filepath.Join(opts.RepoRoot, "server", ".tmp")
5859
return &Runner{
5960
caller: caller,
6061
options: opts,
@@ -124,6 +125,12 @@ func (r *Runner) Run() (bool, []TestResult) {
124125
}
125126

126127
func (r *Runner) runToolTests(toolName, testsDir string) {
128+
// Skip codeql_pack_install when --no-install-packs is set
129+
if r.options.NoInstallPacks && toolName == "codeql_pack_install" {
130+
fmt.Printf("\n %s (skipped: --no-install-packs)\n", toolName)
131+
return
132+
}
133+
127134
// Deprecated monitoring/session tools — skip entirely
128135
if isDeprecatedTool(toolName) {
129136
fmt.Printf("\n %s (skipped: deprecated)\n", toolName)
@@ -227,6 +234,14 @@ func (r *Runner) runSingleTest(toolName, testCase, toolDir string) {
227234
return
228235
}
229236

237+
// Validate non-empty response content — catches broken tools that
238+
// return success with empty results.
239+
if len(content) == 0 {
240+
r.recordResult(toolName, testCase, false, "tool returned no content blocks", elapsed)
241+
fmt.Printf(" FAIL %s (no content) [%.1fs]\n", testCase, elapsed.Seconds())
242+
return
243+
}
244+
230245
r.recordResult(toolName, testCase, true, "", elapsed)
231246
fmt.Printf(" PASS %s [%.1fs]\n", testCase, elapsed.Seconds())
232247
}

client/internal/testing/runner_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package testing
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57
)
68

@@ -119,3 +121,72 @@ func TestRunnerWithMockCaller(t *testing.T) {
119121
t.Errorf("expected 0 results, got %d", len(results))
120122
}
121123
}
124+
125+
func TestRunnerNoInstallPacks(t *testing.T) {
126+
caller := newMockCaller()
127+
128+
opts := RunnerOptions{
129+
RepoRoot: "/nonexistent",
130+
NoInstallPacks: true,
131+
FilterTools: []string{"codeql_pack_install"},
132+
}
133+
134+
runner := NewRunner(caller, opts)
135+
if runner == nil {
136+
t.Fatal("NewRunner returned nil")
137+
}
138+
139+
// codeql_pack_install should be skipped — no results
140+
allPassed, results := runner.Run()
141+
if !allPassed {
142+
t.Error("expected allPassed=true when pack install is skipped")
143+
}
144+
if len(results) != 0 {
145+
t.Errorf("expected 0 results (skipped), got %d", len(results))
146+
}
147+
}
148+
149+
func TestRunnerEmptyContentFails(t *testing.T) {
150+
caller := newMockCaller()
151+
// Return empty content blocks for mock_tool
152+
caller.results["mock_tool"] = mockResult{
153+
content: []ContentBlock{},
154+
isError: false,
155+
err: nil,
156+
}
157+
158+
// Create a minimal test fixture directory
159+
dir := t.TempDir()
160+
testsDir := filepath.Join(dir, "client", "integration-tests", "primitives", "tools")
161+
toolDir := filepath.Join(testsDir, "mock_tool", "basic")
162+
os.MkdirAll(filepath.Join(toolDir, "before"), 0o755)
163+
os.MkdirAll(filepath.Join(toolDir, "after"), 0o755)
164+
os.WriteFile(filepath.Join(toolDir, "test-config.json"),
165+
[]byte(`{"toolName":"mock_tool","arguments":{"key":"value"}}`), 0o600)
166+
167+
opts := RunnerOptions{
168+
RepoRoot: dir,
169+
}
170+
171+
runner := NewRunner(caller, opts)
172+
allPassed, results := runner.Run()
173+
174+
if allPassed {
175+
t.Error("expected allPassed=false when tool returns empty content")
176+
}
177+
if len(results) == 0 {
178+
t.Fatal("expected at least one result")
179+
}
180+
for _, r := range results {
181+
if r.ToolName == "mock_tool" && r.TestName == "basic" {
182+
if r.Passed {
183+
t.Error("expected mock_tool/basic to fail with empty content")
184+
}
185+
if r.Error != "tool returned no content blocks" {
186+
t.Errorf("unexpected error: %q", r.Error)
187+
}
188+
return
189+
}
190+
}
191+
t.Error("mock_tool/basic result not found")
192+
}

0 commit comments

Comments
 (0)