Skip to content

Commit e227cad

Browse files
test(context): pin structured output and legacy text fixtures
Describe profile timestamps and validate null and empty team outputs against modern schemas while retaining exact legacy text. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 0dc66ae commit e227cad

3 files changed

Lines changed: 61 additions & 3 deletions

File tree

‎pkg/github/__toolsnaps__/get_me.snap‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"type": "string"
4040
},
4141
"created_at": {
42+
"description": "Account creation time in RFC3339 format.",
4243
"type": "string"
4344
},
4445
"email": {
@@ -78,6 +79,7 @@
7879
"type": "string"
7980
},
8081
"updated_at": {
82+
"description": "Last profile update time in RFC3339 format.",
8183
"type": "string"
8284
}
8385
},

‎pkg/github/context_tools.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ type UserDetails struct {
6060
PublicGists int `json:"public_gists"`
6161
Followers int `json:"followers"`
6262
Following int `json:"following"`
63-
CreatedAt time.Time `json:"created_at"`
64-
UpdatedAt time.Time `json:"updated_at"`
63+
CreatedAt time.Time `json:"created_at" jsonschema:"Account creation time in RFC3339 format."`
64+
UpdatedAt time.Time `json:"updated_at" jsonschema:"Last profile update time in RFC3339 format."`
6565
PrivateGists int `json:"private_gists,omitempty"`
6666
TotalPrivateRepos int64 `json:"total_private_repos,omitempty"`
6767
OwnedPrivateRepos int64 `json:"owned_private_repos,omitempty"`

‎pkg/github/context_tools_test.go‎

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func TestContextToolsTypedRegistration(t *testing.T) {
174174
mockUser := &github.User{
175175
Login: new("testuser"),
176176
HTMLURL: new("https://github.com/testuser"),
177-
CreatedAt: &github.Timestamp{Time: time.Now()},
177+
CreatedAt: &github.Timestamp{Time: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)},
178178
}
179179
teamsMatcher := githubv4mock.NewQueryMatcher(
180180
"query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}",
@@ -209,6 +209,29 @@ func TestContextToolsTypedRegistration(t *testing.T) {
209209
},
210210
}),
211211
)
212+
emptyOrganizationsMatcher := githubv4mock.NewQueryMatcher(
213+
"query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}",
214+
map[string]any{"login": "no-orgs"},
215+
githubv4mock.DataResponse(map[string]any{
216+
"user": map[string]any{"organizations": map[string]any{"nodes": []any{}}},
217+
}),
218+
)
219+
emptyTeamsMatcher := githubv4mock.NewQueryMatcher(
220+
"query($login:String!){user(login: $login){organizations(first: 100){nodes{login,teams(first: 100, userLogins: [$login]){nodes{name,slug,description}}}}}}",
221+
map[string]any{"login": "no-teams"},
222+
githubv4mock.DataResponse(map[string]any{
223+
"user": map[string]any{"organizations": map[string]any{"nodes": []any{
224+
map[string]any{"login": "testorg", "teams": map[string]any{"nodes": []any{}}},
225+
}}},
226+
}),
227+
)
228+
emptyMembersMatcher := githubv4mock.NewQueryMatcher(
229+
"query($org:String!$teamSlug:String!){organization(login: $org){team(slug: $teamSlug){members(first: 100){nodes{login}}}}}",
230+
map[string]any{"org": "testorg", "teamSlug": "emptyteam"},
231+
githubv4mock.DataResponse(map[string]any{
232+
"organization": map[string]any{"team": map[string]any{"members": map[string]any{"nodes": []any{}}}},
233+
}),
234+
)
212235
mockedGQLClient := githubv4.NewClient(githubv4mock.NewMockedHTTPClient(teamsMatcher, membersMatcher))
213236
failGetMe := false
214237
var graphQLCalls int
@@ -251,9 +274,16 @@ func TestContextToolsTypedRegistration(t *testing.T) {
251274
list, err := clientSession.ListTools(context.Background(), nil)
252275
require.NoError(t, err)
253276
require.Len(t, list.Tools, 3)
277+
outputSchemas := make(map[string]*jsonschema.Resolved)
254278
for _, tool := range list.Tools {
255279
assert.NotNil(t, tool.InputSchema)
256280
assert.NotNil(t, tool.OutputSchema)
281+
outputJSON, err := json.Marshal(tool.OutputSchema)
282+
require.NoError(t, err)
283+
var outputSchema jsonschema.Schema
284+
require.NoError(t, json.Unmarshal(outputJSON, &outputSchema))
285+
outputSchemas[tool.Name], err = outputSchema.Resolve(nil)
286+
require.NoError(t, err)
257287
schemaJSON, err := json.Marshal(tool.InputSchema)
258288
require.NoError(t, err)
259289
var schema struct {
@@ -296,6 +326,8 @@ func TestContextToolsTypedRegistration(t *testing.T) {
296326
legacyText, err := json.Marshal(returnedUser)
297327
require.NoError(t, err)
298328
assert.Equal(t, string(legacyText), getTextResult(t, result).Text)
329+
assert.Equal(t, `{"login":"testuser","profile_url":"https://github.com/testuser","details":{"public_repos":0,"public_gists":0,"followers":0,"following":0,"created_at":"2020-01-02T03:04:05Z","updated_at":"0001-01-01T00:00:00Z"}}`, getTextResult(t, result).Text)
330+
require.NoError(t, outputSchemas["get_me"].Validate(result.StructuredContent))
299331

300332
result, err = clientSession.CallTool(context.Background(), &mcp.CallToolParams{
301333
Name: "get_teams",
@@ -307,6 +339,7 @@ func TestContextToolsTypedRegistration(t *testing.T) {
307339
require.NoError(t, err)
308340
assert.JSONEq(t, `[{"org":"testorg","teams":[{"name":"team1","slug":"team1","description":"Team 1"}]}]`, string(structuredJSON))
309341
assert.Equal(t, `[{"org":"testorg","teams":[{"name":"team1","slug":"team1","description":"Team 1"}]}]`, getTextResult(t, result).Text)
342+
require.NoError(t, outputSchemas["get_teams"].Validate(result.StructuredContent))
310343
assert.Equal(t, 1, graphQLCalls)
311344

312345
result, err = clientSession.CallTool(context.Background(), &mcp.CallToolParams{
@@ -319,6 +352,7 @@ func TestContextToolsTypedRegistration(t *testing.T) {
319352
require.NoError(t, err)
320353
assert.JSONEq(t, `["user1","user2"]`, string(structuredJSON))
321354
assert.Equal(t, `["user1","user2"]`, getTextResult(t, result).Text)
355+
require.NoError(t, outputSchemas["get_team_members"].Validate(result.StructuredContent))
322356
assert.Equal(t, 2, graphQLCalls)
323357

324358
result, err = clientSession.CallTool(context.Background(), &mcp.CallToolParams{
@@ -337,6 +371,28 @@ func TestContextToolsTypedRegistration(t *testing.T) {
337371
assert.True(t, result.IsError, "empty required strings should remain invalid")
338372
assert.Equal(t, 2, graphQLCalls, "schema validation must reject empty required strings before the handler")
339373

374+
for _, tc := range []struct {
375+
name string
376+
args map[string]any
377+
text string
378+
matcher githubv4mock.Matcher
379+
}{
380+
{"get_teams", map[string]any{"user": "no-orgs"}, "null", emptyOrganizationsMatcher},
381+
{"get_teams", map[string]any{"user": "no-teams"}, `[{"org":"testorg","teams":[]}]`, emptyTeamsMatcher},
382+
{"get_team_members", map[string]any{"org": "testorg", "team_slug": "emptyteam"}, "null", emptyMembersMatcher},
383+
} {
384+
mockedGQLClient = githubv4.NewClient(githubv4mock.NewMockedHTTPClient(tc.matcher))
385+
result, err := clientSession.CallTool(context.Background(), &mcp.CallToolParams{Name: tc.name, Arguments: tc.args})
386+
require.NoError(t, err)
387+
require.False(t, result.IsError)
388+
require.Len(t, result.Content, 1, "SDK fallback must not duplicate the legacy text")
389+
assert.Equal(t, tc.text, getTextResult(t, result).Text)
390+
structuredJSON, err := json.Marshal(result.StructuredContent)
391+
require.NoError(t, err)
392+
assert.JSONEq(t, tc.text, string(structuredJSON))
393+
require.NoError(t, outputSchemas[tc.name].Validate(result.StructuredContent))
394+
}
395+
340396
failGetMe = true
341397
result, err = clientSession.CallTool(context.Background(), &mcp.CallToolParams{Name: "get_me"})
342398
require.NoError(t, err)

0 commit comments

Comments
 (0)