Skip to content

Commit 279a59b

Browse files
committed
search_issues: address Copilot review findings
- LabelSearchIssues now returns (SecurityLabel, bool); the bool is false when len(repoVisibilities) != len(readerSets), so callers can omit the label rather than emit one computed from inconsistent inputs. - searchIssuesIFCPostProcess no longer substitutes [owner] when the collaborators API returns an empty list. The substitution was inconsistent with the cross-repo intersection semantics: the owner could appear in another matched private repo's collaborator list and thereby widen the joined reader set incorrectly. Empty collaborator sets are now passed through unchanged. - Add a subtest exercising the collaborators-failure branch (500 on /repos/{owner}/{repo}/collaborators), asserting the tool still succeeds and result.Meta["ifc"] is absent. - Extend the LabelSearchIssues table tests with the slice-length mismatch case. Addresses the three Copilot findings on #2456.
1 parent a259da5 commit 279a59b

4 files changed

Lines changed: 68 additions & 15 deletions

File tree

pkg/github/issues.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1018,16 +1018,21 @@ func searchIssuesIFCPostProcess(deps ToolDependencies) searchPostProcessFn {
10181018
if err != nil {
10191019
return
10201020
}
1021-
if len(collaborators) == 0 {
1022-
collaborators = []string{r.owner}
1023-
}
1021+
// Preserve an empty collaborator set as-is. Substituting the
1022+
// owner here would corrupt the cross-repo intersection (the
1023+
// owner could appear in another repo's collaborator list and
1024+
// widen the joined reader set incorrectly).
10241025
readerSets = append(readerSets, collaborators)
10251026
}
10261027

1028+
label, ok := ifc.LabelSearchIssues(visibilities, readerSets)
1029+
if !ok {
1030+
return
1031+
}
10271032
if callResult.Meta == nil {
10281033
callResult.Meta = mcp.Meta{}
10291034
}
1030-
callResult.Meta["ifc"] = ifc.LabelSearchIssues(visibilities, readerSets)
1035+
callResult.Meta["ifc"] = label
10311036
}
10321037
}
10331038

pkg/github/issues_test.go

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -708,11 +708,12 @@ func Test_SearchIssues_IFC_InsidersMode(t *testing.T) {
708708
}
709709

710710
type repoFixture struct {
711-
owner string
712-
repo string
713-
isPrivate bool
714-
collaborators []string
715-
repoStatus int
711+
owner string
712+
repo string
713+
isPrivate bool
714+
collaborators []string
715+
repoStatus int
716+
collaboratorsStatus int
716717
}
717718

718719
repoHandlers := func(repos []repoFixture) map[string]http.HandlerFunc {
@@ -749,6 +750,10 @@ func Test_SearchIssues_IFC_InsidersMode(t *testing.T) {
749750
_, _ = w.Write([]byte("[]"))
750751
return
751752
}
753+
if r.collaboratorsStatus != 0 && r.collaboratorsStatus != http.StatusOK {
754+
w.WriteHeader(r.collaboratorsStatus)
755+
return
756+
}
752757
users := make([]*github.User, len(r.collaborators))
753758
for i, login := range r.collaborators {
754759
users[i] = &github.User{Login: github.Ptr(login)}
@@ -873,6 +878,27 @@ func Test_SearchIssues_IFC_InsidersMode(t *testing.T) {
873878
}
874879
})
875880

