@shipfox/api-integration-github 12.5.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.
Files changed (39) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +8 -5
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/github-octokit.d.ts +7 -0
  7. package/dist/api/github-octokit.d.ts.map +1 -0
  8. package/dist/api/github-octokit.js +32 -0
  9. package/dist/api/github-octokit.js.map +1 -0
  10. package/dist/api/installation-token-provider.d.ts.map +1 -1
  11. package/dist/api/installation-token-provider.js +4 -2
  12. package/dist/api/installation-token-provider.js.map +1 -1
  13. package/dist/config.d.ts +1 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +8 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/agent-tools.d.ts.map +1 -1
  18. package/dist/core/agent-tools.js +124 -78
  19. package/dist/core/agent-tools.js.map +1 -1
  20. package/dist/metrics/instance.d.ts +1 -0
  21. package/dist/metrics/instance.d.ts.map +1 -1
  22. package/dist/metrics/instance.js +19 -0
  23. package/dist/metrics/instance.js.map +1 -1
  24. package/dist/tsconfig.test.tsbuildinfo +1 -1
  25. package/package.json +1 -1
  26. package/src/api/client.test.ts +25 -5
  27. package/src/api/client.ts +22 -5
  28. package/src/api/github-octokit.test.ts +115 -0
  29. package/src/api/github-octokit.ts +49 -0
  30. package/src/api/installation-token-provider.test.ts +44 -5
  31. package/src/api/installation-token-provider.ts +5 -2
  32. package/src/config.ts +5 -0
  33. package/src/core/agent-tools.test.ts +261 -74
  34. package/src/core/agent-tools.ts +191 -80
  35. package/src/metrics/instance.ts +25 -0
  36. package/test/env.ts +1 -0
  37. package/test/fixtures/github-installation-token.ts +8 -0
  38. package/test/index.ts +4 -0
  39. package/tsconfig.build.tsbuildinfo +1 -1
@@ -12,6 +12,8 @@ import {
12
12
  } from '#core/agent-tools.js';
13
13
  import {createGithubIntegrationProvider} from '#index.js';
14
14
 
15
+ const githubAppReviewUser = {login: 'shipfox-test[bot]'};
16
+
15
17
  const expectedCatalogRows = [
16
18
  {
17
19
  id: 'issue_read',
@@ -810,6 +812,7 @@ describe('github agent tool catalog', () => {
810
812
  expect(result).toEqual({
811
813
  isError: true,
812
814
  content: [{type: 'text', text: 'GitHub artifact download did not return a download URL'}],
815
+ structuredContent: {code: 'malformed-provider-response'},
813
816
  });
814
817
  });
815
818
 
@@ -856,34 +859,22 @@ describe('github agent tool catalog', () => {
856
859
  });
857
860
  });
858
861
 
859
- it('adds a comment to the latest pending review through GraphQL', async () => {
860
- const request = vi.fn();
861
- const graphql = vi
862
- .fn()
863
- .mockResolvedValueOnce({
864
- viewer: {login: 'shipfox-ai[bot]'},
865
- repository: {
866
- pullRequest: {
867
- reviews: {
868
- nodes: [
869
- {
870
- id: 'review-older',
871
- author: {login: 'shipfox-ai[bot]'},
872
- createdAt: '2026-08-09T10:00:00Z',
873
- },
874
- {
875
- id: 'review-latest',
876
- author: {login: 'shipfox-ai[bot]'},
877
- createdAt: '2026-08-09T10:01:00Z',
878
- },
879
- ],
880
- },
881
- },
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'},
882
872
  },
883
- })
884
- .mockResolvedValueOnce({
885
- addPullRequestReviewThread: {thread: {id: 'thread-1'}},
886
- });
873
+ ],
874
+ });
875
+ const graphql = vi.fn().mockResolvedValueOnce({
876
+ addPullRequestReviewThread: {thread: {id: 'thread-1'}},
877
+ });
887
878
  const provider = createAgentToolsProvider({request, graphql});
888
879
  const session = await provider.openSession({
889
880
  connection: connection(),
@@ -907,28 +898,29 @@ describe('github agent tool catalog', () => {
907
898
  },
908
899
  });
909
900
 
910
- expect(request).not.toHaveBeenCalled();
911
- expect(graphql).toHaveBeenNthCalledWith(
912
- 1,
913
- expect.stringContaining('reviews(last: 100, states: [PENDING])'),
914
- {owner: 'shipfox', repo: 'platform', pullNumber: 2},
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
+ }),
915
910
  );
