Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix: add minimal_output opt-out and Accept header test for code search
Address PR review feedback:

1. Add minimal_output parameter (default: true) to search_code, matching
   the pattern from search_repositories. When false, returns the full
   GitHub API CodeSearchResult for backward compatibility.

2. Add Accept header assertion to tests via a new withHeaders() helper
   on partialMock, verifying the text-match Accept header is actually
   requested (not just mocked in the response).

3. Add test case for minimal_output=false path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  • Loading branch information
SamMorrowDrums and Copilot committed May 13, 2026
commit f11bf9ad0a2e42672fa1478fa5d304303c098d5c
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1273,6 +1273,7 @@ The following sets of tools are available:

- **search_code** - Search code
- **Required OAuth Scopes**: `repo`
- `minimal_output`: Return minimal repository information (default: true). When false, returns full GitHub API repository objects. (boolean, optional)
- `order`: Sort order for results (string, optional)
- `page`: Page number for pagination (min 1) (number, optional)
- `perPage`: Results per page for pagination (min 1, max 100) (number, optional)
Expand Down
5 changes: 5 additions & 0 deletions pkg/github/__toolsnaps__/search_code.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"description": "Fast and precise code search across ALL GitHub repositories using GitHub's native search engine. Best for finding exact symbols, functions, classes, or specific code patterns.",
"inputSchema": {
"properties": {
"minimal_output": {
"default": true,
"description": "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.",
"type": "boolean"
},
"order": {
"description": "Sort order for results",
"enum": [
Expand Down
18 changes: 15 additions & 3 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,15 @@ func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock {
type partialMock struct {
t *testing.T

expectedPath string
expectedQueryParams map[string]string
expectedRequestBody any
expectedPath string
expectedQueryParams map[string]string
expectedRequestBody any
expectedHeaderContains map[string]string
}

func (p *partialMock) withHeaders(headers map[string]string) *partialMock {
p.expectedHeaderContains = headers
return p
}

func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc {
Expand All @@ -247,6 +253,12 @@ func (p *partialMock) andThen(responseHandler http.HandlerFunc) http.HandlerFunc
require.Equal(p.t, p.expectedRequestBody, unmarshaledRequestBody)
}

if p.expectedHeaderContains != nil {
for k, v := range p.expectedHeaderContains {
require.Contains(p.t, r.Header.Get(k), v, "expected header %q to contain %q", k, v)
}
}

responseHandler(w, r)
}
}
Expand Down
57 changes: 37 additions & 20 deletions pkg/github/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
Description: "Sort order for results",
Enum: []any{"asc", "desc"},
},
"minimal_output": {
Type: "boolean",
Description: "Return minimal repository information (default: true). When false, returns full GitHub API repository objects.",
Default: json.RawMessage(`true`),
},
},
Required: []string{"query"},
}
Expand Down Expand Up @@ -268,6 +273,10 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}
minimalOutput, err := OptionalBoolParamWithDefault(args, "minimal_output", true)
if err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
}

opts := &github.SearchOptions{
Sort: sort,
Expand Down Expand Up @@ -302,29 +311,37 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to search code", resp, body), nil, nil
}

minimalItems := make([]MinimalCodeResult, 0, len(result.CodeResults))
for _, code := range result.CodeResults {
item := MinimalCodeResult{
Name: code.GetName(),
Path: code.GetPath(),
SHA: code.GetSHA(),
TextMatches: code.TextMatches,
}
if code.Repository != nil {
item.Repository = code.Repository.GetFullName()
var r []byte
if minimalOutput {
minimalItems := make([]MinimalCodeResult, 0, len(result.CodeResults))
for _, code := range result.CodeResults {
item := MinimalCodeResult{
Name: code.GetName(),
Path: code.GetPath(),
SHA: code.GetSHA(),
TextMatches: code.TextMatches,
}
if code.Repository != nil {
item.Repository = code.Repository.GetFullName()
}
minimalItems = append(minimalItems, item)
}
minimalItems = append(minimalItems, item)
}

minimalResult := &MinimalCodeSearchResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: minimalItems,
}
minimalResult := &MinimalCodeSearchResult{
TotalCount: result.GetTotal(),
IncompleteResults: result.GetIncompleteResults(),
Items: minimalItems,
}

r, err := json.Marshal(minimalResult)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
r, err = json.Marshal(minimalResult)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal minimal response", err), nil, nil
}
} else {
r, err = json.Marshal(result)
if err != nil {
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
}
}

