@shipfox/api-integration-github 12.5.0 → 12.7.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 +14 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +8 -5
- 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-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/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.map +1 -1
- package/dist/core/agent-tools.js +203 -68
- package/dist/core/agent-tools.js.map +1 -1
- package/dist/core/github-agent-tool-catalog.d.ts +1 -1
- package/dist/core/github-agent-tool-catalog.d.ts.map +1 -1
- package/dist/core/github-agent-tool-catalog.js +19 -0
- 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 +1 -1
- package/src/api/client.test.ts +25 -5
- package/src/api/client.ts +22 -5
- package/src/api/github-octokit.test.ts +115 -0
- package/src/api/github-octokit.ts +49 -0
- package/src/api/installation-token-provider.test.ts +44 -5
- package/src/api/installation-token-provider.ts +5 -2
- package/src/config.ts +5 -0
- package/src/core/agent-tools.test.ts +395 -74
- package/src/core/agent-tools.ts +284 -69
- package/src/core/github-agent-tool-catalog.ts +35 -0
- 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
|
@@ -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',
|
|
@@ -81,6 +83,7 @@ const expectedCatalogRows = [
|
|
|
81
83
|
'get_files',
|
|
82
84
|
'get_commits',
|
|
83
85
|
'get_review_comments',
|
|
86
|
+
'get_review_threads',
|
|
84
87
|
'get_reviews',
|
|
85
88
|
'get_comments',
|
|
86
89
|
'get_check_runs',
|
|
@@ -146,6 +149,14 @@ const expectedCatalogRows = [
|
|
|
146
149
|
requiredScope: [{permission: 'pull_requests', access: 'write'}],
|
|
147
150
|
methods: ['create', 'submit_pending', 'delete_pending'],
|
|
148
151
|
},
|
|
152
|
+
{
|
|
153
|
+
id: 'pull_request_review_thread_write',
|
|
154
|
+
category: 'pull_requests',
|
|
155
|
+
sensitivity: 'write',
|
|
156
|
+
sensitive: false,
|
|
157
|
+
requiredScope: [{permission: 'pull_requests', access: 'write'}],
|
|
158
|
+
methods: ['resolve'],
|
|
159
|
+
},
|
|
149
160
|
{
|
|
150
161
|
id: 'add_comment_to_pending_review',
|
|
151
162
|
category: 'pull_requests',
|
|
@@ -349,6 +360,12 @@ const githubOperationRouteCases = [
|
|
|
349
360
|
args: {pull_number: 1},
|
|
350
361
|
expectedRoute: 'GET /repos/{owner}/{repo}/pulls/{pull_number}/comments',
|
|
351
362
|
},
|
|
363
|
+
{
|
|
364
|
+
toolId: 'pull_request_read',
|
|
365
|
+
method: 'get_review_threads',
|
|
366
|
+
args: {pull_number: 1},
|
|
367
|
+
expectedRoute: 'POST /graphql',
|
|
368
|
+
},
|
|
352
369
|
{
|
|
353
370
|
toolId: 'pull_request_read',
|
|
354
371
|
method: 'get_reviews',
|
|
@@ -427,6 +444,12 @@ const githubOperationRouteCases = [
|
|
|
427
444
|
runtimeInjectedProperties: ['review_id'],
|
|
428
445
|
expectedRoute: 'DELETE /repos/{owner}/{repo}/pulls/{pull_number}/reviews/{review_id}',
|
|
429
446
|
},
|
|
447
|
+
{
|
|
448
|
+
toolId: 'pull_request_review_thread_write',
|
|
449
|
+
method: 'resolve',
|
|
450
|
+
args: {thread_id: 'PRRT_kwDOExample'},
|
|
451
|
+
expectedRoute: 'POST /graphql',
|
|
452
|
+
},
|
|
430
453
|
{
|
|
431
454
|
toolId: 'add_comment_to_pending_review',
|
|
432
455
|
args: {pull_number: 1, path: 'src/index.ts', body: 'Comment'},
|
|
@@ -661,6 +684,7 @@ describe('github agent tool catalog', () => {
|
|
|
661
684
|
{properties: {method: {const: 'get_files'}}, required: []},
|
|
662
685
|
{properties: {method: {const: 'get_commits'}}, required: []},
|
|
663
686
|
{properties: {method: {const: 'get_review_comments'}}, required: []},
|
|
687
|
+
{properties: {method: {const: 'get_review_threads'}}, required: []},
|
|
664
688
|
{properties: {method: {const: 'get_reviews'}}, required: []},
|
|
665
689
|
{properties: {method: {const: 'get_comments'}}, required: []},
|
|
666
690
|
{properties: {method: {const: 'get_check_runs'}}, required: ['ref']},
|
|
@@ -810,6 +834,7 @@ describe('github agent tool catalog', () => {
|
|
|
810
834
|
expect(result).toEqual({
|
|
811
835
|
isError: true,
|
|
812
836
|
content: [{type: 'text', text: 'GitHub artifact download did not return a download URL'}],
|
|
837
|
+
structuredContent: {code: 'malformed-provider-response'},
|
|
813
838
|
});
|
|
814
839
|
});
|
|
815
840
|
|
|
@@ -838,6 +863,104 @@ describe('github agent tool catalog', () => {
|
|
|
838
863
|
});
|
|
839
864
|
});
|
|
840
865
|
|
|
866
|
+
it('reads pull request review threads through GraphQL', async () => {
|
|
867
|
+
const request = vi.fn();
|
|
868
|
+
const data = {
|
|
869
|
+
repository: {
|
|
870
|
+
pullRequest: {
|
|
871
|
+
reviewThreads: {
|
|
872
|
+
nodes: [
|
|
873
|
+
{
|
|
874
|
+
id: 'PRRT_kwDOExample',
|
|
875
|
+
isResolved: false,
|
|
876
|
+
comments: {
|
|
877
|
+
nodes: [
|
|
878
|
+
{
|
|
879
|
+
id: 'PRRC_kwDOExample',
|
|
880
|
+
databaseId: 7,
|
|
881
|
+
body: 'Please handle this.',
|
|
882
|
+
author: {login: 'reviewer'},
|
|
883
|
+
path: 'src/index.ts',
|
|
884
|
+
line: 42,
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
},
|
|
888
|
+
},
|
|
889
|
+
],
|
|
890
|
+
},
|
|
891
|
+
},
|
|
892
|
+
},
|
|
893
|
+
};
|
|
894
|
+
const graphql = vi.fn().mockResolvedValueOnce(data);
|
|
895
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
896
|
+
const session = await provider.openSession({
|
|
897
|
+
connection: connection(),
|
|
898
|
+
tools: [pullRequestReadTool()],
|
|
899
|
+
scope: undefined,
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const result = await session.call({
|
|
903
|
+
toolId: 'pull_request_read',
|
|
904
|
+
arguments: {
|
|
905
|
+
method: 'get_review_threads',
|
|
906
|
+
owner: 'shipfox',
|
|
907
|
+
repo: 'platform',
|
|
908
|
+
pull_number: 2,
|
|
909
|
+
cursor: 'cursor-1',
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
expect(request).not.toHaveBeenCalled();
|
|
914
|
+
expect(graphql).toHaveBeenCalledWith(
|
|
915
|
+
expect.stringContaining('reviewThreads(first: 100, after: $after)'),
|
|
916
|
+
{owner: 'shipfox', repo: 'platform', pullNumber: 2, after: 'cursor-1'},
|
|
917
|
+
);
|
|
918
|
+
const query = graphql.mock.calls[0]?.[0];
|
|
919
|
+
expect(query).toContain('isResolved');
|
|
920
|
+
expect(query).toContain('author');
|
|
921
|
+
expect(query).toContain('path');
|
|
922
|
+
expect(query).toContain('line');
|
|
923
|
+
expect(result).toEqual({
|
|
924
|
+
content: [{type: 'text', text: JSON.stringify(data)}],
|
|
925
|
+
structuredContent: data,
|
|
926
|
+
});
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('resolves a pull request review thread through GraphQL', async () => {
|
|
930
|
+
const request = vi.fn();
|
|
931
|
+
const data = {
|
|
932
|
+
resolveReviewThread: {
|
|
933
|
+
thread: {id: 'PRRT_kwDOExample', isResolved: true},
|
|
934
|
+
},
|
|
935
|
+
};
|
|
936
|
+
const graphql = vi.fn().mockResolvedValueOnce(data);
|
|
937
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
938
|
+
const session = await provider.openSession({
|
|
939
|
+
connection: connection(),
|
|
940
|
+
tools: [pullRequestReviewThreadWriteTool()],
|
|
941
|
+
scope: undefined,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const result = await session.call({
|
|
945
|
+
toolId: 'pull_request_review_thread_write',
|
|
946
|
+
arguments: {
|
|
947
|
+
method: 'resolve',
|
|
948
|
+
owner: 'shipfox',
|
|
949
|
+
repo: 'platform',
|
|
950
|
+
thread_id: 'PRRT_kwDOExample',
|
|
951
|
+
},
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
expect(request).not.toHaveBeenCalled();
|
|
955
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining('resolveReviewThread'), {
|
|
956
|
+
input: {threadId: 'PRRT_kwDOExample'},
|
|
957
|
+
});
|
|
958
|
+
expect(result).toEqual({
|
|
959
|
+
content: [{type: 'text', text: JSON.stringify(data)}],
|
|
960
|
+
structuredContent: data,
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
|
|
841
964
|
it('projects issue comment reactions through the provider session', async () => {
|
|
842
965
|
const request = vi.fn(() => Promise.resolve({data: {id: 7}}));
|
|
843
966
|
const result = await callGithubToolWithRequest(
|
|
@@ -856,34 +979,22 @@ describe('github agent tool catalog', () => {
|
|
|
856
979
|
});
|
|
857
980
|
});
|
|
858
981
|
|
|
859
|
-
it('adds a comment to the latest pending review through GraphQL', async () => {
|
|
860
|
-
const request = vi.fn()
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
},
|
|
982
|
+
it('adds a comment to the latest caller pending review through GraphQL', async () => {
|
|
983
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
984
|
+
data: [
|
|
985
|
+
{id: 40, node_id: 'review-older', state: 'PENDING', user: githubAppReviewUser},
|
|
986
|
+
{id: 41, node_id: 'review-latest', state: 'PENDING', user: githubAppReviewUser},
|
|
987
|
+
{
|
|
988
|
+
id: 42,
|
|
989
|
+
node_id: 'another-review',
|
|
990
|
+
state: 'PENDING',
|
|
991
|
+
user: {login: 'another-user'},
|
|
882
992
|
},
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
993
|
+
],
|
|
994
|
+
});
|
|
995
|
+
const graphql = vi.fn().mockResolvedValueOnce({
|
|
996
|
+
addPullRequestReviewThread: {thread: {id: 'thread-1'}},
|
|
997
|
+
});
|
|
887
998
|
const provider = createAgentToolsProvider({request, graphql});
|
|
888
999
|
const session = await provider.openSession({
|
|
889
1000
|
connection: connection(),
|
|
@@ -907,28 +1018,29 @@ describe('github agent tool catalog', () => {
|
|
|
907
1018
|
},
|
|
908
1019
|
});
|
|
909
1020
|
|
|
910
|
-
expect(request).
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1021
|
+
expect(request).toHaveBeenCalledWith(
|
|
1022
|
+
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1023
|
+
expect.objectContaining({
|
|
1024
|
+
owner: 'shipfox',
|
|
1025
|
+
repo: 'platform',
|
|
1026
|
+
pull_number: 2,
|
|
1027
|
+
per_page: 100,
|
|
1028
|
+
page: 1,
|
|
1029
|
+
}),
|
|
915
1030
|
);
|
|
916
|
-
expect(
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
startLine: 40,
|
|
928
|
-
startSide: 'RIGHT',
|
|
929
|
-
},
|
|
1031
|
+
expect(request.mock.calls[0]?.[1]).toHaveProperty('request.signal');
|
|
1032
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining('addPullRequestReviewThread'), {
|
|
1033
|
+
input: {
|
|
1034
|
+
pullRequestReviewId: 'review-latest',
|
|
1035
|
+
path: 'src/agent-tools.ts',
|
|
1036
|
+
body: 'Please handle this error.',
|
|
1037
|
+
subjectType: 'LINE',
|
|
1038
|
+
line: 42,
|
|
1039
|
+
side: 'RIGHT',
|
|
1040
|
+
startLine: 40,
|
|
1041
|
+
startSide: 'RIGHT',
|
|
930
1042
|
},
|
|
931
|
-
);
|
|
1043
|
+
});
|
|
932
1044
|
expect(result).toEqual({
|
|
933
1045
|
content: [
|
|
934
1046
|
{
|
|
@@ -941,23 +1053,17 @@ describe('github agent tool catalog', () => {
|
|
|
941
1053
|
});
|
|
942
1054
|
|
|
943
1055
|
it('returns an explicit error when there is no pending review for the caller', async () => {
|
|
944
|
-
const request = vi.fn()
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
{
|
|
952
|
-
id: 'review-other-user',
|
|
953
|
-
author: {login: 'another-user'},
|
|
954
|
-
createdAt: '2026-08-09T10:00:00Z',
|
|
955
|
-
},
|
|
956
|
-
],
|
|
957
|
-
},
|
|
1056
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1057
|
+
data: [
|
|
1058
|
+
{
|
|
1059
|
+
id: 41,
|
|
1060
|
+
node_id: 'another-review',
|
|
1061
|
+
state: 'PENDING',
|
|
1062
|
+
user: {login: 'another-user'},
|
|
958
1063
|
},
|
|
959
|
-
|
|
1064
|
+
],
|
|
960
1065
|
});
|
|
1066
|
+
const graphql = vi.fn();
|
|
961
1067
|
const provider = createAgentToolsProvider({request, graphql});
|
|
962
1068
|
const session = await provider.openSession({
|
|
963
1069
|
connection: connection(),
|
|
@@ -976,8 +1082,8 @@ describe('github agent tool catalog', () => {
|
|
|
976
1082
|
},
|
|
977
1083
|
});
|
|
978
1084
|
|
|
979
|
-
expect(
|
|
980
|
-
expect(
|
|
1085
|
+
expect(request).toHaveBeenCalledOnce();
|
|
1086
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
981
1087
|
expect(result).toEqual({
|
|
982
1088
|
isError: true,
|
|
983
1089
|
content: [
|
|
@@ -986,7 +1092,142 @@ describe('github agent tool catalog', () => {
|
|
|
986
1092
|
text: 'No pending pull request review found for the authenticated GitHub user.',
|
|
987
1093
|
},
|
|
988
1094
|
],
|
|
1095
|
+
structuredContent: {code: 'provider-rejected'},
|
|
1096
|
+
});
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it('returns an access-denied code when the installation lacks a required permission', async () => {
|
|
1100
|
+
const provider = new GithubAgentToolsProvider({
|
|
1101
|
+
getInstallationByConnectionId: vi.fn(() => Promise.resolve(installation())),
|
|
1102
|
+
tokenProvider: {
|
|
1103
|
+
getInstallationAccessToken: vi.fn(() =>
|
|
1104
|
+
Promise.resolve({
|
|
1105
|
+
token: 'installation-token',
|
|
1106
|
+
expiresAt: new Date(),
|
|
1107
|
+
permissions: {},
|
|
1108
|
+
}),
|
|
1109
|
+
),
|
|
1110
|
+
},
|
|
1111
|
+
});
|
|
1112
|
+
const tool = githubAgentToolCatalog.find((entry) => entry.id === 'list_issues');
|
|
1113
|
+
if (!tool) throw new Error('Missing list_issues tool');
|
|
1114
|
+
const session = await provider.openSession({
|
|
1115
|
+
connection: connection(),
|
|
1116
|
+
tools: [tool],
|
|
1117
|
+
scope: undefined,
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
const result = await session.call({
|
|
1121
|
+
toolId: 'list_issues',
|
|
1122
|
+
arguments: {owner: 'shipfox', repo: 'platform'},
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
expect(result).toEqual({
|
|
1126
|
+
isError: true,
|
|
1127
|
+
content: [
|
|
1128
|
+
{
|
|
1129
|
+
type: 'text',
|
|
1130
|
+
text: 'GitHub installation token is missing permission for this operation',
|
|
1131
|
+
},
|
|
1132
|
+
],
|
|
1133
|
+
structuredContent: {code: 'access-denied'},
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
it('rejects a pending review without a GraphQL node ID', async () => {
|
|
1138
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1139
|
+
data: [{id: 41, state: 'PENDING', user: githubAppReviewUser}],
|
|
1140
|
+
});
|
|
1141
|
+
const graphql = vi.fn();
|
|
1142
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1143
|
+
const session = await provider.openSession({
|
|
1144
|
+
connection: connection(),
|
|
1145
|
+
tools: [pendingReviewTool()],
|
|
1146
|
+
scope: undefined,
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
await expect(
|
|
1150
|
+
session.call({
|
|
1151
|
+
toolId: 'add_comment_to_pending_review',
|
|
1152
|
+
arguments: {
|
|
1153
|
+
owner: 'shipfox',
|
|
1154
|
+
repo: 'platform',
|
|
1155
|
+
pull_number: 2,
|
|
1156
|
+
path: 'src/agent-tools.ts',
|
|
1157
|
+
body: 'Please handle this error.',
|
|
1158
|
+
},
|
|
1159
|
+
}),
|
|
1160
|
+
).rejects.toMatchObject({
|
|
1161
|
+
reason: 'malformed-provider-response',
|
|
1162
|
+
message: 'GitHub pending pull request review did not include a node ID',
|
|
989
1163
|
});
|
|
1164
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
it('skips a malformed newer review for an older valid caller review', async () => {
|
|
1168
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1169
|
+
data: [
|
|
1170
|
+
{
|
|
1171
|
+
id: 40,
|
|
1172
|
+
node_id: 'review-valid',
|
|
1173
|
+
state: 'PENDING',
|
|
1174
|
+
user: githubAppReviewUser,
|
|
1175
|
+
},
|
|
1176
|
+
{id: 41, node_id: ' ', state: 'PENDING', user: githubAppReviewUser},
|
|
1177
|
+
],
|
|
1178
|
+
});
|
|
1179
|
+
const graphql = vi.fn().mockResolvedValueOnce({
|
|
1180
|
+
addPullRequestReviewThread: {thread: {id: 'thread-1'}},
|
|
1181
|
+
});
|
|
1182
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1183
|
+
const session = await provider.openSession({
|
|
1184
|
+
connection: connection(),
|
|
1185
|
+
tools: [pendingReviewTool()],
|
|
1186
|
+
scope: undefined,
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
await session.call({
|
|
1190
|
+
toolId: 'add_comment_to_pending_review',
|
|
1191
|
+
arguments: {
|
|
1192
|
+
owner: 'shipfox',
|
|
1193
|
+
repo: 'platform',
|
|
1194
|
+
pull_number: 2,
|
|
1195
|
+
path: 'src/agent-tools.ts',
|
|
1196
|
+
body: 'Please handle this error.',
|
|
1197
|
+
},
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
expect(graphql).toHaveBeenCalledWith(expect.stringContaining('addPullRequestReviewThread'), {
|
|
1201
|
+
input: expect.objectContaining({pullRequestReviewId: 'review-valid'}),
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
it('rejects a malformed pending review list response', async () => {
|
|
1206
|
+
const request = vi.fn().mockResolvedValueOnce({data: {reviews: []}});
|
|
1207
|
+
const graphql = vi.fn();
|
|
1208
|
+
const provider = createAgentToolsProvider({request, graphql});
|
|
1209
|
+
const session = await provider.openSession({
|
|
1210
|
+
connection: connection(),
|
|
1211
|
+
tools: [pendingReviewTool()],
|
|
1212
|
+
scope: undefined,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
await expect(
|
|
1216
|
+
session.call({
|
|
1217
|
+
toolId: 'add_comment_to_pending_review',
|
|
1218
|
+
arguments: {
|
|
1219
|
+
owner: 'shipfox',
|
|
1220
|
+
repo: 'platform',
|
|
1221
|
+
pull_number: 2,
|
|
1222
|
+
path: 'src/agent-tools.ts',
|
|
1223
|
+
body: 'Please handle this error.',
|
|
1224
|
+
},
|
|
1225
|
+
}),
|
|
1226
|
+
).rejects.toMatchObject({
|
|
1227
|
+
reason: 'malformed-provider-response',
|
|
1228
|
+
message: 'GitHub pull request review list response was malformed',
|
|
1229
|
+
});
|
|
1230
|
+
expect(graphql).not.toHaveBeenCalled();
|
|
990
1231
|
});
|
|
991
1232
|
|
|
992
1233
|
it('maps Octokit 4xx failures to terminal provider errors', async () => {
|
|
@@ -1040,6 +1281,7 @@ describe('github agent tool catalog', () => {
|
|
|
1040
1281
|
expect(result).toEqual({
|
|
1041
1282
|
isError: true,
|
|
1042
1283
|
content: [{type: 'text', text: 'Missing required parameter: ref'}],
|
|
1284
|
+
structuredContent: {code: 'invalid-request'},
|
|
1043
1285
|
});
|
|
1044
1286
|
});
|
|
1045
1287
|
|
|
@@ -1094,8 +1336,9 @@ describe('github agent tool catalog', () => {
|
|
|
1094
1336
|
.mockResolvedValueOnce({
|
|
1095
1337
|
data: [
|
|
1096
1338
|
{id: 40, state: 'APPROVED'},
|
|
1097
|
-
{id: 41, state: 'PENDING'},
|
|
1098
|
-
{id: 42, state: 'PENDING'},
|
|
1339
|
+
{id: 41, state: 'PENDING', user: githubAppReviewUser},
|
|
1340
|
+
{id: 42, state: 'PENDING', user: githubAppReviewUser},
|
|
1341
|
+
{id: 43, state: 'PENDING', user: {login: 'another-user'}},
|
|
1099
1342
|
],
|
|
1100
1343
|
})
|
|
1101
1344
|
.mockResolvedValueOnce({data: testCase.data});
|
|
@@ -1113,13 +1356,13 @@ describe('github agent tool catalog', () => {
|
|
|
1113
1356
|
expect(request).toHaveBeenNthCalledWith(
|
|
1114
1357
|
1,
|
|
1115
1358
|
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1116
|
-
{
|
|
1359
|
+
expect.objectContaining({
|
|
1117
1360
|
owner: 'shipfox',
|
|
1118
1361
|
repo: 'platform',
|
|
1119
1362
|
pull_number: 2,
|
|
1120
1363
|
per_page: 100,
|
|
1121
1364
|
page: 1,
|
|
1122
|
-
},
|
|
1365
|
+
}),
|
|
1123
1366
|
);
|
|
1124
1367
|
expect(request).toHaveBeenNthCalledWith(2, testCase.route, testCase.parameters);
|
|
1125
1368
|
});
|
|
@@ -1128,9 +1371,17 @@ describe('github agent tool catalog', () => {
|
|
|
1128
1371
|
const request = vi
|
|
1129
1372
|
.fn()
|
|
1130
1373
|
.mockResolvedValueOnce({
|
|
1131
|
-
data:
|
|
1374
|
+
data: [
|
|
1375
|
+
...Array.from({length: 99}, (_, index) => ({id: index + 1, state: 'APPROVED'})),
|
|
1376
|
+
{id: 100, state: 'PENDING', user: githubAppReviewUser},
|
|
1377
|
+
],
|
|
1378
|
+
headers: {
|
|
1379
|
+
link: '<https://api.github.com/repositories/1/pulls/2/reviews?per_page=100&page=2>; rel="last"',
|
|
1380
|
+
},
|
|
1381
|
+
})
|
|
1382
|
+
.mockResolvedValueOnce({
|
|
1383
|
+
data: [{id: 101, state: 'PENDING', user: githubAppReviewUser}],
|
|
1132
1384
|
})
|
|
1133
|
-
.mockResolvedValueOnce({data: [{id: 101, state: 'PENDING'}]})
|
|
1134
1385
|
.mockResolvedValueOnce({data: {id: 101, state: 'COMMENTED'}});
|
|
1135
1386
|
|
|
1136
1387
|
const result = await callGithubToolWithRequest(
|
|
@@ -1153,24 +1404,24 @@ describe('github agent tool catalog', () => {
|
|
|
1153
1404
|
expect(request).toHaveBeenNthCalledWith(
|
|
1154
1405
|
1,
|
|
1155
1406
|
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1156
|
-
{
|
|
1407
|
+
expect.objectContaining({
|
|
1157
1408
|
owner: 'shipfox',
|
|
1158
1409
|
repo: 'platform',
|
|
1159
1410
|
pull_number: 2,
|
|
1160
1411
|
per_page: 100,
|
|
1161
1412
|
page: 1,
|
|
1162
|
-
},
|
|
1413
|
+
}),
|
|
1163
1414
|
);
|
|
1164
1415
|
expect(request).toHaveBeenNthCalledWith(
|
|
1165
1416
|
2,
|
|
1166
1417
|
'GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews',
|
|
1167
|
-
{
|
|
1418
|
+
expect.objectContaining({
|
|
1168
1419
|
owner: 'shipfox',
|
|
1169
1420
|
repo: 'platform',
|
|
1170
1421
|
pull_number: 2,
|
|
1171
1422
|
per_page: 100,
|
|
1172
1423
|
page: 2,
|
|
1173
|
-
},
|
|
1424
|
+
}),
|
|
1174
1425
|
);
|
|
1175
1426
|
expect(request).toHaveBeenNthCalledWith(
|
|
1176
1427
|
3,
|
|
@@ -1186,6 +1437,61 @@ describe('github agent tool catalog', () => {
|
|
|
1186
1437
|
);
|
|
1187
1438
|
});
|
|
1188
1439
|
|
|
1440
|
+
it('bounds pending review lookup to the newest review pages', async () => {
|
|
1441
|
+
const request = vi.fn().mockResolvedValue({data: []});
|
|
1442
|
+
request.mockResolvedValueOnce({
|
|
1443
|
+
data: Array.from({length: 100}, (_, index) => ({id: index + 1, state: 'APPROVED'})),
|
|
1444
|
+
headers: {
|
|
1445
|
+
link: '<https://api.github.com/repositories/1/pulls/2/reviews?per_page=100&page=6>; rel="last"',
|
|
1446
|
+
},
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
await expect(
|
|
1450
|
+
callGithubToolWithRequest(
|
|
1451
|
+
'pull_request_review_write',
|
|
1452
|
+
{
|
|
1453
|
+
method: 'submit_pending',
|
|
1454
|
+
owner: 'shipfox',
|
|
1455
|
+
repo: 'platform',
|
|
1456
|
+
pull_number: 2,
|
|
1457
|
+
body: 'Looks good.',
|
|
1458
|
+
event: 'COMMENT',
|
|
1459
|
+
},
|
|
1460
|
+
request,
|
|
1461
|
+
),
|
|
1462
|
+
).rejects.toMatchObject({
|
|
1463
|
+
reason: 'content-too-large',
|
|
1464
|
+
message: 'GitHub pull request review history exceeded the pending review lookup limit',
|
|
1465
|
+
});
|
|
1466
|
+
expect(request).toHaveBeenCalledTimes(5);
|
|
1467
|
+
expect(request.mock.calls.map((call) => call[1]?.page)).toEqual([1, 6, 5, 4, 3]);
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
it.each([0, -1])('rejects non-positive pending review ID %s', async (id) => {
|
|
1471
|
+
const request = vi.fn().mockResolvedValueOnce({
|
|
1472
|
+
data: [{id, state: 'PENDING', user: githubAppReviewUser}],
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
await expect(
|
|
1476
|
+
callGithubToolWithRequest(
|
|
1477
|
+
'pull_request_review_write',
|
|
1478
|
+
{
|
|
1479
|
+
method: 'submit_pending',
|
|
1480
|
+
owner: 'shipfox',
|
|
1481
|
+
repo: 'platform',
|
|
1482
|
+
pull_number: 2,
|
|
1483
|
+
body: 'Looks good.',
|
|
1484
|
+
event: 'COMMENT',
|
|
1485
|
+
},
|
|
1486
|
+
request,
|
|
1487
|
+
),
|
|
1488
|
+
).rejects.toMatchObject({
|
|
1489
|
+
reason: 'malformed-provider-response',
|
|
1490
|
+
message: 'GitHub pending pull request review did not include a numeric ID',
|
|
1491
|
+
});
|
|
1492
|
+
expect(request).toHaveBeenCalledOnce();
|
|
1493
|
+
});
|
|
1494
|
+
|
|
1189
1495
|
it.each([
|
|
1190
1496
|
'submit_pending',
|
|
1191
1497
|
'delete_pending',
|
|
@@ -1206,6 +1512,7 @@ describe('github agent tool catalog', () => {
|
|
|
1206
1512
|
text: 'No pending pull request review found for the authenticated GitHub user.',
|
|
1207
1513
|
},
|
|
1208
1514
|
],
|
|
1515
|
+
structuredContent: {code: 'provider-rejected'},
|
|
1209
1516
|
});
|
|
1210
1517
|
expect(request).toHaveBeenCalledOnce();
|
|
1211
1518
|
});
|
|
@@ -1359,6 +1666,20 @@ function pendingReviewTool() {
|
|
|
1359
1666
|
return tool;
|
|
1360
1667
|
}
|
|
1361
1668
|
|
|
1669
|
+
function pullRequestReadTool() {
|
|
1670
|
+
const tool = githubAgentToolCatalog.find((entry) => entry.id === 'pull_request_read');
|
|
1671
|
+
if (!tool) throw new Error('Missing pull_request_read tool');
|
|
1672
|
+
return tool;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function pullRequestReviewThreadWriteTool() {
|
|
1676
|
+
const tool = githubAgentToolCatalog.find(
|
|
1677
|
+
(entry) => entry.id === 'pull_request_review_thread_write',
|
|
1678
|
+
);
|
|
1679
|
+
if (!tool) throw new Error('Missing pull_request_review_thread_write tool');
|
|
1680
|
+
return tool;
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1362
1683
|
function connection() {
|
|
1363
1684
|
return {
|
|
1364
1685
|
id: 'connection-1',
|