881+
t.Run("insiders mode skips ifc label when collaborators lookup fails", func(t *testing.T) {
882+
searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{makeIssue("octocat", "private-repo", 1)}}
883+
deps := BaseDeps{
884+
Client: github.NewClient(makeMockClient(searchResult, []repoFixture{
885+
{owner: "octocat", repo: "private-repo", isPrivate: true, collaboratorsStatus: http.StatusInternalServerError},
886+
})),
887+
Flags: FeatureFlags{InsidersMode: true},
888+
}
889+
handler := serverTool.Handler(deps)
890+
891+
request := createMCPRequest(reqParams)
892+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
893+
require.NoError(t, err)
894+
require.False(t, result.IsError, "tool call should still succeed when collaborators lookup fails")
895+
896+
if result.Meta != nil {
897+
_, hasIFC := result.Meta["ifc"]
898+
assert.False(t, hasIFC, "ifc label should be omitted when collaborators lookup fails")
899+
}
900+
})
901+
876902
t.Run("insiders mode empty results emits public untrusted", func(t *testing.T) {
877903
searchResult := &github.IssuesSearchResult{Issues: []*github.Issue{}}
878904
deps := BaseDeps{

pkg/ifc/ifc.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,22 @@ func LabelGetFileContents(isPrivate bool, readers []string) SecurityLabel {
104104
//
105105
// repoVisibilities[i] reports whether the i-th matched repository is private;
106106
// readerSets[i] is that repository's reader set (only consulted for private
107-
// repos). The two slices must have the same length.
108-
func LabelSearchIssues(repoVisibilities []bool, readerSets [][]string) SecurityLabel {
107+
// repos). The two slices must have the same length; the second return value
108+
// is false when they do not, in which case the caller should omit the label
109+
// rather than emit one computed from inconsistent inputs.
110+
func LabelSearchIssues(repoVisibilities []bool, readerSets [][]string) (SecurityLabel, bool) {
111+
if len(repoVisibilities) != len(readerSets) {
112+
return SecurityLabel{}, false
113+
}
109114
if len(repoVisibilities) == 0 {
110-
return PublicUntrusted()
115+
return PublicUntrusted(), true
111116
}
112117
for _, isPrivate := range repoVisibilities {
113118
if !isPrivate {
114-
return PublicUntrusted()
119+
return PublicUntrusted(), true
115120
}
116121
}
117-
return PrivateUntrusted(intersectReaders(readerSets))
122+
return PrivateUntrusted(intersectReaders(readerSets)), true
118123
}
119124

120125
// intersectReaders returns the readers present in every set, preserving the

pkg/ifc/ifc_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,72 @@ func TestLabelSearchIssues(t *testing.T) {
1313
name string
1414
visibilities []bool
1515
readers [][]string
16+
wantOK bool
1617
wantIntegrity Integrity
1718
wantConfidential []Confidentiality
1819
}{
1920
{
2021
name: "empty result is treated as public",
22+
wantOK: true,
2123
wantIntegrity: IntegrityUntrusted,
2224
wantConfidential: []Confidentiality{ConfidentialityPublic},
2325
},
2426
{
2527
name: "single public repo",
2628
visibilities: []bool{false},
2729
readers: [][]string{nil},
30+
wantOK: true,
2831
wantIntegrity: IntegrityUntrusted,
2932
wantConfidential: []Confidentiality{ConfidentialityPublic},
3033
},
3134
{
3235
name: "mixed public and private collapses to public",
3336
visibilities: []bool{true, false},
3437
readers: [][]string{{"alice"}, nil},
38+
wantOK: true,
3539
wantIntegrity: IntegrityUntrusted,
3640
wantConfidential: []Confidentiality{ConfidentialityPublic},
3741
},
3842
{
3943
name: "two private repos with intersecting collaborators",
4044
visibilities: []bool{true, true},
4145
readers: [][]string{{"alice", "bob", "carol"}, {"bob", "carol", "dan"}},
46+
wantOK: true,
4247
wantIntegrity: IntegrityUntrusted,
4348
wantConfidential: []Confidentiality{"bob", "carol"},
4449
},
4550
{
4651
name: "private repos with no overlap yield empty reader set",
4752
visibilities: []bool{true, true},
4853
readers: [][]string{{"alice"}, {"bob"}},
54+
wantOK: true,
4955
wantIntegrity: IntegrityUntrusted,
5056
wantConfidential: []Confidentiality{},
5157
},
5258
{
5359
name: "intersection preserves first-set order and dedupes",
5460
visibilities: []bool{true, true, true},
5561
readers: [][]string{{"alice", "bob", "alice"}, {"bob", "alice"}, {"alice", "bob"}},
62+
wantOK: true,
5663
wantIntegrity: IntegrityUntrusted,
5764
wantConfidential: []Confidentiality{"alice", "bob"},
5865
},
66+
{
67+
name: "mismatched slice lengths return ok=false",
68+
visibilities: []bool{true, true},
69+
readers: [][]string{{"alice"}},
70+
wantOK: false,
71+
},
5972
}
6073

6174
for _, tc := range tests {
6275
t.Run(tc.name, func(t *testing.T) {
6376
t.Parallel()
64-
label := LabelSearchIssues(tc.visibilities, tc.readers)
77+
label, ok := LabelSearchIssues(tc.visibilities, tc.readers)
78+
assert.Equal(t, tc.wantOK, ok)
79+
if !tc.wantOK {
80+
return
81+
}
6582
assert.Equal(t, tc.wantIntegrity, label.Integrity)
6683
if len(tc.wantConfidential) == 0 {
6784
assert.Empty(t, label.Confidentiality)

0 commit comments

Comments
 (0)