return utils.NewToolResultText(string(r)), nil, nil
Expand Down
88 changes: 65 additions & 23 deletions pkg/github/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ func Test_SearchCode(t *testing.T) {
assert.Contains(t, schema.Properties, "order")
assert.Contains(t, schema.Properties, "perPage")
assert.Contains(t, schema.Properties, "page")
assert.Contains(t, schema.Properties, "minimal_output")
assert.ElementsMatch(t, schema.Required, []string{"query"})

// Setup mock search results
Expand Down Expand Up @@ -455,13 +456,18 @@ func Test_SearchCode(t *testing.T) {
},
}

textMatchAcceptHeader := map[string]string{
"Accept": "text-match",
}

tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedResult *github.CodeSearchResult
expectedErrMsg string
minimalOutput bool
}{
{
name: "successful code search with all parameters",
Expand All @@ -472,7 +478,7 @@ func Test_SearchCode(t *testing.T) {
"order": "desc",
"page": "1",
"per_page": "30",
}).andThen(
}).withHeaders(textMatchAcceptHeader).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
}),
Expand All @@ -485,6 +491,7 @@ func Test_SearchCode(t *testing.T) {
},
expectError: false,
expectedResult: mockSearchResult,
minimalOutput: true,
},
{
name: "code search with minimal parameters",
Expand All @@ -493,7 +500,7 @@ func Test_SearchCode(t *testing.T) {
"q": "fmt.Println language:go",
"page": "1",
"per_page": "30",
}).andThen(
}).withHeaders(textMatchAcceptHeader).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
}),
Expand All @@ -502,6 +509,26 @@ func Test_SearchCode(t *testing.T) {
},
expectError: false,
expectedResult: mockSearchResult,
minimalOutput: true,
},
{
name: "code search with full output",
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
GetSearchCode: expectQueryParams(t, map[string]string{
"q": "fmt.Println language:go",
"page": "1",
"per_page": "30",
}).withHeaders(textMatchAcceptHeader).andThen(
mockResponse(t, http.StatusOK, mockSearchResult),
),
}),
requestArgs: map[string]any{
"query": "fmt.Println language:go",
"minimal_output": false,
},
expectError: false,
expectedResult: mockSearchResult,
minimalOutput: false,
},
{
name: "search code fails",
Expand Down Expand Up @@ -546,30 +573,45 @@ func Test_SearchCode(t *testing.T) {
require.NoError(t, err)
require.False(t, result.IsError)

// Parse the result and get the text content if no error
textContent := getTextResult(t, result)

// Unmarshal and verify the minimal result
var returnedResult MinimalCodeSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
assert.Len(t, returnedResult.Items, len(tc.expectedResult.CodeResults))
for i, code := range returnedResult.Items {
assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name)
assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path)
assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA)
assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository)
}
if tc.minimalOutput {
// Unmarshal and verify the minimal result
var returnedResult MinimalCodeSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
assert.Len(t, returnedResult.Items, len(tc.expectedResult.CodeResults))
for i, code := range returnedResult.Items {
assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name)
assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path)
assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA)
assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository)
}

// Verify text matches are included when present
if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 {
require.NotEmpty(t, returnedResult.Items[0].TextMatches)
assert.Equal(t,
tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(),
returnedResult.Items[0].TextMatches[0].GetFragment(),
)
// Verify text matches are included when present
if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 {
require.NotEmpty(t, returnedResult.Items[0].TextMatches)
assert.Equal(t,
tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(),
returnedResult.Items[0].TextMatches[0].GetFragment(),
)
}
} else {
// Verify the full result is returned
var returnedResult github.CodeSearchResult
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
require.NoError(t, err)
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
assert.Len(t, returnedResult.CodeResults, len(tc.expectedResult.CodeResults))
for i, code := range returnedResult.CodeResults {
assert.Equal(t, *tc.expectedResult.CodeResults[i].Name, *code.Name)
assert.Equal(t, *tc.expectedResult.CodeResults[i].Path, *code.Path)
assert.Equal(t, *tc.expectedResult.CodeResults[i].SHA, *code.SHA)
assert.Equal(t, *tc.expectedResult.CodeResults[i].Repository.FullName, *code.Repository.FullName)
}
}
})
}
Expand Down
Loading