Skip to content

Commit fbf68b2

Browse files
feat: return minimal code search results with text match snippets (#2476)
* feat: return minimal code search results with text match snippets Return a MinimalCodeSearchResult type from search_code instead of the raw GitHub API CodeSearchResult. This reduces token usage by ~4x by: - Projecting the repository object to just the full_name string instead of the full ~3KB repository payload repeated per result - Enabling the text-match Accept header so code snippets (fragments) are included in results, which were previously missing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: drop html_url from MinimalCodeResult The URL is derivable from repository + path + sha, so it's redundant token cost per result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * 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> * refactor: remove minimal_output opt-out from search_code The full CodeResult only adds a bloated Repository object (~3KB of template URLs) and a derivable HTMLURL. Nothing in the full output is useful beyond what the minimal type already provides, so always return the compact form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3a4bc26 commit fbf68b2

4 files changed

Lines changed: 98 additions & 30 deletions

File tree

pkg/github/helper_test.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,15 @@ func expectRequestBody(t *testing.T, expectedRequestBody any) *partialMock {
220220
type partialMock struct {
221221
t *testing.T
222222

223-
expectedPath string
224-
expectedQueryParams map[string]string
225-
expectedRequestBody any
223+
expectedPath string
224+
expectedQueryParams map[string]string
225+
expectedRequestBody any
226+
expectedHeaderContains map[string]string
227+
}
228+
229+
func (p *partialMock) withHeaders(headers map[string]string) *partialMock {
230+
p.expectedHeaderContains = headers
231+
return p
226232
}
227233

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

256+
if p.expectedHeaderContains != nil {
257+
for k, v := range p.expectedHeaderContains {
258+
require.Contains(p.t, r.Header.Get(k), v, "expected header %q to contain %q", k, v)
259+
}
260+
}
261+
250262
responseHandler(w, r)
251263
}
252264
}

pkg/github/minimal_types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ type MinimalSearchRepositoriesResult struct {
5151
Items []MinimalRepository `json:"items"`
5252
}
5353

54+
// MinimalCodeSearchResult is the trimmed output type for code search results.
55+
type MinimalCodeSearchResult struct {
56+
TotalCount int `json:"total_count"`
57+
IncompleteResults bool `json:"incomplete_results"`
58+
Items []MinimalCodeResult `json:"items"`
59+
}
60+
61+
// MinimalCodeResult is the trimmed output type for a single code search hit.
62+
type MinimalCodeResult struct {
63+
Name string `json:"name"`
64+
Path string `json:"path"`
65+
SHA string `json:"sha"`
66+
Repository string `json:"repository"`
67+
TextMatches []*github.TextMatch `json:"text_matches,omitempty"`
68+
}
69+
5470
// MinimalCommitAuthor represents commit author information.
5571
type MinimalCommitAuthor struct {
5672
Name string `json:"name,omitempty"`

pkg/github/search.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,9 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
270270
}
271271

