@shipfox/api-integration-github 12.3.0 → 12.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +15 -9
- package/dist/api/client.js.map +1 -1
- package/dist/api/github-octokit.d.ts +7 -0
- package/dist/api/github-octokit.d.ts.map +1 -0
- package/dist/api/github-octokit.js +32 -0
- package/dist/api/github-octokit.js.map +1 -0
- package/dist/api/installation-token-envelope.d.ts +5 -1
- package/dist/api/installation-token-envelope.d.ts.map +1 -1
- package/dist/api/installation-token-envelope.js +16 -5
- package/dist/api/installation-token-envelope.js.map +1 -1
- package/dist/api/installation-token-provider.d.ts.map +1 -1
- package/dist/api/installation-token-provider.js +4 -2
- package/dist/api/installation-token-provider.js.map +1 -1
- package/dist/api/shared-installation-token-cache.d.ts.map +1 -1
- package/dist/api/shared-installation-token-cache.js +10 -4
- package/dist/api/shared-installation-token-cache.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-tools.d.ts +11 -4
- package/dist/core/agent-tools.d.ts.map +1 -1
- package/dist/core/agent-tools.js +235 -24
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/github-agent-tool-catalog.js +1 -1
- package/dist/core/github-agent-tool-catalog.js.map +1 -1
- package/dist/metrics/instance.d.ts +1 -0
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +19 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/api/client.test.ts +66 -10
- package/src/api/client.ts +44 -7
- package/src/api/github-octokit.test.ts +115 -0
- package/src/api/github-octokit.ts +49 -0
- package/src/api/installation-token-envelope.ts +24 -2
- package/src/api/installation-token-provider.test.ts +44 -5
- package/src/api/installation-token-provider.ts +5 -2
- package/src/api/shared-installation-token-cache.test.ts +28 -0
- package/src/api/shared-installation-token-cache.ts +7 -0
- package/src/config.ts +5 -0
- package/src/core/agent-tools.test.ts +1049 -8
- package/src/core/agent-tools.ts +388 -25
- package/src/core/github-agent-tool-catalog.ts +1 -1
- package/src/metrics/instance.ts +25 -0
- package/test/env.ts +1 -0
- package/test/fixtures/github-installation-token.ts +8 -0
- package/test/index.ts +4 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,13 +1,19 @@
|
|
|
1
|
+
import {RequestError} from 'octokit';
|
|
1
2
|
import type {GithubApiClient} from '#api/client.js';
|
|
2
3
|
import {DEFAULT_JOB_LOG_TAIL_LINES} from '#core/actions-logs.js';
|
|
3
4
|
import {
|
|
4
5
|
type GithubAgentToolId,
|
|
5
6
|
GithubAgentToolsProvider,
|
|
7
|
+
type GithubToolClient,
|
|
6
8
|
githubAgentToolCatalog,
|
|
7
9
|
githubAgentToolSelectionCatalog,
|
|
10
|
+
githubOperationRoute,
|
|
11
|
+
projectGithubOperationParameters,
|
|
8
12
|
} from '#core/agent-tools.js';
|
|
9
13
|
import {createGithubIntegrationProvider} from '#index.js';
|
|
10
14
|
|
|
15
|
+
const githubAppReviewUser = {login: 'shipfox-test[bot]'};
|
|
16
|
+
|
|
11
17
|
const expectedCatalogRows = [
|
|
12
18
|
{
|
|
13
19
|
id: 'issue_read',
|
|
@@ -200,6 +206,331 @@ const expectedCatalogRows = [
|
|
|
200
206
|
},
|
|
201
207
|
];
|
|
202
208
|
|
|
209
|
+
type GithubOperationRouteCase = {
|
|
210
|
+
toolId: GithubAgentToolId;
|
|
211
|
+
method?: string;
|
|
212
|
+
args: Record<string, unknown>;
|
|
213
|
+
expectedRoute: string;
|
|
214
|
+
runtimeInjectedProperties?: readonly string[];
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const githubOperationRouteCases = [
|
|
218
|
+
{
|
|
219
|
+
toolId: 'issue_read',
|
|
220
|
+
method: 'get',
|
|
221
|
+
args: {issue_number: 1},
|
|
222
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
toolId: 'issue_read',
|
|
226
|
+
method: 'get_comments',
|
|
227
|
+
args: {issue_number: 1},
|
|
228
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}/comments',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
toolId: 'issue_read',
|
|
232
|
+
method: 'get_sub_issues',
|
|
233
|
+
args: {issue_number: 1},
|
|
234
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}/sub_issues',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
toolId: 'issue_read',
|
|
238
|
+
method: 'get_parent',
|
|
239
|
+
args: {issue_number: 1},
|
|
240
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}/parent',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
toolId: 'issue_read',
|
|
244
|
+
method: 'get_labels',
|
|
245
|
+
args: {issue_number: 1},
|
|
246
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{issue_number}/labels',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
toolId: 'list_issue_types',
|
|
250
|
+
args: {owner: 'shipfox'},
|
|
251
|
+
expectedRoute: 'GET /orgs/{owner}/issue-types',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
toolId: 'list_issue_types',
|
|
255
|
+
args: {owner: 'shipfox', repo: 'platform'},
|
|
256
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issue-types',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
toolId: 'list_issues',
|
|
260
|
+
args: {},
|
|
261
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
toolId: 'search_issues',
|
|
265
|
+
args: {},
|
|
266
|
+
expectedRoute: 'GET /search/issues',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
toolId: 'add_issue_comment',
|
|
270
|
+
args: {issue_number: 1, body: 'Comment'},
|
|
271
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
toolId: 'add_issue_comment',
|
|
275
|
+
args: {issue_number: 1, reaction: '+1'},
|
|
276
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues/{issue_number}/reactions',
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
toolId: 'add_issue_comment',
|
|
280
|
+
args: {issue_number: 1, reaction: '+1', body: 'Comment'},
|
|
281
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues/{issue_number}/comments',
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
toolId: 'add_issue_comment',
|
|
285
|
+
args: {comment_id: 1, reaction: '+1'},
|
|
286
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues/comments/{comment_id}/reactions',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
toolId: 'issue_write',
|
|
290
|
+
method: 'create',
|
|
291
|
+
args: {},
|
|
292
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues',
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
toolId: 'issue_write',
|
|
296
|
+
method: 'update',
|
|
297
|
+
args: {issue_number: 1},
|
|
298
|
+
expectedRoute: 'PATCH /repos/{owner}/{repo}/issues/{issue_number}',
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
toolId: 'sub_issue_write',
|
|
302
|
+
method: 'add',
|
|
303
|
+
args: {issue_number: 1},
|
|
304
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/issues/{issue_number}/sub_issues',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
toolId: 'sub_issue_write',
|
|
308
|
+
method: 'remove',
|
|
309
|
+
args: {issue_number: 1, sub_issue_id: 2},
|
|
310
|
+
expectedRoute: 'DELETE /repos/{owner}/{repo}/issues/{issue_number}/sub_issues/{sub_issue_id}',
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
toolId: 'sub_issue_write',
|
|
314
|
+
method: 'reprioritize',
|
|
315
|
+
args: {issue_number: 1, sub_issue_id: 2, after_id: 3},
|
|
316
|
+
expectedRoute: 'PATCH /repos/{owner}/{repo}/issues/{issue_number}/sub_issues/priority',
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
toolId: 'pull_request_read',
|
|
320
|
+
method: 'get',
|
|
321
|
+
args: {pull_number: 1},
|
|
322
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}',
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
toolId: 'pull_request_read',
|
|
326
|
+
method: 'get_diff',
|
|
327
|
+
args: {pull_number: 1},
|
|
328
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}',
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
toolId: 'pull_request_read',
|
|
332
|
+
method: 'get_status',
|
|
333
|
+
args: {pull_number: 1, ref: 'main'},
|
|
334
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/commits/{ref}/status',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
toolId: 'pull_request_read',
|
|
338
|
+
method: 'get_files',
|
|
339
|
+
args: {pull_number: 1},
|
|
340
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/files',
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
toolId: 'pull_request_read',
|
|
344
|
+
method: 'get_commits',
|
|
345
|
+
args: {pull_number: 1},
|
|
346
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/commits',
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
toolId: 'pull_request_read',
|
|
350
|
+
method: 'get_review_comments',
|
|
351
|
+
args: {pull_number: 1},
|
|
352
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/comments',
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
toolId: 'pull_request_read',
|
|
356
|
+
method: 'get_reviews',
|
|
357
|
+
args: {pull_number: 1},
|
|
358
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
toolId: 'pull_request_read',
|
|
362
|
+
method: 'get_comments',
|
|
363
|
+
args: {pull_number: 1},
|
|
364
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/issues/{pull_number}/comments',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
toolId: 'pull_request_read',
|
|
368
|
+
method: 'get_check_runs',
|
|
369
|
+
args: {pull_number: 1, ref: 'main'},
|
|
370
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/commits/{ref}/check-runs',
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
toolId: 'list_pull_requests',
|
|
374
|
+
args: {},
|
|
375
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/pulls',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
toolId: 'search_pull_requests',
|
|
379
|
+
args: {},
|
|
380
|
+
expectedRoute: 'GET /search/issues',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
toolId: 'create_pull_request',
|
|
384
|
+
args: {},
|
|
385
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/pulls',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
toolId: 'update_pull_request',
|
|
389
|
+
args: {pull_number: 1},
|
|
390
|
+
expectedRoute: 'PATCH /repos/{owner}/{repo}/pulls/{pull_number}',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
toolId: 'add_reply_to_pull_request_comment',
|
|
394
|
+
args: {pull_number: 1, comment_id: 2, body: 'Reply'},
|
|
395
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/pulls/{pull_number}/comments/{comment_id}/replies',
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
toolId: 'add_reply_to_pull_request_comment',
|
|
399
|
+
args: {comment_id: 2, reaction: '+1'},
|
|
400
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/pulls/comments/{comment_id}/reactions',
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
toolId: 'merge_pull_request',
|
|
404
|
+
args: {pull_number: 1},
|
|
405
|
+
expectedRoute: 'PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge',
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
toolId: 'update_pull_request_branch',
|
|
409
|
+
args: {pull_number: 1},
|
|
410
|
+
expectedRoute: 'PUT /repos/{owner}/{repo}/pulls/{pull_number}/update-branch',
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
toolId: 'pull_request_review_write',
|
|
414
|
+
method: 'create',
|
|
415
|
+
args: {pull_number: 1},
|
|
416
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
toolId: 'pull_request_review_write',
|
|
420
|
+
method: 'submit_pending',
|
|
421
|
+
args: {pull_number: 1},
|
|
422
|
+
runtimeInjectedProperties: ['review_id'],
|
|
423
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events',
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
toolId: 'pull_request_review_write',
|
|
427
|
+
method: 'delete_pending',
|
|
428
|
+
args: {pull_number: 1},
|
|
429
|
+
runtimeInjectedProperties: ['review_id'],
|
|
430
|
+
expectedRoute: 'DELETE /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}',
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
toolId: 'add_comment_to_pending_review',
|
|
434
|
+
args: {pull_number: 1, path: 'src/index.ts', body: 'Comment'},
|
|
435
|
+
expectedRoute: 'POST /graphql',
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
toolId: 'actions_list',
|
|
439
|
+
method: 'list_workflows',
|
|
440
|
+
args: {},
|
|
441
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/workflows',
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
toolId: 'actions_list',
|
|
445
|
+
method: 'list_workflow_runs',
|
|
446
|
+
args: {resource_id: 'workflow'},
|
|
447
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/workflows/{resource_id}/runs',
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
toolId: 'actions_list',
|
|
451
|
+
method: 'list_workflow_jobs',
|
|
452
|
+
args: {resource_id: 'run'},
|
|
453
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/runs/{resource_id}/jobs',
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
toolId: 'actions_list',
|
|
457
|
+
method: 'list_workflow_run_artifacts',
|
|
458
|
+
args: {resource_id: 'run'},
|
|
459
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/runs/{resource_id}/artifacts',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
toolId: 'actions_get',
|
|
463
|
+
method: 'get_workflow',
|
|
464
|
+
args: {resource_id: 'workflow'},
|
|
465
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/workflows/{resource_id}',
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
toolId: 'actions_get',
|
|
469
|
+
method: 'get_workflow_run',
|
|
470
|
+
args: {resource_id: 'run'},
|
|
471
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/runs/{resource_id}',
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
toolId: 'actions_get',
|
|
475
|
+
method: 'get_workflow_job',
|
|
476
|
+
args: {resource_id: 'job'},
|
|
477
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/jobs/{resource_id}',
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
toolId: 'actions_get',
|
|
481
|
+
method: 'download_workflow_run_artifact',
|
|
482
|
+
args: {resource_id: 'artifact'},
|
|
483
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/artifacts/{resource_id}/zip',
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
toolId: 'actions_get',
|
|
487
|
+
method: 'get_workflow_run_usage',
|
|
488
|
+
args: {resource_id: 'run'},
|
|
489
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/runs/{resource_id}/timing',
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
toolId: 'actions_get',
|
|
493
|
+
method: 'get_workflow_run_logs_url',
|
|
494
|
+
args: {resource_id: 'run'},
|
|
495
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/runs/{resource_id}/logs',
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
toolId: 'actions_run_trigger',
|
|
499
|
+
method: 'run_workflow',
|
|
500
|
+
args: {workflow_id: 'ci.yml', ref: 'main'},
|
|
501
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/actions/workflows/{workflow_id}/dispatches',
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
toolId: 'actions_run_trigger',
|
|
505
|
+
method: 'rerun_workflow_run',
|
|
506
|
+
args: {run_id: 1},
|
|
507
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
toolId: 'actions_run_trigger',
|
|
511
|
+
method: 'rerun_failed_jobs',
|
|
512
|
+
args: {run_id: 1},
|
|
513
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/actions/runs/{run_id}/rerun-failed-jobs',
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
toolId: 'actions_run_trigger',
|
|
517
|
+
method: 'cancel_workflow_run',
|
|
518
|
+
args: {run_id: 1},
|
|
519
|
+
expectedRoute: 'POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel',
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
toolId: 'actions_run_trigger',
|
|
523
|
+
method: 'delete_workflow_run_logs',
|
|
524
|
+
args: {run_id: 1},
|
|
525
|
+
expectedRoute: 'DELETE /repos/{owner}/{repo}/actions/runs/{run_id}/logs',
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
toolId: 'get_job_logs',
|
|
529
|
+
args: {job_id: 1},
|
|
530
|
+
expectedRoute: 'GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs',
|
|
531
|
+
},
|
|
532
|
+
] satisfies readonly GithubOperationRouteCase[];
|
|
533
|
+
|
|
203
534
|
describe('github agent tool catalog', () => {
|
|
204
535
|
it('matches the GitHub MCP-style tool rows', () => {
|
|
205
536
|
const rows = githubAgentToolCatalog.map(
|
|
@@ -216,6 +547,57 @@ describe('github agent tool catalog', () => {
|
|
|
216
547
|
expect(rows).toEqual(expectedCatalogRows);
|
|
217
548
|
});
|
|
218
549
|
|
|
550
|
+
it('covers every catalog operation with an exact route case', () => {
|
|
551
|
+
const catalogOperationKeys = githubAgentToolCatalog.flatMap(
|
|
552
|
+
(entry) =>
|
|
553
|
+
entry.methods?.map((method) => operationKey(entry.id, method.id)) ?? [
|
|
554
|
+
operationKey(entry.id),
|
|
555
|
+
],
|
|
556
|
+
);
|
|
557
|
+
const routeCaseOperationKeys = githubOperationRouteCases.map(({toolId, method}) =>
|
|
558
|
+
operationKey(toolId, method),
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
expect([...new Set(routeCaseOperationKeys)].sort()).toEqual(
|
|
562
|
+
[...new Set(catalogOperationKeys)].sort(),
|
|
563
|
+
);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it.each(
|
|
567
|
+
githubOperationRouteCases,
|
|
568
|
+
)('asserts the $toolId.$method route and its input placeholders', ({
|
|
569
|
+
toolId,
|
|
570
|
+
method,
|
|
571
|
+
args,
|
|
572
|
+
expectedRoute,
|
|
573
|
+
runtimeInjectedProperties = [],
|
|
574
|
+
}) => {
|
|
575
|
+
const route = githubOperationRoute(toolId, method, args);
|
|
576
|
+
|
|
577
|
+
expect(route).toBe(expectedRoute);
|
|
578
|
+
if (route === undefined) return;
|
|
579
|
+
|
|
580
|
+
const inputProperties = new Set(Object.keys(inputSchemaFor(toolId).properties ?? {}));
|
|
581
|
+
const undeclaredArguments = Object.keys(args).filter((name) => !inputProperties.has(name));
|
|
582
|
+
expect(undeclaredArguments).toEqual([]);
|
|
583
|
+
|
|
584
|
+
const projectedParameters = projectGithubOperationParameters(toolId, method, args);
|
|
585
|
+
const injectedProperties = new Set([
|
|
586
|
+
...runtimeInjectedProperties,
|
|
587
|
+
...Object.keys(projectedParameters).filter(
|
|
588
|
+
(name) => !inputProperties.has(name) && !Object.hasOwn(args, name),
|
|
589
|
+
),
|
|
590
|
+
]);
|
|
591
|
+
const placeholders = Array.from(route.matchAll(/\{([^{}]+)\}/g), (match) => match[1]).filter(
|
|
592
|
+
(name): name is string => name !== undefined,
|
|
593
|
+
);
|
|
594
|
+
const undeclaredPlaceholders = placeholders.filter(
|
|
595
|
+
(name) => !inputProperties.has(name) && !injectedProperties.has(name),
|
|
596
|
+
);
|
|
597
|
+
|
|
598
|
+
expect(undeclaredPlaceholders).toEqual([]);
|
|
599
|
+
});
|
|
600
|
+
|
|
219
601
|
it('defines descriptions and schemas for every tool and method', () => {
|
|
220
602
|
const entriesMissingCatalogData = githubAgentToolCatalog.filter(
|
|
221
603
|
(entry) =>
|
|
@@ -369,6 +751,406 @@ describe('github agent tool catalog', () => {
|
|
|
369
751
|
});
|
|
370
752
|
});
|
|
371
753
|
|
|
754
|
+
it('returns artifact download metadata without buffering archive bytes', async () => {
|
|
755
|
+
const request = vi.fn(() =>
|
|
756
|
+
Promise.resolve({
|
|
757
|
+
status: 302,
|
|
758
|
+
headers: {
|
|
759
|
+
location: 'https://objects.example/artifact.zip?token=temporary',
|
|
760
|
+
'content-type': 'application/zip',
|
|
761
|
+
'content-length': '1234',
|
|
762
|
+
},
|
|
763
|
+
data: new ArrayBuffer(1024),
|
|
764
|
+
}),
|
|
765
|
+
);
|
|
766
|
+
const result = await callGithubToolWithRequest(
|
|
767
|
+
'actions_get',
|
|
768
|
+
{
|
|
769
|
+
method: 'download_workflow_run_artifact',
|
|
770
|
+
owner: 'shipfox',
|
|
771
|
+
repo: 'platform',
|
|
772
|
+
resource_id: '42',
|
|
773
|
+
},
|
|
774
|
+
request,
|
|
775
|
+
);
|
|
776
|
+
const expected = {
|
|
777
|
+
archive_format: 'zip',
|
|
778
|
+
download_url: 'https://objects.example/artifact.zip?token=temporary',
|
|
779
|
+
artifact_id: '42',
|
|
780
|
+
content_type: 'application/zip',
|
|
781
|
+
size_bytes: 1234,
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
expect(request).toHaveBeenCalledWith(
|
|
785
|
+
'GET /repos/{owner}/{repo}/actions/artifacts/{resource_id}/zip',
|
|
786
|
+
{owner: 'shipfox', repo: 'platform', resource_id: '42'},
|
|
787
|
+
);
|
|
788
|
+
expect(result).toEqual({
|
|
789
|
+
content: [{type: 'text', text: JSON.stringify(expected)}],
|
|
790
|
+
structuredContent: expected,
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('fails artifact downloads without a redirect URL instead of returning an empty success', async () => {
|
|
795
|
+
const result = await callGithubToolWithRequest(
|
|
796
|
+
'actions_get',
|
|
797
|
+
{
|
|
798
|
+
method: 'download_workflow_run_artifact',
|
|
799
|
+
owner: 'shipfox',
|
|
800
|
+
repo: 'platform',
|
|
801
|
+
resource_id: '42',
|
|
802
|
+
},
|
|
803
|
+
vi.fn(() =>
|
|
804
|
+
Promise.resolve({
|
|
805
|
+
status: 302,
|
|
806
|
+
headers: {},
|
|
807
|
+
data: new ArrayBuffer(0),
|
|
808
|
+
}),
|
|
809
|
+
),
|
|
810
|
+
);
|
|
811
|
+
|
|
812
|
+
expect(result).toEqual({
|
|
813
|
+
isError: true,
|
|
814
|
+
content: [{type: 'text', text: 'GitHub artifact download did not return a download URL'}],
|
|
815
|
+
structuredContent: {code: 'malformed-provider-response'},
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
it('projects get_diff request headers through the provider session', async () => {
|
|
820
|
+
const request = vi.fn(() => Promise.resolve({data: 'diff --git a/file b/file'}));
|
|
821
|
+
const result = await callGithubToolWithRequest(
|
|
822
|
+
'pull_request_read',
|
|
823
|
+
{
|
|
824
|
+
method: 'get_diff',
|
|
825
|
+
owner: 'shipfox',
|
|
826
|
+
repo: 'platform',
|
|
827
|
+
pull_number: 2,
|
|
828
|
+
},
|
|
829
|
+
request,
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
expect(request).toHaveBeenCalledWith('GET /repos/{owner}/{repo}/pulls/{pull_number}', {
|
|
833
|
+
owner: 'shipfox',
|
|
834
|
+
repo: 'platform',
|
|
835
|
+
pull_number: 2,
|
|
836
|
+
headers: {accept: 'application/vnd.github.diff'},
|
|
837
|
+
});
|
|
838
|
+
expect(result).toEqual({
|
|
839
|
+
content: [{type: 'text', text: '{"result":"diff --git a/file b/file"}'}],
|
|
840
|
+
structuredContent: {result: 'diff --git a/file b/file'},
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('projects issue comment reactions through the provider session', async () => {
|
|
845
|
+
const request = vi.fn(() => Promise.resolve({data: {id: 7}}));
|
|
846
|
+
const result = await callGithubToolWithRequest(
|
|
847
|
+
'add_issue_comment',
|
|
848
|
+
{owner: 'shipfox', repo: 'platform', issue_number: 1, reaction: '+1'},
|
|
849
|
+
request,
|
|
850
|
+
);
|
|
851
|
+
|
|
852
|
+
expect(request).toHaveBeenCalledWith(
|
|
853
|
+
'POST /repos/{owner}/{repo}/issues/{issue_number}/reactions',
|
|
854
|
+
{owner: 'shipfox', repo: 'platform', issue_number: 1, content: '+1'},
|
|
855
|
+
);
|
|
856
|
+
expect(result).toEqual({
|
|
857
|
+
content: [{type: 'text', text: '{"id":7}'}],
|
|
858
|
+
structuredContent: {id: 7},
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
it('adds a comment to the latest caller pending review through GraphQL', async () => {
|
|
863
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
864
|
+
data: [
|
|
865
|
+
{id: 40, node_id: 'review-older', state: 'PENDING', user: githubAppReviewUser},
|
|
866
|
+
{id: 41, node_id: 'review-latest', state: 'PENDING', user: githubAppReviewUser},
|
|
867
|
+
{
|
|
868
|
+
id: 42,
|
|
869
|
+
node_id: 'another-review',
|
|
870
|
+
state: 'PENDING',
|
|
871
|
+
user: {login: 'another-user'},
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
});
|
|
875
|
+
const graphql = vi.fn().mockResolvedValueOnce({
|
|
876
|
+
addPullRequestReviewThread: {thread: {id: 'thread-1'}},
|
|
877
|
+
});
|
|
878
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
879
|
+
const session = await provider.openSession({
|
|
880
|
+
connection: connection(),
|
|
881
|
+
tools: [pendingReviewTool()],
|
|
882
|
+
scope: undefined,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const result = await session.call({
|
|
886
|
+
toolId: 'add_comment_to_pending_review',
|
|
887
|
+
arguments: {
|
|
888
|
+
owner: 'shipfox',
|
|
889
|
+
repo: 'platform',
|
|
890
|
+
pull_number: 2,
|
|
891
|
+
path: 'src/agent-tools.ts',
|
|
892
|
+
body: 'Please handle this error.',
|
|
893
|
+
subject_type: 'LINE',
|
|
894
|
+
line: 42,
|
|
895
|
+
side: 'RIGHT',
|
|
896
|
+
start_line: 40,
|
|
897
|
+
start_side: 'RIGHT',
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
expect(request).toHaveBeenCalledWith(
|
|
902
|
+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
903
|
+
expect.objectContaining({
|
|
904
|
+
owner: 'shipfox',
|
|
905
|
+
repo: 'platform',
|
|
906
|
+
pull_number: 2,
|
|
907
|
+
per_page: 100,
|
|
908
|
+
page: 1,
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
expect(request.mock.calls[0]?.[1]).toHaveProperty('request.signal');
|
|
912
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining('addPullRequestReviewThread'), {
|
|
913
|
+
input: {
|
|
914
|
+
pullRequestReviewId: 'review-latest',
|
|
915
|
+
path: 'src/agent-tools.ts',
|
|
916
|
+
body: 'Please handle this error.',
|
|
917
|
+
subjectType: 'LINE',
|
|
918
|
+
line: 42,
|
|
919
|
+
side: 'RIGHT',
|
|
920
|
+
startLine: 40,
|
|
921
|
+
startSide: 'RIGHT',
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
expect(result).toEqual({
|
|
925
|
+
content: [
|
|
926
|
+
{
|
|
927
|
+
type: 'text',
|
|
928
|
+
text: '{"addPullRequestReviewThread":{"thread":{"id":"thread-1"}}}',
|
|
929
|
+
},
|
|
930
|
+
],
|
|
931
|
+
structuredContent: {addPullRequestReviewThread: {thread: {id: 'thread-1'}}},
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
it('returns an explicit error when there is no pending review for the caller', async () => {
|
|
936
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
937
|
+
data: [
|
|
938
|
+
{
|
|
939
|
+
id: 41,
|
|
940
|
+
node_id: 'another-review',
|
|
941
|
+
state: 'PENDING',
|
|
942
|
+
user: {login: 'another-user'},
|
|
943
|
+
},
|
|
944
|
+
],
|
|
945
|
+
});
|
|
946
|
+
const graphql = vi.fn();
|
|
947
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
948
|
+
const session = await provider.openSession({
|
|
949
|
+
connection: connection(),
|
|
950
|
+
tools: [pendingReviewTool()],
|
|
951
|
+
scope: undefined,
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const result = await session.call({
|
|
955
|
+
toolId: 'add_comment_to_pending_review',
|
|
956
|
+
arguments: {
|
|
957
|
+
owner: 'shipfox',
|
|
958
|
+
repo: 'platform',
|
|
959
|
+
pull_number: 2,
|
|
960
|
+
path: 'src/agent-tools.ts',
|
|
961
|
+
body: 'Please handle this error.',
|
|
962
|
+
},
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
expect(request).toHaveBeenCalledOnce();
|
|
966
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
967
|
+
expect(result).toEqual({
|
|
968
|
+
isError: true,
|
|
969
|
+
content: [
|
|
970
|
+
{
|
|
971
|
+
type: 'text',
|
|
972
|
+
text: 'No pending pull request review found for the authenticated GitHub user.',
|
|
973
|
+
},
|
|
974
|
+
],
|
|
975
|
+
structuredContent: {code: 'provider-rejected'},
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
it('returns an access-denied code when the installation lacks a required permission', async () => {
|
|
980
|
+
const provider = new GithubAgentToolsProvider({
|
|
981
|
+
getInstallationByConnectionId: vi.fn(() => Promise.resolve(installation())),
|
|
982
|
+
tokenProvider: {
|
|
983
|
+
getInstallationAccessToken: vi.fn(() =>
|
|
984
|
+
Promise.resolve({
|
|
985
|
+
token: 'installation-token',
|
|
986
|
+
expiresAt: new Date(),
|
|
987
|
+
permissions: {},
|
|
988
|
+
}),
|
|
989
|
+
),
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
const tool = githubAgentToolCatalog.find((entry) => entry.id === 'list_issues');
|
|
993
|
+
if (!tool) throw new Error('Missing list_issues tool');
|
|
994
|
+
const session = await provider.openSession({
|
|
995
|
+
connection: connection(),
|
|
996
|
+
tools: [tool],
|
|
997
|
+
scope: undefined,
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
const result = await session.call({
|
|
1001
|
+
toolId: 'list_issues',
|
|
1002
|
+
arguments: {owner: 'shipfox', repo: 'platform'},
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
expect(result).toEqual({
|
|
1006
|
+
isError: true,
|
|
1007
|
+
content: [
|
|
1008
|
+
{
|
|
1009
|
+
type: 'text',
|
|
1010
|
+
text: 'GitHub installation token is missing permission for this operation',
|
|
1011
|
+
},
|
|
1012
|
+
],
|
|
1013
|
+
structuredContent: {code: 'access-denied'},
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
it('rejects a pending review without a GraphQL node ID', async () => {
|
|
1018
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1019
|
+
data: [{id: 41, state: 'PENDING', user: githubAppReviewUser}],
|
|
1020
|
+
});
|
|
1021
|
+
const graphql = vi.fn();
|
|
1022
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1023
|
+
const session = await provider.openSession({
|
|
1024
|
+
connection: connection(),
|
|
1025
|
+
tools: [pendingReviewTool()],
|
|
1026
|
+
scope: undefined,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
await expect(
|
|
1030
|
+
session.call({
|
|
1031
|
+
toolId: 'add_comment_to_pending_review',
|
|
1032
|
+
arguments: {
|
|
1033
|
+
owner: 'shipfox',
|
|
1034
|
+
repo: 'platform',
|
|
1035
|
+
pull_number: 2,
|
|
1036
|
+
path: 'src/agent-tools.ts',
|
|
1037
|
+
body: 'Please handle this error.',
|
|
1038
|
+
},
|
|
1039
|
+
}),
|
|
1040
|
+
).rejects.toMatchObject({
|
|
1041
|
+
reason: 'malformed-provider-response',
|
|
1042
|
+
message: 'GitHub pending pull request review did not include a node ID',
|
|
1043
|
+
});
|
|
1044
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it('skips a malformed newer review for an older valid caller review', async () => {
|
|
1048
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1049
|
+
data: [
|
|
1050
|
+
{
|
|
1051
|
+
id: 40,
|
|
1052
|
+
node_id: 'review-valid',
|
|
1053
|
+
state: 'PENDING',
|
|
1054
|
+
user: githubAppReviewUser,
|
|
1055
|
+
},
|
|
1056
|
+
{id: 41, node_id: ' ', state: 'PENDING', user: githubAppReviewUser},
|
|
1057
|
+
],
|
|
1058
|
+
});
|
|
1059
|
+
const graphql = vi.fn().mockResolvedValueOnce({
|
|
1060
|
+
addPullRequestReviewThread: {thread: {id: 'thread-1'}},
|
|
1061
|
+
});
|
|
1062
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1063
|
+
const session = await provider.openSession({
|
|
1064
|
+
connection: connection(),
|
|
1065
|
+
tools: [pendingReviewTool()],
|
|
1066
|
+
scope: undefined,
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
await session.call({
|
|
1070
|
+
toolId: 'add_comment_to_pending_review',
|
|
1071
|
+
arguments: {
|
|
1072
|
+
owner: 'shipfox',
|
|
1073
|
+
repo: 'platform',
|
|
1074
|
+
pull_number: 2,
|
|
1075
|
+
path: 'src/agent-tools.ts',
|
|
1076
|
+
body: 'Please handle this error.',
|
|
1077
|
+
},
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining('addPullRequestReviewThread'), {
|
|
1081
|
+
input: expect.objectContaining({pullRequestReviewId: 'review-valid'}),
|
|
1082
|
+
});
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
it('rejects a malformed pending review list response', async () => {
|
|
1086
|
+
const request = vi.fn().mockResolvedValueOnce({data: {reviews: []}});
|
|
1087
|
+
const graphql = vi.fn();
|
|
1088
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1089
|
+
const session = await provider.openSession({
|
|
1090
|
+
connection: connection(),
|
|
1091
|
+
tools: [pendingReviewTool()],
|
|
1092
|
+
scope: undefined,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
await expect(
|
|
1096
|
+
session.call({
|
|
1097
|
+
toolId: 'add_comment_to_pending_review',
|
|
1098
|
+
arguments: {
|
|
1099
|
+
owner: 'shipfox',
|
|
1100
|
+
repo: 'platform',
|
|
1101
|
+
pull_number: 2,
|
|
1102
|
+
path: 'src/agent-tools.ts',
|
|
1103
|
+
body: 'Please handle this error.',
|
|
1104
|
+
},
|
|
1105
|
+
}),
|
|
1106
|
+
).rejects.toMatchObject({
|
|
1107
|
+
reason: 'malformed-provider-response',
|
|
1108
|
+
message: 'GitHub pull request review list response was malformed',
|
|
1109
|
+
});
|
|
1110
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
it('maps Octokit 4xx failures to terminal provider errors', async () => {
|
|
1114
|
+
const providerError = new RequestError('commit_id is missing', 422, {
|
|
1115
|
+
request: {
|
|
1116
|
+
method: 'POST',
|
|
1117
|
+
url: 'https://api.github.com/repos/shipfox/platform/issues/1',
|
|
1118
|
+
headers: {},
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
const provider = new GithubAgentToolsProvider({
|
|
1122
|
+
getInstallationByConnectionId: vi.fn(() => Promise.resolve(installation())),
|
|
1123
|
+
tokenProvider: {
|
|
1124
|
+
getInstallationAccessToken: vi.fn(() =>
|
|
1125
|
+
Promise.resolve({
|
|
1126
|
+
token: 'installation-token',
|
|
1127
|
+
expiresAt: new Date(),
|
|
1128
|
+
permissions: {issues: 'read' as const},
|
|
1129
|
+
}),
|
|
1130
|
+
),
|
|
1131
|
+
},
|
|
1132
|
+
createClient: vi.fn(() => ({
|
|
1133
|
+
request: vi.fn(() => Promise.reject(providerError)),
|
|
1134
|
+
})),
|
|
1135
|
+
});
|
|
1136
|
+
const session = await provider.openSession({
|
|
1137
|
+
connection: connection(),
|
|
1138
|
+
tools: [githubAgentToolCatalog[0]],
|
|
1139
|
+
scope: undefined,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
await expect(
|
|
1143
|
+
session.call({
|
|
1144
|
+
toolId: 'issue_read',
|
|
1145
|
+
arguments: {method: 'get', owner: 'shipfox', repo: 'platform', issue_number: 1},
|
|
1146
|
+
}),
|
|
1147
|
+
).rejects.toMatchObject({
|
|
1148
|
+
reason: 'provider-rejected',
|
|
1149
|
+
message: 'commit_id is missing',
|
|
1150
|
+
status: 422,
|
|
1151
|
+
});
|
|
1152
|
+
});
|
|
1153
|
+
|
|
372
1154
|
it('requires a ref for pull request status and check-run reads', async () => {
|
|
373
1155
|
const result = await callGithubTool(
|
|
374
1156
|
'pull_request_read',
|
|
@@ -379,7 +1161,240 @@ describe('github agent tool catalog', () => {
|
|
|
379
1161
|
expect(result).toEqual({
|
|
380
1162
|
isError: true,
|
|
381
1163
|
content: [{type: 'text', text: 'Missing required parameter: ref'}],
|
|
1164
|
+
structuredContent: {code: 'invalid-request'},
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it.each([
|
|
1169
|
+
{
|
|
1170
|
+
method: 'submit_pending',
|
|
1171
|
+
arguments: {
|
|
1172
|
+
method: 'submit_pending',
|
|
1173
|
+
owner: 'shipfox',
|
|
1174
|
+
repo: 'platform',
|
|
1175
|
+
pull_number: 2,
|
|
1176
|
+
body: 'Please address this before merging.',
|
|
1177
|
+
event: 'REQUEST_CHANGES',
|
|
1178
|
+
},
|
|
1179
|
+
route: 'POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events',
|
|
1180
|
+
parameters: {
|
|
1181
|
+
owner: 'shipfox',
|
|
1182
|
+
repo: 'platform',
|
|
1183
|
+
pull_number: 2,
|
|
1184
|
+
body: 'Please address this before merging.',
|
|
1185
|
+
event: 'REQUEST_CHANGES',
|
|
1186
|
+
review_id: 42,
|
|
1187
|
+
},
|
|
1188
|
+
data: {id: 42, state: 'CHANGES_REQUESTED'},
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
method: 'delete_pending',
|
|
1192
|
+
arguments: {
|
|
1193
|
+
method: 'delete_pending',
|
|
1194
|
+
owner: 'shipfox',
|
|
1195
|
+
repo: 'platform',
|
|
1196
|
+
pull_number: 2,
|
|
1197
|
+
},
|
|
1198
|
+
route: 'DELETE /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}',
|
|
1199
|
+
parameters: {
|
|
1200
|
+
owner: 'shipfox',
|
|
1201
|
+
repo: 'platform',
|
|
1202
|
+
pull_number: 2,
|
|
1203
|
+
review_id: 42,
|
|
1204
|
+
},
|
|
1205
|
+
data: {id: 42, state: 'PENDING'},
|
|
1206
|
+
},
|
|
1207
|
+
] satisfies Array<{
|
|
1208
|
+
method: 'submit_pending' | 'delete_pending';
|
|
1209
|
+
arguments: Record<string, unknown>;
|
|
1210
|
+
route: string;
|
|
1211
|
+
parameters: Record<string, unknown>;
|
|
1212
|
+
data: unknown;
|
|
1213
|
+
}>)('$method resolves the latest pending review before writing', async (testCase) => {
|
|
1214
|
+
const request = vi
|
|
1215
|
+
.fn()
|
|
1216
|
+
.mockResolvedValueOnce({
|
|
1217
|
+
data: [
|
|
1218
|
+
{id: 40, state: 'APPROVED'},
|
|
1219
|
+
{id: 41, state: 'PENDING', user: githubAppReviewUser},
|
|
1220
|
+
{id: 42, state: 'PENDING', user: githubAppReviewUser},
|
|
1221
|
+
{id: 43, state: 'PENDING', user: {login: 'another-user'}},
|
|
1222
|
+
],
|
|
1223
|
+
})
|
|
1224
|
+
.mockResolvedValueOnce({data: testCase.data});
|
|
1225
|
+
|
|
1226
|
+
const result = await callGithubToolWithRequest(
|
|
1227
|
+
'pull_request_review_write',
|
|
1228
|
+
testCase.arguments,
|
|
1229
|
+
request,
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
expect(result).toEqual({
|
|
1233
|
+
content: [{type: 'text', text: JSON.stringify(testCase.data)}],
|
|
1234
|
+
structuredContent: testCase.data,
|
|
1235
|
+
});
|
|
1236
|
+
expect(request).toHaveBeenNthCalledWith(
|
|
1237
|
+
1,
|
|
1238
|
+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1239
|
+
expect.objectContaining({
|
|
1240
|
+
owner: 'shipfox',
|
|
1241
|
+
repo: 'platform',
|
|
1242
|
+
pull_number: 2,
|
|
1243
|
+
per_page: 100,
|
|
1244
|
+
page: 1,
|
|
1245
|
+
}),
|
|
1246
|
+
);
|
|
1247
|
+
expect(request).toHaveBeenNthCalledWith(2, testCase.route, testCase.parameters);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('resolves a pending review from a later review page', async () => {
|
|
1251
|
+
const request = vi
|
|
1252
|
+
.fn()
|
|
1253
|
+
.mockResolvedValueOnce({
|
|
1254
|
+
data: [
|
|
1255
|
+
...Array.from({length: 99}, (_, index) => ({id: index + 1, state: 'APPROVED'})),
|
|
1256
|
+
{id: 100, state: 'PENDING', user: githubAppReviewUser},
|
|
1257
|
+
],
|
|
1258
|
+
headers: {
|
|
1259
|
+
link: '<https://api.github.com/repositories/1/pulls/2/reviews?per_page=100&page=2>; rel="last"',
|
|
1260
|
+
},
|
|
1261
|
+
})
|
|
1262
|
+
.mockResolvedValueOnce({
|
|
1263
|
+
data: [{id: 101, state: 'PENDING', user: githubAppReviewUser}],
|
|
1264
|
+
})
|
|
1265
|
+
.mockResolvedValueOnce({data: {id: 101, state: 'COMMENTED'}});
|
|
1266
|
+
|
|
1267
|
+
const result = await callGithubToolWithRequest(
|
|
1268
|
+
'pull_request_review_write',
|
|
1269
|
+
{
|
|
1270
|
+
method: 'submit_pending',
|
|
1271
|
+
owner: 'shipfox',
|
|
1272
|
+
repo: 'platform',
|
|
1273
|
+
pull_number: 2,
|
|
1274
|
+
body: 'Looks good.',
|
|
1275
|
+
event: 'COMMENT',
|
|
1276
|
+
},
|
|
1277
|
+
request,
|
|
1278
|
+
);
|
|
1279
|
+
|
|
1280
|
+
expect(result).toEqual({
|
|
1281
|
+
content: [{type: 'text', text: JSON.stringify({id: 101, state: 'COMMENTED'})}],
|
|
1282
|
+
structuredContent: {id: 101, state: 'COMMENTED'},
|
|
1283
|
+
});
|
|
1284
|
+
expect(request).toHaveBeenNthCalledWith(
|
|
1285
|
+
1,
|
|
1286
|
+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1287
|
+
expect.objectContaining({
|
|
1288
|
+
owner: 'shipfox',
|
|
1289
|
+
repo: 'platform',
|
|
1290
|
+
pull_number: 2,
|
|
1291
|
+
per_page: 100,
|
|
1292
|
+
page: 1,
|
|
1293
|
+
}),
|
|
1294
|
+
);
|
|
1295
|
+
expect(request).toHaveBeenNthCalledWith(
|
|
1296
|
+
2,
|
|
1297
|
+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1298
|
+
expect.objectContaining({
|
|
1299
|
+
owner: 'shipfox',
|
|
1300
|
+
repo: 'platform',
|
|
1301
|
+
pull_number: 2,
|
|
1302
|
+
per_page: 100,
|
|
1303
|
+
page: 2,
|
|
1304
|
+
}),
|
|
1305
|
+
);
|
|
1306
|
+
expect(request).toHaveBeenNthCalledWith(
|
|
1307
|
+
3,
|
|
1308
|
+
'POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}/events',
|
|
1309
|
+
{
|
|
1310
|
+
owner: 'shipfox',
|
|
1311
|
+
repo: 'platform',
|
|
1312
|
+
pull_number: 2,
|
|
1313
|
+
body: 'Looks good.',
|
|
1314
|
+
event: 'COMMENT',
|
|
1315
|
+
review_id: 101,
|
|
1316
|
+
},
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
it('bounds pending review lookup to the newest review pages', async () => {
|
|
1321
|
+
const request = vi.fn().mockResolvedValue({data: []});
|
|
1322
|
+
request.mockResolvedValueOnce({
|
|
1323
|
+
data: Array.from({length: 100}, (_, index) => ({id: index + 1, state: 'APPROVED'})),
|
|
1324
|
+
headers: {
|
|
1325
|
+
link: '<https://api.github.com/repositories/1/pulls/2/reviews?per_page=100&page=6>; rel="last"',
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
await expect(
|
|
1330
|
+
callGithubToolWithRequest(
|
|
1331
|
+
'pull_request_review_write',
|
|
1332
|
+
{
|
|
1333
|
+
method: 'submit_pending',
|
|
1334
|
+
owner: 'shipfox',
|
|
1335
|
+
repo: 'platform',
|
|
1336
|
+
pull_number: 2,
|
|
1337
|
+
body: 'Looks good.',
|
|
1338
|
+
event: 'COMMENT',
|
|
1339
|
+
},
|
|
1340
|
+
request,
|
|
1341
|
+
),
|
|
1342
|
+
).rejects.toMatchObject({
|
|
1343
|
+
reason: 'content-too-large',
|
|
1344
|
+
message: 'GitHub pull request review history exceeded the pending review lookup limit',
|
|
1345
|
+
});
|
|
1346
|
+
expect(request).toHaveBeenCalledTimes(5);
|
|
1347
|
+
expect(request.mock.calls.map((call) => call[1]?.page)).toEqual([1, 6, 5, 4, 3]);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
it.each([0, -1])('rejects non-positive pending review ID %s', async (id) => {
|
|
1351
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1352
|
+
data: [{id, state: 'PENDING', user: githubAppReviewUser}],
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
await expect(
|
|
1356
|
+
callGithubToolWithRequest(
|
|
1357
|
+
'pull_request_review_write',
|
|
1358
|
+
{
|
|
1359
|
+
method: 'submit_pending',
|
|
1360
|
+
owner: 'shipfox',
|
|
1361
|
+
repo: 'platform',
|
|
1362
|
+
pull_number: 2,
|
|
1363
|
+
body: 'Looks good.',
|
|
1364
|
+
event: 'COMMENT',
|
|
1365
|
+
},
|
|
1366
|
+
request,
|
|
1367
|
+
),
|
|
1368
|
+
).rejects.toMatchObject({
|
|
1369
|
+
reason: 'malformed-provider-response',
|
|
1370
|
+
message: 'GitHub pending pull request review did not include a numeric ID',
|
|
1371
|
+
});
|
|
1372
|
+
expect(request).toHaveBeenCalledOnce();
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
it.each([
|
|
1376
|
+
'submit_pending',
|
|
1377
|
+
'delete_pending',
|
|
1378
|
+
] as const)('%s reports when no pending review exists', async (method) => {
|
|
1379
|
+
const request = vi.fn(() => Promise.resolve({data: []}));
|
|
1380
|
+
|
|
1381
|
+
const result = await callGithubToolWithRequest(
|
|
1382
|
+
'pull_request_review_write',
|
|
1383
|
+
{method, owner: 'shipfox', repo: 'platform', pull_number: 2},
|
|
1384
|
+
request,
|
|
1385
|
+
);
|
|
1386
|
+
|
|
1387
|
+
expect(result).toEqual({
|
|
1388
|
+
isError: true,
|
|
1389
|
+
content: [
|
|
1390
|
+
{
|
|
1391
|
+
type: 'text',
|
|
1392
|
+
text: 'No pending pull request review found for the authenticated GitHub user.',
|
|
1393
|
+
},
|
|
1394
|
+
],
|
|
1395
|
+
structuredContent: {code: 'provider-rejected'},
|
|
382
1396
|
});
|
|
1397
|
+
expect(request).toHaveBeenCalledOnce();
|
|
383
1398
|
});
|
|
384
1399
|
|
|
385
1400
|
it.each([
|
|
@@ -479,10 +1494,33 @@ async function callGithubTool(
|
|
|
479
1494
|
toolId: GithubAgentToolId,
|
|
480
1495
|
arguments_: Record<string, unknown>,
|
|
481
1496
|
data: unknown,
|
|
1497
|
+
) {
|
|
1498
|
+
return await callGithubToolWithRequest(
|
|
1499
|
+
toolId,
|
|
1500
|
+
arguments_,
|
|
1501
|
+
vi.fn(() => Promise.resolve({data})),
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
async function callGithubToolWithRequest(
|
|
1506
|
+
toolId: GithubAgentToolId,
|
|
1507
|
+
arguments_: Record<string, unknown>,
|
|
1508
|
+
request: GithubToolClient['request'],
|
|
482
1509
|
) {
|
|
483
1510
|
const tool = githubAgentToolCatalog.find((entry) => entry.id === toolId);
|
|
484
1511
|
if (!tool) throw new Error(`Missing GitHub tool: ${toolId}`);
|
|
485
|
-
const provider =
|
|
1512
|
+
const provider = createAgentToolsProvider({request});
|
|
1513
|
+
const session = await provider.openSession({
|
|
1514
|
+
connection: connection(),
|
|
1515
|
+
tools: [tool],
|
|
1516
|
+
scope: undefined,
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
return await session.call({toolId, arguments: arguments_});
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function createAgentToolsProvider(client: GithubToolClient) {
|
|
1523
|
+
return new GithubAgentToolsProvider({
|
|
486
1524
|
getInstallationByConnectionId: vi.fn(() => Promise.resolve(installation())),
|
|
487
1525
|
tokenProvider: {
|
|
488
1526
|
getInstallationAccessToken: vi.fn(() =>
|
|
@@ -498,15 +1536,14 @@ async function callGithubTool(
|
|
|
498
1536
|
}),
|
|
499
1537
|
),
|
|
500
1538
|
},
|
|
501
|
-
createClient: vi.fn(() =>
|
|
502
|
-
});
|
|
503
|
-
const session = await provider.openSession({
|
|
504
|
-
connection: connection(),
|
|
505
|
-
tools: [tool],
|
|
506
|
-
scope: undefined,
|
|
1539
|
+
createClient: vi.fn(() => client),
|
|
507
1540
|
});
|
|
1541
|
+
}
|
|
508
1542
|
|
|
509
|
-
|
|
1543
|
+
function pendingReviewTool() {
|
|
1544
|
+
const tool = githubAgentToolCatalog.find((entry) => entry.id === 'add_comment_to_pending_review');
|
|
1545
|
+
if (!tool) throw new Error('Missing add_comment_to_pending_review tool');
|
|
1546
|
+
return tool;
|
|
510
1547
|
}
|
|
511
1548
|
|
|
512
1549
|
function connection() {
|
|
@@ -575,3 +1612,7 @@ function inputSchemaFor(id: (typeof githubAgentToolCatalog)[number]['id']) {
|
|
|
575
1612
|
oneOf?: unknown[] | undefined;
|
|
576
1613
|
};
|
|
577
1614
|
}
|
|
1615
|
+
|
|
1616
|
+
function operationKey(toolId: GithubAgentToolId, method?: string) {
|
|
1617
|
+
return `${toolId}.${method ?? ''}`;
|
|
1618
|
+
}
|