@@ -130,8 +130,11 @@ func Test_ListAutomationTags(t *testing.T) {
130130 session := connectCommentVisibilityClient (t , server , protocol )
131131 list , err := session .ListTools (context .Background (), nil )
132132 require .NoError (t , err )
133- require .Len (t , list .Tools , 1 )
134- assert .Equal (t , "list_automation_tags" , list .Tools [0 ].Name )
133+ byName := map [string ]* mcp.Tool {}
134+ for _ , tool := range list .Tools {
135+ byName [tool .Name ] = tool
136+ }
137+ require .Contains (t , byName , "list_automation_tags" )
135138 args := tc .args
136139 if args == nil {
137140 args = map [string ]any {"owner" : "owner" , "repo" : "repo" }
@@ -154,7 +157,151 @@ func Test_ListAutomationTags(t *testing.T) {
154157 require .False (t , result .IsError )
155158 assert .JSONEq (t , tc .want , getTextResult (t , result ).Text )
156159 if protocol == inventory .ProtocolVersionMultiRoundTrip {
157- require .NotNil (t , list .Tools [0 ].OutputSchema )
160+ require .NotNil (t , byName ["list_automation_tags" ].OutputSchema )
161+ encoded , err := json .Marshal (result .StructuredContent )
162+ require .NoError (t , err )
163+ assert .JSONEq (t , tc .want , string (encoded ))
164+ } else {
165+ assert .Nil (t , result .StructuredContent )
166+ }
167+ })
168+ }
169+ })
170+ }
171+ }
172+
173+ func Test_GetAutomationTag (t * testing.T ) {
174+ toolDef := GetAutomationTag (translations .NullTranslationHelper )
175+ require .NoError (t , toolsnaps .Test (toolDef .Tool .Name , toolDef .Tool ))
176+ assert .True (t , toolDef .Tool .Annotations .ReadOnlyHint )
177+
178+ t .Run ("discovery" , func (t * testing.T ) {
179+ for _ , tc := range []struct {
180+ name string
181+ toolsets []string
182+ scopes []string
183+ want bool
184+ }{
185+ {"default" , []string {"default" }, []string {"repo" }, false },
186+ {"opt in read only" , []string {"copilot_automations" }, []string {"repo" }, true },
187+ {"missing scope" , []string {"copilot_automations" }, []string {}, false },
188+ } {
189+ t .Run (tc .name , func (t * testing.T ) {
190+ inv , err := NewInventory (translations .NullTranslationHelper ).
191+ WithToolsets (tc .toolsets ).WithReadOnly (true ).
192+ WithFilter (CreateToolScopeFilter (tc .scopes )).Build ()
193+ require .NoError (t , err )
194+ require .Empty (t , inv .UnrecognizedToolsets ())
195+ var found bool
196+ for _ , tool := range inv .AvailableTools (context .Background ()) {
197+ found = found || tool .Tool .Name == "get_automation_tag"
198+ }
199+ assert .Equal (t , tc .want , found )
200+ })
201+ }
202+ })
203+
204+ for _ , protocol := range []string {inventory .ProtocolVersionMultiRoundTrip , "2025-11-25" } {
205+ t .Run (protocol , func (t * testing.T ) {
206+ for _ , tc := range []struct {
207+ name string
208+ args map [string ]any
209+ status int
210+ body string
211+ escapedPath string
212+ want string
213+ wantError string
214+ }{
215+ {
216+ name : "internal and additive fields" ,
217+ status : http .StatusOK ,
218+ body : `{"id":"tag-1","name":"Release","description":"","version":"opaque-version","created_at":"2026-10-01T00:00:00Z","updated_at":"2026-10-02T00:00:00Z","deleted_at":"2026-10-03T00:00:00Z","can_delete":true,"deletion_pending":false,"has_known_unavailable_members":false,"future_field":true}` ,
219+ want : `{"id":"tag-1","name":"Release","description":"","version":"opaque-version","created_at":"2026-10-01T00:00:00Z","updated_at":"2026-10-02T00:00:00Z"}` ,
220+ },
221+ {
222+ name : "escaped path segments" ,
223+ args : map [string ]any {"owner" : "own er" , "repo" : "re/po" , "tag_id" : "a/b c" },
224+ escapedPath : "/agents/repos/own%20er/re%2Fpo/automation-tags/a%2Fb%20c" ,
225+ status : http .StatusOK ,
226+ body : `{"id":"a/b c","name":"Release","description":"","version":"v","created_at":"2026-10-01T00:00:00Z","updated_at":"2026-10-02T00:00:00Z"}` ,
227+ want : `{"id":"a/b c","name":"Release","description":"","version":"v","created_at":"2026-10-01T00:00:00Z","updated_at":"2026-10-02T00:00:00Z"}` ,
228+ },
229+ {name : "non-disclosure" , status : http .StatusNotFound , body : `{"message":"not found","documentation_url":"https://docs.github.com/rest"}` , wantError : "404" },
230+ {name : "empty denial" , status : http .StatusNotFound , wantError : "404" },
231+ {name : "bad request" , status : http .StatusBadRequest , body : `{"message":"Bad Request"}` , wantError : "400" },
232+ {name : "permission denied" , status : http .StatusForbidden , body : `{"message":"Forbidden"}` , wantError : "403" },
233+ {name : "unauthenticated" , status : http .StatusUnauthorized , body : `{"message":"Requires authentication"}` , wantError : "401" },
234+ {name : "conflict" , status : http .StatusConflict , body : `{"message":"Conflict"}` , wantError : "409" },
235+ {name : "temporarily unavailable" , status : http .StatusServiceUnavailable , body : `{"message":"automation tagging is unavailable","documentation_url":"https://docs.github.com/rest"}` , wantError : "503" },
236+ {name : "server error" , status : http .StatusInternalServerError , body : `{"message":"Internal Server Error"}` , wantError : "500" },
237+ {name : "validation" , status : http .StatusUnprocessableEntity , body : `{"message":"Validation Failed"}` , wantError : "Validation Failed" },
238+ {name : "unexpected success status" , status : http .StatusNoContent , wantError : "204" },
239+ {name : "missing tag id" , args : map [string ]any {"owner" : "owner" , "repo" : "repo" }, wantError : "tag_id" },
240+ {name : "empty tag id" , args : map [string ]any {"owner" : "owner" , "repo" : "repo" , "tag_id" : "" }, wantError : "tag_id" },
241+ {name : "dot tag id" , args : map [string ]any {"owner" : "owner" , "repo" : "repo" , "tag_id" : "." }, wantError : `tag_id must not be "."` },
242+ {name : "dot owner" , args : map [string ]any {"owner" : "." , "repo" : "repo" , "tag_id" : "tag-1" }, wantError : `owner must not be "."` },
243+ {name : "dot repo" , args : map [string ]any {"owner" : "owner" , "repo" : "." , "tag_id" : "tag-1" }, wantError : `repo must not be "."` },
244+ } {
245+ t .Run (tc .name , func (t * testing.T ) {
246+ calls := 0
247+ client := mustNewGHClient (t , MockHTTPClientWithHandler (func (w http.ResponseWriter , r * http.Request ) {
248+ calls ++
249+ if tc .status == 0 {
250+ _ , err := fmt .Fprint (w , `{}` )
251+ require .NoError (t , err )
252+ return
253+ }
254+ escapedPath := tc .escapedPath
255+ if escapedPath == "" {
256+ escapedPath = "/agents/repos/owner/repo/automation-tags/tag-1"
257+ }
258+ assert .Equal (t , "api.github.com" , r .URL .Host )
259+ assert .Equal (t , http .MethodGet , r .Method )
260+ assert .Equal (t , escapedPath , r .URL .EscapedPath ())
261+ assert .Empty (t , r .URL .RawQuery )
262+ w .Header ().Set ("Content-Type" , "application/json" )
263+ w .WriteHeader (tc .status )
264+ _ , err := fmt .Fprint (w , tc .body )
265+ require .NoError (t , err )
266+ }))
267+ deps := BaseDeps {Client : client }
268+ inv , err := NewInventory (translations .NullTranslationHelper ).
269+ WithToolsets ([]string {"copilot_automations" }).WithReadOnly (true ).Build ()
270+ require .NoError (t , err )
271+ server := mcp .NewServer (& mcp.Implementation {Name : "automation-tags" , Version : "v1" }, nil )
272+ server .AddReceivingMiddleware (InjectDepsMiddleware (deps ))
273+ inv .RegisterTools (context .Background (), server , deps )
274+ session := connectCommentVisibilityClient (t , server , protocol )
275+ list , err := session .ListTools (context .Background (), nil )
276+ require .NoError (t , err )
277+ byName := map [string ]* mcp.Tool {}
278+ for _ , tool := range list .Tools {
279+ byName [tool .Name ] = tool
280+ }
281+ require .Contains (t , byName , "get_automation_tag" )
282+ args := tc .args
283+ if args == nil {
284+ args = map [string ]any {"owner" : "owner" , "repo" : "repo" , "tag_id" : "tag-1" }
285+ }
286+ result , err := session .CallTool (context .Background (), & mcp.CallToolParams {
287+ Name : "get_automation_tag" , Arguments : args ,
288+ })
289+ require .NoError (t , err )
290+ if tc .status == 0 {
291+ assert .Zero (t , calls )
292+ } else {
293+ assert .Equal (t , 1 , calls )
294+ }
295+ if tc .wantError != "" {
296+ require .True (t , result .IsError )
297+ assert .Contains (t , getErrorResult (t , result ).Text , tc .wantError )
298+ assert .Nil (t , result .StructuredContent )
299+ return
300+ }
301+ require .False (t , result .IsError )
302+ assert .JSONEq (t , tc .want , getTextResult (t , result ).Text )
303+ if protocol == inventory .ProtocolVersionMultiRoundTrip {
304+ require .NotNil (t , byName ["get_automation_tag" ].OutputSchema )
158305 encoded , err := json .Marshal (result .StructuredContent )
159306 require .NoError (t , err )
160307 assert .JSONEq (t , tc .want , string (encoded ))
0 commit comments