Skip to content

Commit 8fc416b

Browse files
authored
[test] Add tests for difc.IsSingularReadTool (#8201)
## Test Coverage Improvement: `IsSingularReadTool` ### Function Analyzed - **Package**: `internal/difc` - **Function**: `IsSingularReadTool` - **File**: `internal/difc/tool_classification.go` - **Previous Coverage**: 0% - **New Coverage**: 100% - **Package Coverage**: 99.7% → 99.9% - **Complexity**: Low (single-line function with two prefix checks) ### Why This Function? `IsSingularReadTool` had **0% coverage** with no test file at all — the clearest possible testing gap. Despite its simplicity, it plays an important semantic role in the DIFC pipeline: it distinguishes between *singular* read tools (e.g. `get_issue`, `issue_read`) and *collection* tools (e.g. `list_issues`, `search_code`). The distinction determines whether a filtered result returns an MCP error vs an empty list (see usage in `internal/server/unified.go`). ### Tests Added - ✅ **Singular read tools**: `get_*`, `*_read`, `create_*`, `update_*`, `delete_*`, `read_*` — all return `true` - ✅ **`list_` prefix**: `list_issues`, `list_commits`, `list_pull_requests`, `list_` alone — all return `false` - ✅ **`search_` prefix**: `search_code`, `search_issues`, `search_pull_requests`, `search_` alone — all return `false` - ✅ **No underscore edge cases**: `list`, `search` (no `_`) — return `true` - ✅ **Similar-but-non-matching prefixes**: `listed_`, `listing_`, `searching_` — return `true` - ✅ **Names containing keywords mid-string**: `get_list`, `global_search` — return `true` - ✅ **Case sensitivity**: `List_`, `LIST_`, `Search_`, `SEARCH_` (uppercase) — return `true` - ✅ **Boundary inputs**: empty string, single character — return `true` ### Coverage Report ``` Before: 0.0% coverage (IsSingularReadTool) After: 100.0% coverage (IsSingularReadTool) Package improvement: 99.7% → 99.9% ``` ### Test Execution All 29 sub-tests pass: ``` --- PASS: TestIsSingularReadTool (0.00s) (29 sub-tests all PASS) ok github.com/github/gh-aw-mcpg/internal/difc 0.005s coverage: 99.9% of statements ``` --- *Generated by Test Coverage Improver* *Next run should target the next most complex under-tested function (e.g. `LogUnrecognizedEndpointPassthrough` at 75% or `BuildVersionString` at 89.5%)* > [!WARNING] > <details> > <summary>Firewall blocked 7 domains</summary> > > The following domains were blocked by the firewall during workflow execution: > > - `go.opentelemetry.io` > - `go.yaml.in` > - `golang.org` > - `google.golang.org` > - `gopkg.in` > - `proxy.golang.org` > - `releaseassets.githubusercontent.com` >> To allow these domains, add them to the `network.allowed` list in your workflow frontmatter: > > ```yaml > network: > allowed: > - defaults > - "go.opentelemetry.io" > - "go.yaml.in" > - "golang.org" > - "google.golang.org" > - "gopkg.in" > - "proxy.golang.org" > - "releaseassets.githubusercontent.com" > ``` > > See [Network Configuration](https://github.github.com/gh-aw/reference/network/) for more information. > > </details> > Generated by [Test Coverage Improver](https://github.com/github/gh-aw-mcpg/actions/runs/28295403844) · 224.6 AIC · ⊞ 7.1K · [◷](https://github.com/search?q=repo%3Agithub%2Fgh-aw-mcpg+%22gh-aw-workflow-id%3A+test-coverage-improver%22&type=pullrequests) <!-- gh-aw-agentic-workflow: Test Coverage Improver, engine: copilot, version: 1.0.63, model: claude-sonnet-4.6, id: 28295403844, workflow_id: test-coverage-improver, run: https://github.com/github/gh-aw-mcpg/actions/runs/28295403844 --> <!-- gh-aw-workflow-id: test-coverage-improver --> <!-- gh-aw-workflow-call-id: github/gh-aw-mcpg/test-coverage-improver -->
2 parents 86b396d + 8e7f91b commit 8fc416b

1 file changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package difc
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestIsSingularReadTool(t *testing.T) {
10+
t.Parallel()
11+
12+
tests := []struct {
13+
name string
14+
toolName string
15+
want bool
16+
}{
17+
// Singular read tools: any name not starting with "list_" or "search_"
18+
{
19+
name: "get_ prefix is a singular read tool",
20+
toolName: "get_issue",
21+
want: true,
22+
},
23+
{
24+
name: "get_ prefix with long name is a singular read tool",
25+
toolName: "get_file_contents",
26+
want: true,
27+
},
28+
{
29+
name: "_read suffix is a singular read tool",
30+
toolName: "issue_read",
31+
want: true,
32+
},
33+
{
34+
name: "create_ prefix is treated as a singular tool",
35+
toolName: "create_issue",
36+
want: true,
37+
},
38+
{
39+
name: "update_ prefix is treated as a singular tool",
40+
toolName: "update_pull_request",
41+
want: true,
42+
},
43+
{
44+
name: "delete_ prefix is treated as a singular tool",
45+
toolName: "delete_branch",
46+
want: true,
47+
},
48+
{
49+
name: "read_ prefix is a singular read tool",
50+
toolName: "read_file",
51+
want: true,
52+
},
53+
{
54+
name: "arbitrary non-prefixed tool name is singular",
55+
toolName: "fork_repository",
56+
want: true,
57+
},
58+
59+
// Collection tools: prefix "list_" → false
60+
{
61+
name: "list_ prefix is a collection tool",
62+
toolName: "list_issues",
63+
want: false,
64+
},
65+
{
66+
name: "list_ prefix with different resource is a collection tool",
67+
toolName: "list_commits",
68+
want: false,
69+
},
70+
{
71+
name: "list_ prefix with multi-word resource",
72+
toolName: "list_pull_requests",
73+
want: false,
74+
},
75+
{
76+
name: "list_ alone (nothing after underscore) is a collection tool",
77+
toolName: "list_",
78+
want: false,
79+
},
80+
81+
// Collection tools: prefix "search_" → false
82+
{
83+
name: "search_ prefix is a collection tool",
84+
toolName: "search_code",
85+
want: false,
86+
},
87+
{
88+
name: "search_ prefix with different resource",
89+
toolName: "search_issues",
90+
want: false,
91+
},
92+
{
93+
name: "search_ prefix with multi-word resource",
94+
toolName: "search_pull_requests",
95+
want: false,
96+
},
97+
{
98+
name: "search_ alone is a collection tool",
99+
toolName: "search_",
100+
want: false,
101+
},
102+
103+
// Edge cases: prefix without underscore does NOT match
104+
{
105+
name: "list without underscore is singular (no list_ prefix)",
106+
toolName: "list",
107+
want: true,
108+
},
109+
{
110+
name: "search without underscore is singular (no search_ prefix)",
111+
toolName: "search",
112+
want: true,
113+
},
114+
115+
// Edge cases: similar but not matching prefixes
116+
{
117+
name: "listed_ prefix does not match list_ — is singular",
118+
toolName: "listed_items",
119+
want: true,
120+
},
121+
{
122+
name: "listing_ prefix does not match list_ — is singular",
123+
toolName: "listing_files",
124+
want: true,
125+
},
126+
{
127+
name: "searching_ prefix does not match search_ — is singular",
128+
toolName: "searching_code",
129+
want: true,
130+
},
131+
{
132+
name: "get_list name contains list but does not start with list_",
133+
toolName: "get_list",
134+
want: true,
135+
},
136+
{
137+
name: "global_search name contains search but does not start with search_",
138+
toolName: "global_search",
139+
want: true,
140+
},
141+
142+
// Edge cases: case sensitivity — Go's strings.HasPrefix is case-sensitive
143+
{
144+
name: "List_ with capital L is singular (case-sensitive)",
145+
toolName: "List_issues",
146+
want: true,
147+
},
148+
{
149+
name: "LIST_ uppercase is singular (case-sensitive)",
150+
toolName: "LIST_ISSUES",
151+
want: true,
152+
},
153+
{
154+
name: "Search_ with capital S is singular (case-sensitive)",
155+
toolName: "Search_code",
156+
want: true,
157+
},
158+
{
159+
name: "SEARCH_ uppercase is singular (case-sensitive)",
160+
toolName: "SEARCH_CODE",
161+
want: true,
162+
},
163+
164+
// Edge cases: empty and minimal strings
165+
{
166+
name: "empty string is singular (no prefix match)",
167+
toolName: "",
168+
want: true,
169+
},
170+
{
171+
name: "single character is singular",
172+
toolName: "x",
173+
want: true,
174+
},
175+
}
176+
177+
for _, tt := range tests {
178+
tt := tt
179+
t.Run(tt.name, func(t *testing.T) {
180+
t.Parallel()
181+
got := IsSingularReadTool(tt.toolName)
182+
assert.Equal(t, tt.want, got, "IsSingularReadTool(%q)", tt.toolName)
183+
})
184+
}
185+
}

0 commit comments

Comments
 (0)