272272
opts := &github.SearchOptions{
273-
Sort: sort,
274-
Order: order,
273+
Sort: sort,
274+
Order: order,
275+
TextMatch: true,
275276
ListOptions: github.ListOptions{
276277
PerPage: pagination.PerPage,
277278
Page: pagination.Page,
@@ -301,7 +302,27 @@ func SearchCode(t translations.TranslationHelperFunc) inventory.ServerTool {
301302
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to search code", resp, body), nil, nil
302303
}
303304

304-
r, err := json.Marshal(result)
305+
minimalItems := make([]MinimalCodeResult, 0, len(result.CodeResults))
306+
for _, code := range result.CodeResults {
307+
item := MinimalCodeResult{
308+
Name: code.GetName(),
309+
Path: code.GetPath(),
310+
SHA: code.GetSHA(),
311+
TextMatches: code.TextMatches,
312+
}
313+
if code.Repository != nil {
314+
item.Repository = code.Repository.GetFullName()
315+
}
316+
minimalItems = append(minimalItems, item)
317+
}
318+
319+
minimalResult := &MinimalCodeSearchResult{
320+
TotalCount: result.GetTotal(),
321+
IncompleteResults: result.GetIncompleteResults(),
322+
Items: minimalItems,
323+
}
324+
325+
r, err := json.Marshal(minimalResult)
305326
if err != nil {
306327
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
307328
}

pkg/github/search_test.go

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -430,22 +430,35 @@ func Test_SearchCode(t *testing.T) {
430430
IncompleteResults: github.Ptr(false),
431431
CodeResults: []*github.CodeResult{
432432
{
433-
Name: github.Ptr("file1.go"),
434-
Path: github.Ptr("path/to/file1.go"),
435-
SHA: github.Ptr("abc123def456"),
436-
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file1.go"),
437-
Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
433+
Name: github.Ptr("file1.go"),
434+
Path: github.Ptr("path/to/file1.go"),
435+
SHA: github.Ptr("abc123def456"),
436+
Repository: &github.Repository{
437+
Name: github.Ptr("repo"),
438+
FullName: github.Ptr("owner/repo"),
439+
},
440+
TextMatches: []*github.TextMatch{
441+
{
442+
Fragment: github.Ptr("func main() { fmt.Println(\"hello\") }"),
443+
},
444+
},
438445
},
439446
{
440-
Name: github.Ptr("file2.go"),
441-
Path: github.Ptr("path/to/file2.go"),
442-
SHA: github.Ptr("def456abc123"),
443-
HTMLURL: github.Ptr("https://github.com/owner/repo/blob/main/path/to/file2.go"),
444-
Repository: &github.Repository{Name: github.Ptr("repo"), FullName: github.Ptr("owner/repo")},
447+
Name: github.Ptr("file2.go"),
448+
Path: github.Ptr("path/to/file2.go"),
449+
SHA: github.Ptr("def456abc123"),
450+
Repository: &github.Repository{
451+
Name: github.Ptr("repo"),
452+
FullName: github.Ptr("owner/repo"),
453+
},
445454
},
446455
},
447456
}
448457

458+
textMatchAcceptHeader := map[string]string{
459+
"Accept": "text-match",
460+
}
461+
449462
tests := []struct {
450463
name string
451464
mockedClient *http.Client
@@ -463,7 +476,7 @@ func Test_SearchCode(t *testing.T) {
463476
"order": "desc",
464477
"page": "1",
465478
"per_page": "30",
466-
}).andThen(
479+
}).withHeaders(textMatchAcceptHeader).andThen(
467480
mockResponse(t, http.StatusOK, mockSearchResult),
468481
),
469482
}),
@@ -484,7 +497,7 @@ func Test_SearchCode(t *testing.T) {
484497
"q": "fmt.Println language:go",
485498
"page": "1",
486499
"per_page": "30",
487-
}).andThen(
500+
}).withHeaders(textMatchAcceptHeader).andThen(
488501
mockResponse(t, http.StatusOK, mockSearchResult),
489502
),
490503
}),
@@ -537,22 +550,28 @@ func Test_SearchCode(t *testing.T) {
537550
require.NoError(t, err)
538551
require.False(t, result.IsError)
539552

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

543-
// Unmarshal and verify the result
544-
var returnedResult github.CodeSearchResult
555+
var returnedResult MinimalCodeSearchResult
545556
err = json.Unmarshal([]byte(textContent.Text), &returnedResult)
546557
require.NoError(t, err)
547-
assert.Equal(t, *tc.expectedResult.Total, *returnedResult.Total)
548-
assert.Equal(t, *tc.expectedResult.IncompleteResults, *returnedResult.IncompleteResults)
549-
assert.Len(t, returnedResult.CodeResults, len(tc.expectedResult.CodeResults))
550-
for i, code := range returnedResult.CodeResults {
551-
assert.Equal(t, *tc.expectedResult.CodeResults[i].Name, *code.Name)
552-
assert.Equal(t, *tc.expectedResult.CodeResults[i].Path, *code.Path)
553-
assert.Equal(t, *tc.expectedResult.CodeResults[i].SHA, *code.SHA)
554-
assert.Equal(t, *tc.expectedResult.CodeResults[i].HTMLURL, *code.HTMLURL)
555-
assert.Equal(t, *tc.expectedResult.CodeResults[i].Repository.FullName, *code.Repository.FullName)
558+
assert.Equal(t, *tc.expectedResult.Total, returnedResult.TotalCount)
559+
assert.Equal(t, *tc.expectedResult.IncompleteResults, returnedResult.IncompleteResults)
560+
assert.Len(t, returnedResult.Items, len(tc.expectedResult.CodeResults))
561+
for i, code := range returnedResult.Items {
562+
assert.Equal(t, tc.expectedResult.CodeResults[i].GetName(), code.Name)
563+
assert.Equal(t, tc.expectedResult.CodeResults[i].GetPath(), code.Path)
564+
assert.Equal(t, tc.expectedResult.CodeResults[i].GetSHA(), code.SHA)
565+
assert.Equal(t, tc.expectedResult.CodeResults[i].Repository.GetFullName(), code.Repository)
566+
}
567+
568+
// Verify text matches are included when present
569+
if len(tc.expectedResult.CodeResults[0].TextMatches) > 0 {
570+
require.NotEmpty(t, returnedResult.Items[0].TextMatches)
571+
assert.Equal(t,
572+
tc.expectedResult.CodeResults[0].TextMatches[0].GetFragment(),
573+
returnedResult.Items[0].TextMatches[0].GetFragment(),
574+
)
556575
}
557576
})
558577
}

0 commit comments

Comments
 (0)