916
- expect(graphql).toHaveBeenNthCalledWith(
917
- 2,
918
- expect.stringContaining('addPullRequestReviewThread'),
919
- {
920
- input: {
921
- pullRequestReviewId: 'review-latest',
922
- path: 'src/agent-tools.ts',
923
- body: 'Please handle this error.',
924
- subjectType: 'LINE',
925
- line: 42,
926
- side: 'RIGHT',
927
- startLine: 40,
928
- startSide: 'RIGHT',
929
- },
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',
930
922
  },
931
- );
923
+ });
932
924
  expect(result).toEqual({
933
925
  content: [
934
926
  {
@@ -941,23 +933,17 @@ describe('github agent tool catalog', () => {
941
933
  });
942
934
 
943
935
  it('returns an explicit error when there is no pending review for the caller', async () => {
944
- const request = vi.fn();
945
- const graphql = vi.fn().mockResolvedValueOnce({
946
- viewer: {login: 'shipfox-ai[bot]'},
947
- repository: {
948
- pullRequest: {
949
- reviews: {
950
- nodes: [
951
- {
952
- id: 'review-other-user',
953
- author: {login: 'another-user'},
954
- createdAt: '2026-08-09T10:00:00Z',
955
- },
956
- ],
957
- },
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'},
958
943
  },
959
- },
944
+ ],
960
945
  });
946
+ const graphql = vi.fn();
961
947
  const provider = createAgentToolsProvider({request, graphql});
962
948
  const session = await provider.openSession({
963
949
  connection: connection(),
@@ -976,8 +962,8 @@ describe('github agent tool catalog', () => {
976
962
  },
977
963
  });
978
964
 
979
- expect(graphql).toHaveBeenCalledTimes(1);
980
- expect(request).not.toHaveBeenCalled();
965
+ expect(request).toHaveBeenCalledOnce();
966
+ expect(graphql).not.toHaveBeenCalled();
981
967
  expect(result).toEqual({
982
968
  isError: true,
983
969
  content: [
@@ -986,9 +972,144 @@ describe('github agent tool catalog', () => {
986
972
  text: 'No pending pull request review found for the authenticated GitHub user.',
987
973
  },
988
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'}),
989
1082
  });
990
1083
  });
991
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
+
992
1113
  it('maps Octokit 4xx failures to terminal provider errors', async () => {
993
1114
  const providerError = new RequestError('commit_id is missing', 422, {
994
1115
  request: {
@@ -1040,6 +1161,7 @@ describe('github agent tool catalog', () => {
1040
1161
  expect(result).toEqual({
1041
1162
  isError: true,
1042
1163
  content: [{type: 'text', text: 'Missing required parameter: ref'}],
1164
+ structuredContent: {code: 'invalid-request'},
1043
1165
  });
1044
1166
  });
1045
1167
 
@@ -1094,8 +1216,9 @@ describe('github agent tool catalog', () => {
1094
1216
  .mockResolvedValueOnce({
1095
1217
  data: [
1096
1218
  {id: 40, state: 'APPROVED'},
1097
- {id: 41, state: 'PENDING'},
1098
- {id: 42, state: 'PENDING'},
1219
+ {id: 41, state: 'PENDING', user: githubAppReviewUser},
1220
+ {id: 42, state: 'PENDING', user: githubAppReviewUser},
1221
+ {id: 43, state: 'PENDING', user: {login: 'another-user'}},
1099
1222
  ],
1100
1223
  })
1101
1224
  .mockResolvedValueOnce({data: testCase.data});
@@ -1113,13 +1236,13 @@ describe('github agent tool catalog', () => {
1113
1236
  expect(request).toHaveBeenNthCalledWith(
1114
1237
  1,
1115
1238
  'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
1116
- {
1239
+ expect.objectContaining({
1117
1240
  owner: 'shipfox',
1118
1241
  repo: 'platform',
1119
1242
  pull_number: 2,
1120
1243
  per_page: 100,
1121
1244
  page: 1,
1122
- },
1245
+ }),
1123
1246
  );
1124
1247
  expect(request).toHaveBeenNthCalledWith(2, testCase.route, testCase.parameters);
1125
1248
  });
@@ -1128,9 +1251,17 @@ describe('github agent tool catalog', () => {
1128
1251
  const request = vi
1129
1252
  .fn()
1130
1253
  .mockResolvedValueOnce({
1131
- data: Array.from({length: 100}, (_, index) => ({id: index + 1, state: 'APPROVED'})),
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}],
1132
1264
  })
1133
- .mockResolvedValueOnce({data: [{id: 101, state: 'PENDING'}]})
1134
1265
  .mockResolvedValueOnce({data: {id: 101, state: 'COMMENTED'}});
1135
1266
 
1136
1267
  const result = await callGithubToolWithRequest(
@@ -1153,24 +1284,24 @@ describe('github agent tool catalog', () => {
1153
1284
  expect(request).toHaveBeenNthCalledWith(
1154
1285
  1,
1155
1286
  'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
1156
- {
1287
+ expect.objectContaining({
1157
1288
  owner: 'shipfox',
1158
1289
  repo: 'platform',
1159
1290
  pull_number: 2,
1160
1291
  per_page: 100,
1161
1292
  page: 1,
1162
- },
1293
+ }),
1163
1294
  );
1164
1295
  expect(request).toHaveBeenNthCalledWith(
1165
1296
  2,
1166
1297
  'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
1167
- {
1298
+ expect.objectContaining({
1168
1299
  owner: 'shipfox',
1169
1300
  repo: 'platform',
1170
1301
  pull_number: 2,
1171
1302
  per_page: 100,
1172
1303
  page: 2,
1173
- },
1304
+ }),
1174
1305
  );
1175
1306
  expect(request).toHaveBeenNthCalledWith(
1176
1307
  3,
@@ -1186,6 +1317,61 @@ describe('github agent tool catalog', () => {
1186
1317
  );
1187
1318
  });
1188
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
+
1189
1375
  it.each([
1190
1376
  'submit_pending',
1191
1377
  'delete_pending',
@@ -1206,6 +1392,7 @@ describe('github agent tool catalog', () => {
1206
1392
  text: 'No pending pull request review found for the authenticated GitHub user.',
1207
1393
  },
1208
1394
  ],
1395
+ structuredContent: {code: 'provider-rejected'},
1209
1396
  });
1210
1397
  expect(request).toHaveBeenCalledOnce();
1211
1398
  });