github-issue-tower-defence-management 1.32.0 → 1.34.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/CHANGELOG.md +20 -0
- package/README.md +92 -6
- package/bin/adapter/entry-points/cli/index.js +422 -5
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +67 -33
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/FetchWebhookRepository.js +10 -0
- package/bin/adapter/repositories/FetchWebhookRepository.js.map +1 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js +190 -0
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js +225 -0
- package/bin/adapter/repositories/OauthAPIClaudeRepository.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +17 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +73 -17
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/WebhookRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +1315 -15
- package/src/adapter/entry-points/cli/index.ts +648 -5
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +14 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +17 -2
- package/src/adapter/repositories/FetchWebhookRepository.ts +7 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.ts +291 -0
- package/src/adapter/repositories/OauthAPIClaudeRepository.ts +279 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +28 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +30 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +722 -16
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +117 -20
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +2 -0
- package/src/domain/usecases/adapter-interfaces/WebhookRepository.ts +3 -0
- package/types/adapter/entry-points/cli/index.d.ts +19 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/FetchWebhookRepository.d.ts +5 -0
- package/types/adapter/repositories/FetchWebhookRepository.d.ts.map +1 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +12 -0
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts +13 -0
- package/types/adapter/repositories/OauthAPIClaudeRepository.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +10 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +2 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/WebhookRepository.d.ts.map +1 -0
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { NotifyFinishedIssuePreparationUseCase } from './NotifyFinishedIssuePreparationUseCase';
|
|
2
|
-
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
-
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
|
-
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
5
2
|
import { Issue } from '../entities/Issue';
|
|
6
3
|
import { Project } from '../entities/Project';
|
|
7
4
|
import { Comment } from '../entities/Comment';
|
|
8
|
-
|
|
9
|
-
type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
|
|
5
|
+
import { StoryObjectMap } from '../entities/StoryObjectMap';
|
|
10
6
|
|
|
11
7
|
const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
|
12
8
|
id: 'project-1',
|
|
@@ -62,13 +58,23 @@ const createMockComment = (overrides: Partial<Comment> = {}): Comment => ({
|
|
|
62
58
|
|
|
63
59
|
describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
64
60
|
let useCase: NotifyFinishedIssuePreparationUseCase;
|
|
65
|
-
let mockProjectRepository:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
let mockProjectRepository: {
|
|
62
|
+
getByUrl: jest.Mock;
|
|
63
|
+
prepareStatus: jest.Mock;
|
|
64
|
+
};
|
|
65
|
+
let mockIssueRepository: {
|
|
66
|
+
get: jest.Mock;
|
|
67
|
+
update: jest.Mock;
|
|
68
|
+
findRelatedOpenPRs: jest.Mock;
|
|
69
|
+
getStoryObjectMap: jest.Mock;
|
|
70
|
+
};
|
|
71
|
+
let mockIssueCommentRepository: {
|
|
72
|
+
getCommentsFromIssue: jest.Mock;
|
|
73
|
+
createComment: jest.Mock;
|
|
74
|
+
};
|
|
75
|
+
let mockWebhookRepository: {
|
|
76
|
+
sendGetRequest: jest.Mock;
|
|
77
|
+
};
|
|
72
78
|
let mockProject: Project;
|
|
73
79
|
|
|
74
80
|
beforeEach(() => {
|
|
@@ -78,9 +84,15 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
78
84
|
|
|
79
85
|
mockProjectRepository = {
|
|
80
86
|
getByUrl: jest.fn(),
|
|
87
|
+
prepareStatus: jest
|
|
88
|
+
.fn()
|
|
89
|
+
.mockImplementation((_name: string, project: Project) =>
|
|
90
|
+
Promise.resolve(project),
|
|
91
|
+
),
|
|
81
92
|
};
|
|
82
93
|
|
|
83
94
|
mockIssueRepository = {
|
|
95
|
+
getStoryObjectMap: jest.fn(),
|
|
84
96
|
get: jest.fn(),
|
|
85
97
|
update: jest.fn(),
|
|
86
98
|
findRelatedOpenPRs: jest.fn(),
|
|
@@ -91,10 +103,73 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
91
103
|
createComment: jest.fn(),
|
|
92
104
|
};
|
|
93
105
|
|
|
106
|
+
mockWebhookRepository = {
|
|
107
|
+
sendGetRequest: jest.fn(),
|
|
108
|
+
};
|
|
109
|
+
|
|
94
110
|
useCase = new NotifyFinishedIssuePreparationUseCase(
|
|
95
111
|
mockProjectRepository,
|
|
96
112
|
mockIssueRepository,
|
|
97
113
|
mockIssueCommentRepository,
|
|
114
|
+
mockWebhookRepository,
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should call prepareStatus for preparationStatus, awaitingWorkspaceStatus, and awaitingQualityCheckStatus with chained project objects', async () => {
|
|
119
|
+
const projectAfterFirstPrepare = createMockProject();
|
|
120
|
+
const projectAfterSecondPrepare = createMockProject();
|
|
121
|
+
const projectAfterThirdPrepare = createMockProject();
|
|
122
|
+
const issue = createMockIssue({
|
|
123
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
124
|
+
status: 'Preparation',
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
128
|
+
mockProjectRepository.prepareStatus
|
|
129
|
+
.mockResolvedValueOnce(projectAfterFirstPrepare)
|
|
130
|
+
.mockResolvedValueOnce(projectAfterSecondPrepare)
|
|
131
|
+
.mockResolvedValueOnce(projectAfterThirdPrepare);
|
|
132
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
133
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
134
|
+
createMockComment({ content: 'From: Test report' }),
|
|
135
|
+
]);
|
|
136
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
137
|
+
{
|
|
138
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
139
|
+
isConflicted: false,
|
|
140
|
+
isPassedAllCiJob: true,
|
|
141
|
+
isCiStateSuccess: true,
|
|
142
|
+
isResolvedAllReviewComments: true,
|
|
143
|
+
isBranchOutOfDate: false,
|
|
144
|
+
missingRequiredCheckNames: [],
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
await useCase.run({
|
|
149
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
150
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
151
|
+
preparationStatus: 'Preparation',
|
|
152
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
153
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
154
|
+
thresholdForAutoReject: 3,
|
|
155
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenCalledTimes(3);
|
|
159
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenNthCalledWith(
|
|
160
|
+
1,
|
|
161
|
+
'Preparation',
|
|
162
|
+
mockProject,
|
|
163
|
+
);
|
|
164
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenNthCalledWith(
|
|
165
|
+
2,
|
|
166
|
+
'Awaiting Workspace',
|
|
167
|
+
projectAfterFirstPrepare,
|
|
168
|
+
);
|
|
169
|
+
expect(mockProjectRepository.prepareStatus).toHaveBeenNthCalledWith(
|
|
170
|
+
3,
|
|
171
|
+
'Awaiting Quality Check',
|
|
172
|
+
projectAfterSecondPrepare,
|
|
98
173
|
);
|
|
99
174
|
});
|
|
100
175
|
|
|
@@ -114,8 +189,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
114
189
|
url: 'https://github.com/user/repo/pull/1',
|
|
115
190
|
isConflicted: false,
|
|
116
191
|
isPassedAllCiJob: true,
|
|
192
|
+
isCiStateSuccess: true,
|
|
117
193
|
isResolvedAllReviewComments: true,
|
|
118
194
|
isBranchOutOfDate: false,
|
|
195
|
+
missingRequiredCheckNames: [],
|
|
119
196
|
},
|
|
120
197
|
]);
|
|
121
198
|
|
|
@@ -126,6 +203,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
126
203
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
127
204
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
128
205
|
thresholdForAutoReject: 3,
|
|
206
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
129
207
|
});
|
|
130
208
|
|
|
131
209
|
expect(mockIssueRepository.update).toHaveBeenCalledTimes(1);
|
|
@@ -150,6 +228,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
150
228
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
151
229
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
152
230
|
thresholdForAutoReject: 3,
|
|
231
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
153
232
|
}),
|
|
154
233
|
).rejects.toThrow(
|
|
155
234
|
'Issue not found: https://github.com/user/repo/issues/999',
|
|
@@ -173,6 +252,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
173
252
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
174
253
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
175
254
|
thresholdForAutoReject: 3,
|
|
255
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
176
256
|
}),
|
|
177
257
|
).rejects.toThrow(
|
|
178
258
|
'Illegal issue status for https://github.com/user/repo/issues/1: expected Preparation, but got Done',
|
|
@@ -197,8 +277,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
197
277
|
url: 'https://github.com/user/repo/pull/1',
|
|
198
278
|
isConflicted: false,
|
|
199
279
|
isPassedAllCiJob: true,
|
|
280
|
+
isCiStateSuccess: true,
|
|
200
281
|
isResolvedAllReviewComments: true,
|
|
201
282
|
isBranchOutOfDate: false,
|
|
283
|
+
missingRequiredCheckNames: [],
|
|
202
284
|
},
|
|
203
285
|
]);
|
|
204
286
|
|
|
@@ -209,6 +291,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
209
291
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
210
292
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
211
293
|
thresholdForAutoReject: 3,
|
|
294
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
212
295
|
});
|
|
213
296
|
|
|
214
297
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -225,7 +308,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
225
308
|
);
|
|
226
309
|
});
|
|
227
310
|
|
|
228
|
-
it('should
|
|
311
|
+
it('should reject when last comment does not start with From:', async () => {
|
|
229
312
|
const issue = createMockIssue({
|
|
230
313
|
url: 'https://github.com/user/repo/issues/1',
|
|
231
314
|
status: 'Preparation',
|
|
@@ -241,8 +324,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
241
324
|
url: 'https://github.com/user/repo/pull/1',
|
|
242
325
|
isConflicted: false,
|
|
243
326
|
isPassedAllCiJob: true,
|
|
327
|
+
isCiStateSuccess: true,
|
|
244
328
|
isResolvedAllReviewComments: true,
|
|
245
329
|
isBranchOutOfDate: false,
|
|
330
|
+
missingRequiredCheckNames: [],
|
|
246
331
|
},
|
|
247
332
|
]);
|
|
248
333
|
|
|
@@ -253,14 +338,21 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
253
338
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
254
339
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
255
340
|
thresholdForAutoReject: 3,
|
|
341
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
256
342
|
});
|
|
257
343
|
|
|
258
344
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
259
345
|
expect.objectContaining({
|
|
260
|
-
status: 'Awaiting
|
|
346
|
+
status: 'Awaiting Workspace',
|
|
261
347
|
}),
|
|
262
348
|
mockProject,
|
|
263
349
|
);
|
|
350
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
353
|
+
}),
|
|
354
|
+
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
355
|
+
);
|
|
264
356
|
});
|
|
265
357
|
|
|
266
358
|
it('should reject and set status to Awaiting Workspace when no comments exist', async () => {
|
|
@@ -277,8 +369,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
277
369
|
url: 'https://github.com/user/repo/pull/1',
|
|
278
370
|
isConflicted: false,
|
|
279
371
|
isPassedAllCiJob: true,
|
|
372
|
+
isCiStateSuccess: true,
|
|
280
373
|
isResolvedAllReviewComments: true,
|
|
281
374
|
isBranchOutOfDate: false,
|
|
375
|
+
missingRequiredCheckNames: [],
|
|
282
376
|
},
|
|
283
377
|
]);
|
|
284
378
|
|
|
@@ -289,6 +383,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
289
383
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
290
384
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
291
385
|
thresholdForAutoReject: 3,
|
|
386
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
292
387
|
});
|
|
293
388
|
|
|
294
389
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -321,6 +416,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
321
416
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
322
417
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
323
418
|
thresholdForAutoReject: 3,
|
|
419
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
324
420
|
});
|
|
325
421
|
|
|
326
422
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -356,8 +452,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
356
452
|
url: 'https://github.com/user/repo/pull/1',
|
|
357
453
|
isConflicted: false,
|
|
358
454
|
isPassedAllCiJob: true,
|
|
455
|
+
isCiStateSuccess: true,
|
|
359
456
|
isResolvedAllReviewComments: true,
|
|
360
457
|
isBranchOutOfDate: false,
|
|
458
|
+
missingRequiredCheckNames: [],
|
|
361
459
|
},
|
|
362
460
|
]);
|
|
363
461
|
|
|
@@ -368,6 +466,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
368
466
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
369
467
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
370
468
|
thresholdForAutoReject: 3,
|
|
469
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
371
470
|
});
|
|
372
471
|
|
|
373
472
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -378,6 +477,94 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
378
477
|
);
|
|
379
478
|
});
|
|
380
479
|
|
|
480
|
+
it('should not auto-escalate when retry comment exists even if threshold met', async () => {
|
|
481
|
+
const issue = createMockIssue({
|
|
482
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
483
|
+
status: 'Preparation',
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
487
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
488
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
489
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
490
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
491
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
492
|
+
createMockComment({ content: 'retry' }),
|
|
493
|
+
]);
|
|
494
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
495
|
+
{
|
|
496
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
497
|
+
isConflicted: false,
|
|
498
|
+
isPassedAllCiJob: true,
|
|
499
|
+
isCiStateSuccess: true,
|
|
500
|
+
isResolvedAllReviewComments: true,
|
|
501
|
+
isBranchOutOfDate: false,
|
|
502
|
+
missingRequiredCheckNames: [],
|
|
503
|
+
},
|
|
504
|
+
]);
|
|
505
|
+
|
|
506
|
+
await useCase.run({
|
|
507
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
508
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
509
|
+
preparationStatus: 'Preparation',
|
|
510
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
511
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
512
|
+
thresholdForAutoReject: 3,
|
|
513
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
517
|
+
expect.objectContaining({
|
|
518
|
+
status: 'Awaiting Quality Check',
|
|
519
|
+
}),
|
|
520
|
+
mockProject,
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should handle case-insensitive retry comment', async () => {
|
|
525
|
+
const issue = createMockIssue({
|
|
526
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
527
|
+
status: 'Preparation',
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
531
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
532
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
533
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
534
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
535
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
536
|
+
createMockComment({ content: 'Retry please' }),
|
|
537
|
+
]);
|
|
538
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
539
|
+
{
|
|
540
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
541
|
+
isConflicted: false,
|
|
542
|
+
isPassedAllCiJob: true,
|
|
543
|
+
isCiStateSuccess: true,
|
|
544
|
+
isResolvedAllReviewComments: true,
|
|
545
|
+
isBranchOutOfDate: false,
|
|
546
|
+
missingRequiredCheckNames: [],
|
|
547
|
+
},
|
|
548
|
+
]);
|
|
549
|
+
|
|
550
|
+
await useCase.run({
|
|
551
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
552
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
553
|
+
preparationStatus: 'Preparation',
|
|
554
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
555
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
556
|
+
thresholdForAutoReject: 3,
|
|
557
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
561
|
+
expect.objectContaining({
|
|
562
|
+
status: 'Awaiting Quality Check',
|
|
563
|
+
}),
|
|
564
|
+
mockProject,
|
|
565
|
+
);
|
|
566
|
+
});
|
|
567
|
+
|
|
381
568
|
it('should reject when PR is not found', async () => {
|
|
382
569
|
const issue = createMockIssue({
|
|
383
570
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -398,6 +585,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
398
585
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
399
586
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
400
587
|
thresholdForAutoReject: 3,
|
|
588
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
401
589
|
});
|
|
402
590
|
|
|
403
591
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -430,15 +618,19 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
430
618
|
url: 'https://github.com/user/repo/pull/1',
|
|
431
619
|
isConflicted: false,
|
|
432
620
|
isPassedAllCiJob: true,
|
|
621
|
+
isCiStateSuccess: true,
|
|
433
622
|
isResolvedAllReviewComments: true,
|
|
434
623
|
isBranchOutOfDate: false,
|
|
624
|
+
missingRequiredCheckNames: [],
|
|
435
625
|
},
|
|
436
626
|
{
|
|
437
627
|
url: 'https://github.com/user/repo/pull/2',
|
|
438
628
|
isConflicted: false,
|
|
439
629
|
isPassedAllCiJob: true,
|
|
630
|
+
isCiStateSuccess: true,
|
|
440
631
|
isResolvedAllReviewComments: true,
|
|
441
632
|
isBranchOutOfDate: false,
|
|
633
|
+
missingRequiredCheckNames: [],
|
|
442
634
|
},
|
|
443
635
|
]);
|
|
444
636
|
|
|
@@ -449,6 +641,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
449
641
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
450
642
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
451
643
|
thresholdForAutoReject: 3,
|
|
644
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
452
645
|
});
|
|
453
646
|
|
|
454
647
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -481,8 +674,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
481
674
|
url: 'https://github.com/user/repo/pull/1',
|
|
482
675
|
isConflicted: true,
|
|
483
676
|
isPassedAllCiJob: true,
|
|
677
|
+
isCiStateSuccess: true,
|
|
484
678
|
isResolvedAllReviewComments: true,
|
|
485
679
|
isBranchOutOfDate: false,
|
|
680
|
+
missingRequiredCheckNames: [],
|
|
486
681
|
},
|
|
487
682
|
]);
|
|
488
683
|
|
|
@@ -493,6 +688,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
493
688
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
494
689
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
495
690
|
thresholdForAutoReject: 3,
|
|
691
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
496
692
|
});
|
|
497
693
|
|
|
498
694
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -525,8 +721,110 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
525
721
|
url: 'https://github.com/user/repo/pull/1',
|
|
526
722
|
isConflicted: false,
|
|
527
723
|
isPassedAllCiJob: false,
|
|
724
|
+
isCiStateSuccess: false,
|
|
725
|
+
isResolvedAllReviewComments: true,
|
|
726
|
+
isBranchOutOfDate: false,
|
|
727
|
+
missingRequiredCheckNames: [],
|
|
728
|
+
},
|
|
729
|
+
]);
|
|
730
|
+
|
|
731
|
+
await useCase.run({
|
|
732
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
733
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
734
|
+
preparationStatus: 'Preparation',
|
|
735
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
736
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
737
|
+
thresholdForAutoReject: 3,
|
|
738
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
742
|
+
expect.objectContaining({
|
|
743
|
+
status: 'Awaiting Workspace',
|
|
744
|
+
}),
|
|
745
|
+
mockProject,
|
|
746
|
+
);
|
|
747
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
748
|
+
expect.objectContaining({
|
|
749
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
750
|
+
}),
|
|
751
|
+
expect.stringContaining('ANY_CI_JOB_FAILED_OR_IN_PROGRESS'),
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('should reject with REQUIRED_CI_JOB_NEVER_STARTED when required checks are missing', async () => {
|
|
756
|
+
const issue = createMockIssue({
|
|
757
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
758
|
+
status: 'Preparation',
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
762
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
763
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
764
|
+
createMockComment({ content: 'From: Test report' }),
|
|
765
|
+
]);
|
|
766
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
767
|
+
{
|
|
768
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
769
|
+
isConflicted: false,
|
|
770
|
+
isPassedAllCiJob: false,
|
|
771
|
+
isCiStateSuccess: true,
|
|
772
|
+
isResolvedAllReviewComments: true,
|
|
773
|
+
isBranchOutOfDate: false,
|
|
774
|
+
missingRequiredCheckNames: ['E2E Tests', 'deploy-preview'],
|
|
775
|
+
},
|
|
776
|
+
]);
|
|
777
|
+
|
|
778
|
+
await useCase.run({
|
|
779
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
780
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
781
|
+
preparationStatus: 'Preparation',
|
|
782
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
783
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
784
|
+
thresholdForAutoReject: 3,
|
|
785
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
789
|
+
expect.objectContaining({
|
|
790
|
+
status: 'Awaiting Workspace',
|
|
791
|
+
}),
|
|
792
|
+
mockProject,
|
|
793
|
+
);
|
|
794
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
795
|
+
expect.objectContaining({
|
|
796
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
797
|
+
}),
|
|
798
|
+
expect.stringContaining('REQUIRED_CI_JOB_NEVER_STARTED'),
|
|
799
|
+
);
|
|
800
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
801
|
+
expect.objectContaining({
|
|
802
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
803
|
+
}),
|
|
804
|
+
expect.stringContaining('E2E Tests'),
|
|
805
|
+
);
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it('should reject with ANY_CI_JOB_FAILED_OR_IN_PROGRESS when CI has failures and required checks are also missing', async () => {
|
|
809
|
+
const issue = createMockIssue({
|
|
810
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
811
|
+
status: 'Preparation',
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
815
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
816
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
817
|
+
createMockComment({ content: 'From: Test report' }),
|
|
818
|
+
]);
|
|
819
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
820
|
+
{
|
|
821
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
822
|
+
isConflicted: false,
|
|
823
|
+
isPassedAllCiJob: false,
|
|
824
|
+
isCiStateSuccess: false,
|
|
528
825
|
isResolvedAllReviewComments: true,
|
|
529
826
|
isBranchOutOfDate: false,
|
|
827
|
+
missingRequiredCheckNames: ['deploy-preview'],
|
|
530
828
|
},
|
|
531
829
|
]);
|
|
532
830
|
|
|
@@ -537,6 +835,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
537
835
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
538
836
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
539
837
|
thresholdForAutoReject: 3,
|
|
838
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
540
839
|
});
|
|
541
840
|
|
|
542
841
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -549,7 +848,54 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
549
848
|
expect.objectContaining({
|
|
550
849
|
url: 'https://github.com/user/repo/issues/1',
|
|
551
850
|
}),
|
|
552
|
-
expect.stringContaining('
|
|
851
|
+
expect.stringContaining('ANY_CI_JOB_FAILED_OR_IN_PROGRESS'),
|
|
852
|
+
);
|
|
853
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
854
|
+
expect.objectContaining({
|
|
855
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
856
|
+
}),
|
|
857
|
+
expect.stringContaining('deploy-preview'),
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('should include PR URL in rejection comment details', async () => {
|
|
862
|
+
const issue = createMockIssue({
|
|
863
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
864
|
+
status: 'Preparation',
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
868
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
869
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
870
|
+
createMockComment({ content: 'From: Test report' }),
|
|
871
|
+
]);
|
|
872
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
873
|
+
{
|
|
874
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
875
|
+
isConflicted: false,
|
|
876
|
+
isPassedAllCiJob: false,
|
|
877
|
+
isCiStateSuccess: false,
|
|
878
|
+
isResolvedAllReviewComments: true,
|
|
879
|
+
isBranchOutOfDate: false,
|
|
880
|
+
missingRequiredCheckNames: [],
|
|
881
|
+
},
|
|
882
|
+
]);
|
|
883
|
+
|
|
884
|
+
await useCase.run({
|
|
885
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
886
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
887
|
+
preparationStatus: 'Preparation',
|
|
888
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
889
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
890
|
+
thresholdForAutoReject: 3,
|
|
891
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
895
|
+
expect.objectContaining({
|
|
896
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
897
|
+
}),
|
|
898
|
+
expect.stringContaining('https://github.com/user/repo/pull/1'),
|
|
553
899
|
);
|
|
554
900
|
});
|
|
555
901
|
|
|
@@ -569,8 +915,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
569
915
|
url: 'https://github.com/user/repo/pull/1',
|
|
570
916
|
isConflicted: false,
|
|
571
917
|
isPassedAllCiJob: true,
|
|
918
|
+
isCiStateSuccess: true,
|
|
572
919
|
isResolvedAllReviewComments: false,
|
|
573
920
|
isBranchOutOfDate: false,
|
|
921
|
+
missingRequiredCheckNames: [],
|
|
574
922
|
},
|
|
575
923
|
]);
|
|
576
924
|
|
|
@@ -581,6 +929,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
581
929
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
582
930
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
583
931
|
thresholdForAutoReject: 3,
|
|
932
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
584
933
|
});
|
|
585
934
|
|
|
586
935
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
@@ -617,6 +966,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
617
966
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
618
967
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
619
968
|
thresholdForAutoReject: 3,
|
|
969
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
620
970
|
});
|
|
621
971
|
|
|
622
972
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
@@ -628,6 +978,49 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
628
978
|
);
|
|
629
979
|
});
|
|
630
980
|
|
|
981
|
+
it('should check PRs when issue has category:e2e label', async () => {
|
|
982
|
+
const issue = createMockIssue({
|
|
983
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
984
|
+
status: 'Preparation',
|
|
985
|
+
labels: ['category:e2e'],
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
989
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
990
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
991
|
+
createMockComment({ content: 'From: Test report' }),
|
|
992
|
+
]);
|
|
993
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
994
|
+
{
|
|
995
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
996
|
+
isConflicted: false,
|
|
997
|
+
isPassedAllCiJob: true,
|
|
998
|
+
isCiStateSuccess: true,
|
|
999
|
+
isResolvedAllReviewComments: true,
|
|
1000
|
+
isBranchOutOfDate: false,
|
|
1001
|
+
missingRequiredCheckNames: [],
|
|
1002
|
+
},
|
|
1003
|
+
]);
|
|
1004
|
+
|
|
1005
|
+
await useCase.run({
|
|
1006
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1007
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1008
|
+
preparationStatus: 'Preparation',
|
|
1009
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1010
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1011
|
+
thresholdForAutoReject: 3,
|
|
1012
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
1016
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1017
|
+
expect.objectContaining({
|
|
1018
|
+
status: 'Awaiting Quality Check',
|
|
1019
|
+
}),
|
|
1020
|
+
mockProject,
|
|
1021
|
+
);
|
|
1022
|
+
});
|
|
1023
|
+
|
|
631
1024
|
it('should still check for report comment even when issue has category label', async () => {
|
|
632
1025
|
const issue = createMockIssue({
|
|
633
1026
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -650,6 +1043,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
650
1043
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
651
1044
|
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
652
1045
|
thresholdForAutoReject: 3,
|
|
1046
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
653
1047
|
});
|
|
654
1048
|
|
|
655
1049
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
@@ -663,7 +1057,319 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
663
1057
|
expect.objectContaining({
|
|
664
1058
|
url: 'https://github.com/user/repo/issues/1',
|
|
665
1059
|
}),
|
|
666
|
-
expect.stringContaining('
|
|
1060
|
+
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
667
1061
|
);
|
|
668
1062
|
});
|
|
1063
|
+
|
|
1064
|
+
describe('workflow blocker webhook notification', () => {
|
|
1065
|
+
const createWorkflowBlockerStoryObjectMap = (
|
|
1066
|
+
issueUrl: string,
|
|
1067
|
+
): StoryObjectMap => {
|
|
1068
|
+
const map: StoryObjectMap = new Map();
|
|
1069
|
+
map.set('Workflow Blocker Story', {
|
|
1070
|
+
story: {
|
|
1071
|
+
id: 'story-1',
|
|
1072
|
+
name: 'Workflow Blocker Story',
|
|
1073
|
+
color: 'GRAY',
|
|
1074
|
+
description: '',
|
|
1075
|
+
},
|
|
1076
|
+
storyIssue: null,
|
|
1077
|
+
issues: [createMockIssue({ url: issueUrl })],
|
|
1078
|
+
});
|
|
1079
|
+
return map;
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
const createNonBlockerStoryObjectMap = (): StoryObjectMap => {
|
|
1083
|
+
const map: StoryObjectMap = new Map();
|
|
1084
|
+
map.set('Regular Story', {
|
|
1085
|
+
story: {
|
|
1086
|
+
id: 'story-2',
|
|
1087
|
+
name: 'Regular Story',
|
|
1088
|
+
color: 'GRAY',
|
|
1089
|
+
description: '',
|
|
1090
|
+
},
|
|
1091
|
+
storyIssue: null,
|
|
1092
|
+
issues: [
|
|
1093
|
+
createMockIssue({
|
|
1094
|
+
url: 'https://github.com/user/repo/issues/99',
|
|
1095
|
+
}),
|
|
1096
|
+
],
|
|
1097
|
+
});
|
|
1098
|
+
return map;
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
it('should send webhook when workflow blocker issue status changes to awaitingQualityCheckStatus on checks pass', async () => {
|
|
1102
|
+
const issue = createMockIssue({
|
|
1103
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1104
|
+
status: 'Preparation',
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1108
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1109
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1110
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1111
|
+
]);
|
|
1112
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1113
|
+
{
|
|
1114
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1115
|
+
isConflicted: false,
|
|
1116
|
+
isPassedAllCiJob: true,
|
|
1117
|
+
isCiStateSuccess: true,
|
|
1118
|
+
isResolvedAllReviewComments: true,
|
|
1119
|
+
isBranchOutOfDate: false,
|
|
1120
|
+
missingRequiredCheckNames: [],
|
|
1121
|
+
},
|
|
1122
|
+
]);
|
|
1123
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1124
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1125
|
+
'https://github.com/user/repo/issues/1',
|
|
1126
|
+
),
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
await useCase.run({
|
|
1130
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1131
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1132
|
+
preparationStatus: 'Preparation',
|
|
1133
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1134
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1135
|
+
thresholdForAutoReject: 3,
|
|
1136
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1137
|
+
'https://example.com/webhook?url={URL}&msg={MESSAGE}',
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledWith(
|
|
1141
|
+
`https://example.com/webhook?url=${encodeURIComponent('https://github.com/user/repo/issues/1')}&msg=${encodeURIComponent('Workflow blocker resolved: https://github.com/user/repo/issues/1')}`,
|
|
1142
|
+
);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
it('should send webhook when workflow blocker issue auto-escalates', async () => {
|
|
1146
|
+
const issue = createMockIssue({
|
|
1147
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1148
|
+
status: 'Preparation',
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1152
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1153
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1154
|
+
createMockComment({
|
|
1155
|
+
content: 'Auto Status Check: REJECTED - first',
|
|
1156
|
+
}),
|
|
1157
|
+
createMockComment({
|
|
1158
|
+
content: 'Auto Status Check: REJECTED - second',
|
|
1159
|
+
}),
|
|
1160
|
+
createMockComment({
|
|
1161
|
+
content: 'Auto Status Check: REJECTED - third',
|
|
1162
|
+
}),
|
|
1163
|
+
]);
|
|
1164
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1165
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1166
|
+
'https://github.com/user/repo/issues/1',
|
|
1167
|
+
),
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
await useCase.run({
|
|
1171
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1172
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1173
|
+
preparationStatus: 'Preparation',
|
|
1174
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1175
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1176
|
+
thresholdForAutoReject: 3,
|
|
1177
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1178
|
+
'https://example.com/notify={MESSAGE}',
|
|
1179
|
+
});
|
|
1180
|
+
|
|
1181
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledTimes(1);
|
|
1182
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledWith(
|
|
1183
|
+
expect.stringContaining('https://example.com/notify='),
|
|
1184
|
+
);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
it('should not send webhook for non-blocker issues', async () => {
|
|
1188
|
+
const issue = createMockIssue({
|
|
1189
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1190
|
+
status: 'Preparation',
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1194
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1195
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1196
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1197
|
+
]);
|
|
1198
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1199
|
+
{
|
|
1200
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1201
|
+
isConflicted: false,
|
|
1202
|
+
isPassedAllCiJob: true,
|
|
1203
|
+
isCiStateSuccess: true,
|
|
1204
|
+
isResolvedAllReviewComments: true,
|
|
1205
|
+
isBranchOutOfDate: false,
|
|
1206
|
+
missingRequiredCheckNames: [],
|
|
1207
|
+
},
|
|
1208
|
+
]);
|
|
1209
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1210
|
+
createNonBlockerStoryObjectMap(),
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
await useCase.run({
|
|
1214
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1215
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1216
|
+
preparationStatus: 'Preparation',
|
|
1217
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1218
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1219
|
+
thresholdForAutoReject: 3,
|
|
1220
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1221
|
+
'https://example.com/webhook?msg={MESSAGE}',
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalled();
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
it('should not send webhook when URL is null', async () => {
|
|
1228
|
+
const issue = createMockIssue({
|
|
1229
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1230
|
+
status: 'Preparation',
|
|
1231
|
+
});
|
|
1232
|
+
|
|
1233
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1234
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1235
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1236
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1237
|
+
]);
|
|
1238
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1239
|
+
{
|
|
1240
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1241
|
+
isConflicted: false,
|
|
1242
|
+
isPassedAllCiJob: true,
|
|
1243
|
+
isCiStateSuccess: true,
|
|
1244
|
+
isResolvedAllReviewComments: true,
|
|
1245
|
+
isBranchOutOfDate: false,
|
|
1246
|
+
missingRequiredCheckNames: [],
|
|
1247
|
+
},
|
|
1248
|
+
]);
|
|
1249
|
+
|
|
1250
|
+
await useCase.run({
|
|
1251
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1252
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1253
|
+
preparationStatus: 'Preparation',
|
|
1254
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1255
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1256
|
+
thresholdForAutoReject: 3,
|
|
1257
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
expect(mockIssueRepository.getStoryObjectMap).not.toHaveBeenCalled();
|
|
1261
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalled();
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
it('should log warning and not block workflow when webhook fails', async () => {
|
|
1265
|
+
const issue = createMockIssue({
|
|
1266
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1267
|
+
status: 'Preparation',
|
|
1268
|
+
});
|
|
1269
|
+
|
|
1270
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1271
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1272
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1273
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1274
|
+
]);
|
|
1275
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1276
|
+
{
|
|
1277
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1278
|
+
isConflicted: false,
|
|
1279
|
+
isPassedAllCiJob: true,
|
|
1280
|
+
isCiStateSuccess: true,
|
|
1281
|
+
isResolvedAllReviewComments: true,
|
|
1282
|
+
isBranchOutOfDate: false,
|
|
1283
|
+
missingRequiredCheckNames: [],
|
|
1284
|
+
},
|
|
1285
|
+
]);
|
|
1286
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1287
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1288
|
+
'https://github.com/user/repo/issues/1',
|
|
1289
|
+
),
|
|
1290
|
+
);
|
|
1291
|
+
mockWebhookRepository.sendGetRequest.mockRejectedValue(
|
|
1292
|
+
new Error('Network error'),
|
|
1293
|
+
);
|
|
1294
|
+
|
|
1295
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
1296
|
+
|
|
1297
|
+
await useCase.run({
|
|
1298
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1299
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1300
|
+
preparationStatus: 'Preparation',
|
|
1301
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1302
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1303
|
+
thresholdForAutoReject: 3,
|
|
1304
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1305
|
+
'https://example.com/webhook?msg={MESSAGE}',
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1309
|
+
'Failed to send workflow blocker notification:',
|
|
1310
|
+
expect.any(Error),
|
|
1311
|
+
);
|
|
1312
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1313
|
+
expect.objectContaining({
|
|
1314
|
+
status: 'Awaiting Quality Check',
|
|
1315
|
+
}),
|
|
1316
|
+
mockProject,
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
consoleWarnSpy.mockRestore();
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
it('should URL-encode placeholders in webhook URL', async () => {
|
|
1323
|
+
const issue = createMockIssue({
|
|
1324
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1325
|
+
status: 'Preparation',
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1329
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1330
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1331
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1332
|
+
]);
|
|
1333
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1334
|
+
{
|
|
1335
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1336
|
+
isConflicted: false,
|
|
1337
|
+
isPassedAllCiJob: true,
|
|
1338
|
+
isCiStateSuccess: true,
|
|
1339
|
+
isResolvedAllReviewComments: true,
|
|
1340
|
+
isBranchOutOfDate: false,
|
|
1341
|
+
missingRequiredCheckNames: [],
|
|
1342
|
+
},
|
|
1343
|
+
]);
|
|
1344
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1345
|
+
createWorkflowBlockerStoryObjectMap(
|
|
1346
|
+
'https://github.com/user/repo/issues/1',
|
|
1347
|
+
),
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
await useCase.run({
|
|
1351
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1352
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1353
|
+
preparationStatus: 'Preparation',
|
|
1354
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1355
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1356
|
+
thresholdForAutoReject: 3,
|
|
1357
|
+
workflowBlockerResolvedWebhookUrl:
|
|
1358
|
+
'https://example.com/runTasker/notify=:={MESSAGE}',
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledTimes(1);
|
|
1362
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalledWith(
|
|
1363
|
+
expect.stringContaining('{MESSAGE}'),
|
|
1364
|
+
);
|
|
1365
|
+
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalledWith(
|
|
1366
|
+
expect.stringContaining('{URL}'),
|
|
1367
|
+
);
|
|
1368
|
+
expect(mockWebhookRepository.sendGetRequest).toHaveBeenCalledWith(
|
|
1369
|
+
expect.stringContaining(
|
|
1370
|
+
encodeURIComponent('Workflow blocker resolved:'),
|
|
1371
|
+
),
|
|
1372
|
+
);
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
669
1375
|
});
|