github-issue-tower-defence-management 1.39.0 → 1.41.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/.github/workflows/create-pr.yml +12 -5
- package/.github/workflows/umino-project.yml +5 -4
- package/CHANGELOG.md +26 -0
- package/README.md +21 -9
- package/bin/adapter/entry-points/cli/index.js +45 -10
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +32 -8
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
- package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +446 -11
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -2
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +111 -16
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +107 -72
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +26 -13
- package/src/adapter/entry-points/cli/index.ts +74 -13
- package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
- package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +689 -12
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -3
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +983 -167
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +22 -9
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1696 -290
- package/src/domain/usecases/StartPreparationUseCase.ts +171 -126
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -4
- package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts +4 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
- package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +7 -6
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -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/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -18
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -3
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -1
|
@@ -46,6 +46,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
|
46
46
|
isInProgress: false,
|
|
47
47
|
isClosed: false,
|
|
48
48
|
createdAt: new Date(),
|
|
49
|
+
author: '',
|
|
49
50
|
...overrides,
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -65,8 +66,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
65
66
|
let mockIssueRepository: {
|
|
66
67
|
get: jest.Mock;
|
|
67
68
|
update: jest.Mock;
|
|
69
|
+
updateNextActionDate: jest.Mock;
|
|
68
70
|
findRelatedOpenPRs: jest.Mock;
|
|
69
71
|
getStoryObjectMap: jest.Mock;
|
|
72
|
+
getOpenPullRequest: jest.Mock;
|
|
70
73
|
};
|
|
71
74
|
let mockIssueCommentRepository: {
|
|
72
75
|
getCommentsFromIssue: jest.Mock;
|
|
@@ -92,10 +95,12 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
mockIssueRepository = {
|
|
95
|
-
getStoryObjectMap: jest.fn(),
|
|
98
|
+
getStoryObjectMap: jest.fn().mockResolvedValue(new Map()),
|
|
96
99
|
get: jest.fn(),
|
|
97
100
|
update: jest.fn(),
|
|
101
|
+
updateNextActionDate: jest.fn(),
|
|
98
102
|
findRelatedOpenPRs: jest.fn(),
|
|
103
|
+
getOpenPullRequest: jest.fn(),
|
|
99
104
|
};
|
|
100
105
|
|
|
101
106
|
mockIssueCommentRepository = {
|
|
@@ -174,6 +179,9 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
174
179
|
});
|
|
175
180
|
|
|
176
181
|
it('should update issue status from Preparation to Awaiting Quality Check when last comment starts with From:', async () => {
|
|
182
|
+
jest.useFakeTimers();
|
|
183
|
+
jest.setSystemTime(new Date('2026-01-01T00:00:00Z'));
|
|
184
|
+
|
|
177
185
|
const issue = createMockIssue({
|
|
178
186
|
url: 'https://github.com/user/repo/issues/1',
|
|
179
187
|
status: 'Preparation',
|
|
@@ -214,6 +222,65 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
214
222
|
}),
|
|
215
223
|
mockProject,
|
|
216
224
|
);
|
|
225
|
+
|
|
226
|
+
const expectedNextActionDate = new Date('2026-01-01T00:00:00Z');
|
|
227
|
+
expectedNextActionDate.setMonth(expectedNextActionDate.getMonth() + 1);
|
|
228
|
+
expect(mockIssueRepository.updateNextActionDate).toHaveBeenCalledWith(
|
|
229
|
+
'https://github.com/user/repo/pull/1',
|
|
230
|
+
mockProject,
|
|
231
|
+
expectedNextActionDate,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
jest.useRealTimers();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should set PR next action date to 1 month from now when approved', async () => {
|
|
238
|
+
jest.useFakeTimers();
|
|
239
|
+
const now = new Date('2026-03-15T12:00:00Z');
|
|
240
|
+
jest.setSystemTime(now);
|
|
241
|
+
|
|
242
|
+
const issue = createMockIssue({
|
|
243
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
244
|
+
status: 'Preparation',
|
|
245
|
+
});
|
|
246
|
+
const prUrl = 'https://github.com/user/repo/pull/42';
|
|
247
|
+
|
|
248
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
249
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
250
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
251
|
+
createMockComment({ content: 'From: Agent report' }),
|
|
252
|
+
]);
|
|
253
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
254
|
+
{
|
|
255
|
+
url: prUrl,
|
|
256
|
+
isConflicted: false,
|
|
257
|
+
isPassedAllCiJob: true,
|
|
258
|
+
isCiStateSuccess: true,
|
|
259
|
+
isResolvedAllReviewComments: true,
|
|
260
|
+
isBranchOutOfDate: false,
|
|
261
|
+
missingRequiredCheckNames: [],
|
|
262
|
+
},
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
await useCase.run({
|
|
266
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
267
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
268
|
+
preparationStatus: 'Preparation',
|
|
269
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
270
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
271
|
+
thresholdForAutoReject: 3,
|
|
272
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const expectedDate = new Date(now);
|
|
276
|
+
expectedDate.setMonth(expectedDate.getMonth() + 1);
|
|
277
|
+
expect(mockIssueRepository.updateNextActionDate).toHaveBeenCalledWith(
|
|
278
|
+
prUrl,
|
|
279
|
+
mockProject,
|
|
280
|
+
expectedDate,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
jest.useRealTimers();
|
|
217
284
|
});
|
|
218
285
|
|
|
219
286
|
it('should throw IssueNotFoundError when issue does not exist', async () => {
|
|
@@ -259,30 +326,18 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
259
326
|
);
|
|
260
327
|
});
|
|
261
328
|
|
|
262
|
-
it('should
|
|
329
|
+
it('should set status to Awaiting Workspace when issue has dependent issue URLs', async () => {
|
|
263
330
|
const issue = createMockIssue({
|
|
264
331
|
url: 'https://github.com/user/repo/issues/1',
|
|
265
332
|
status: 'Preparation',
|
|
333
|
+
dependedIssueUrls: [
|
|
334
|
+
'https://github.com/user/repo/issues/2',
|
|
335
|
+
'https://github.com/user/repo/issues/3',
|
|
336
|
+
],
|
|
266
337
|
});
|
|
267
338
|
|
|
268
339
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
269
340
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
270
|
-
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
271
|
-
createMockComment({
|
|
272
|
-
content: 'Auto Status Check: REJECTED\n["NO_REPORT"]',
|
|
273
|
-
}),
|
|
274
|
-
]);
|
|
275
|
-
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
276
|
-
{
|
|
277
|
-
url: 'https://github.com/user/repo/pull/1',
|
|
278
|
-
isConflicted: false,
|
|
279
|
-
isPassedAllCiJob: true,
|
|
280
|
-
isCiStateSuccess: true,
|
|
281
|
-
isResolvedAllReviewComments: true,
|
|
282
|
-
isBranchOutOfDate: false,
|
|
283
|
-
missingRequiredCheckNames: [],
|
|
284
|
-
},
|
|
285
|
-
]);
|
|
286
341
|
|
|
287
342
|
await useCase.run({
|
|
288
343
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -295,41 +350,44 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
295
350
|
});
|
|
296
351
|
|
|
297
352
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
298
|
-
expect.objectContaining({
|
|
299
|
-
status: 'Awaiting Workspace',
|
|
300
|
-
}),
|
|
353
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
301
354
|
mockProject,
|
|
302
355
|
);
|
|
303
356
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
304
|
-
expect.objectContaining({
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
357
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
358
|
+
expect.stringContaining(
|
|
359
|
+
'Issue has dependent issue URLs: https://github.com/user/repo/issues/2, https://github.com/user/repo/issues/3',
|
|
360
|
+
),
|
|
308
361
|
);
|
|
309
362
|
});
|
|
310
363
|
|
|
311
|
-
it('should
|
|
364
|
+
it('should enrich dependedIssueUrls from storyObjectMap when issue has none', async () => {
|
|
312
365
|
const issue = createMockIssue({
|
|
313
366
|
url: 'https://github.com/user/repo/issues/1',
|
|
314
367
|
status: 'Preparation',
|
|
368
|
+
dependedIssueUrls: [],
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const storyObjectMap: StoryObjectMap = new Map();
|
|
372
|
+
storyObjectMap.set('Some Story', {
|
|
373
|
+
story: {
|
|
374
|
+
id: 'story-1',
|
|
375
|
+
name: 'Some Story',
|
|
376
|
+
color: 'GRAY',
|
|
377
|
+
description: '',
|
|
378
|
+
},
|
|
379
|
+
storyIssue: null,
|
|
380
|
+
issues: [
|
|
381
|
+
createMockIssue({
|
|
382
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
383
|
+
dependedIssueUrls: ['https://github.com/user/repo/issues/5'],
|
|
384
|
+
}),
|
|
385
|
+
],
|
|
315
386
|
});
|
|
316
387
|
|
|
317
388
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
318
389
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
319
|
-
|
|
320
|
-
createMockComment({ content: 'Some other comment' }),
|
|
321
|
-
]);
|
|
322
|
-
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
323
|
-
{
|
|
324
|
-
url: 'https://github.com/user/repo/pull/1',
|
|
325
|
-
isConflicted: false,
|
|
326
|
-
isPassedAllCiJob: true,
|
|
327
|
-
isCiStateSuccess: true,
|
|
328
|
-
isResolvedAllReviewComments: true,
|
|
329
|
-
isBranchOutOfDate: false,
|
|
330
|
-
missingRequiredCheckNames: [],
|
|
331
|
-
},
|
|
332
|
-
]);
|
|
390
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(storyObjectMap);
|
|
333
391
|
|
|
334
392
|
await useCase.run({
|
|
335
393
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -342,39 +400,24 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
342
400
|
});
|
|
343
401
|
|
|
344
402
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
345
|
-
expect.objectContaining({
|
|
346
|
-
status: 'Awaiting Workspace',
|
|
347
|
-
}),
|
|
403
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
348
404
|
mockProject,
|
|
349
405
|
);
|
|
350
406
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
351
|
-
expect.objectContaining({
|
|
352
|
-
|
|
353
|
-
}),
|
|
354
|
-
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
407
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
408
|
+
expect.stringContaining('Issue has dependent issue URLs:'),
|
|
355
409
|
);
|
|
356
410
|
});
|
|
357
411
|
|
|
358
|
-
it('should
|
|
412
|
+
it('should set status to Awaiting Workspace when issue has nextActionDate set', async () => {
|
|
359
413
|
const issue = createMockIssue({
|
|
360
414
|
url: 'https://github.com/user/repo/issues/1',
|
|
361
415
|
status: 'Preparation',
|
|
416
|
+
nextActionDate: new Date('2026-12-01'),
|
|
362
417
|
});
|
|
363
418
|
|
|
364
419
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
365
420
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
366
|
-
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
367
|
-
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
368
|
-
{
|
|
369
|
-
url: 'https://github.com/user/repo/pull/1',
|
|
370
|
-
isConflicted: false,
|
|
371
|
-
isPassedAllCiJob: true,
|
|
372
|
-
isCiStateSuccess: true,
|
|
373
|
-
isResolvedAllReviewComments: true,
|
|
374
|
-
isBranchOutOfDate: false,
|
|
375
|
-
missingRequiredCheckNames: [],
|
|
376
|
-
},
|
|
377
|
-
]);
|
|
378
421
|
|
|
379
422
|
await useCase.run({
|
|
380
423
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -387,27 +430,24 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
387
430
|
});
|
|
388
431
|
|
|
389
432
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
390
|
-
expect.objectContaining({
|
|
391
|
-
status: 'Awaiting Workspace',
|
|
392
|
-
}),
|
|
433
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
393
434
|
mockProject,
|
|
394
435
|
);
|
|
395
|
-
expect(mockIssueCommentRepository.createComment).
|
|
436
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
437
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
438
|
+
expect.stringContaining('Issue has next action date or hour set:'),
|
|
439
|
+
);
|
|
396
440
|
});
|
|
397
441
|
|
|
398
|
-
it('should
|
|
442
|
+
it('should set status to Awaiting Workspace when issue has nextActionHour set', async () => {
|
|
399
443
|
const issue = createMockIssue({
|
|
400
444
|
url: 'https://github.com/user/repo/issues/1',
|
|
401
445
|
status: 'Preparation',
|
|
446
|
+
nextActionHour: 9,
|
|
402
447
|
});
|
|
403
448
|
|
|
404
449
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
405
450
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
406
|
-
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
407
|
-
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
408
|
-
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
409
|
-
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
410
|
-
]);
|
|
411
451
|
|
|
412
452
|
await useCase.run({
|
|
413
453
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -420,22 +460,16 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
420
460
|
});
|
|
421
461
|
|
|
422
462
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
423
|
-
expect.objectContaining({
|
|
424
|
-
status: 'Awaiting Quality Check',
|
|
425
|
-
}),
|
|
463
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
426
464
|
mockProject,
|
|
427
465
|
);
|
|
428
466
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
429
|
-
expect.objectContaining({
|
|
430
|
-
|
|
431
|
-
}),
|
|
432
|
-
expect.stringContaining(
|
|
433
|
-
'Failed to pass the check autimatically for 3 times',
|
|
434
|
-
),
|
|
467
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
468
|
+
expect.stringContaining('nextActionHour=9'),
|
|
435
469
|
);
|
|
436
470
|
});
|
|
437
471
|
|
|
438
|
-
it('should
|
|
472
|
+
it('should reject and set status to Awaiting Workspace when last comment starts with Auto Status Check:', async () => {
|
|
439
473
|
const issue = createMockIssue({
|
|
440
474
|
url: 'https://github.com/user/repo/issues/1',
|
|
441
475
|
status: 'Preparation',
|
|
@@ -444,8 +478,9 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
444
478
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
445
479
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
446
480
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
447
|
-
createMockComment({
|
|
448
|
-
|
|
481
|
+
createMockComment({
|
|
482
|
+
content: 'Auto Status Check: REJECTED\n["NO_REPORT"]',
|
|
483
|
+
}),
|
|
449
484
|
]);
|
|
450
485
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
451
486
|
{
|
|
@@ -475,9 +510,15 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
475
510
|
}),
|
|
476
511
|
mockProject,
|
|
477
512
|
);
|
|
513
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
514
|
+
expect.objectContaining({
|
|
515
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
516
|
+
}),
|
|
517
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
518
|
+
);
|
|
478
519
|
});
|
|
479
520
|
|
|
480
|
-
it('should
|
|
521
|
+
it('should reject when last comment does not start with From:', async () => {
|
|
481
522
|
const issue = createMockIssue({
|
|
482
523
|
url: 'https://github.com/user/repo/issues/1',
|
|
483
524
|
status: 'Preparation',
|
|
@@ -486,10 +527,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
486
527
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
487
528
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
488
529
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
489
|
-
createMockComment({ content: '
|
|
490
|
-
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
491
|
-
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
492
|
-
createMockComment({ content: 'retry' }),
|
|
530
|
+
createMockComment({ content: 'Some other comment' }),
|
|
493
531
|
]);
|
|
494
532
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
495
533
|
{
|
|
@@ -513,15 +551,21 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
513
551
|
workflowBlockerResolvedWebhookUrl: null,
|
|
514
552
|
});
|
|
515
553
|
|
|
516
|
-
expect(mockIssueRepository.update).
|
|
554
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
517
555
|
expect.objectContaining({
|
|
518
|
-
status: 'Awaiting
|
|
556
|
+
status: 'Awaiting Workspace',
|
|
519
557
|
}),
|
|
520
558
|
mockProject,
|
|
521
559
|
);
|
|
560
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
561
|
+
expect.objectContaining({
|
|
562
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
563
|
+
}),
|
|
564
|
+
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
565
|
+
);
|
|
522
566
|
});
|
|
523
567
|
|
|
524
|
-
it('should
|
|
568
|
+
it('should reject and set status to Awaiting Workspace when no comments exist', async () => {
|
|
525
569
|
const issue = createMockIssue({
|
|
526
570
|
url: 'https://github.com/user/repo/issues/1',
|
|
527
571
|
status: 'Preparation',
|
|
@@ -529,12 +573,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
529
573
|
|
|
530
574
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
531
575
|
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
|
-
]);
|
|
576
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
538
577
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
539
578
|
{
|
|
540
579
|
url: 'https://github.com/user/repo/pull/1',
|
|
@@ -557,15 +596,16 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
557
596
|
workflowBlockerResolvedWebhookUrl: null,
|
|
558
597
|
});
|
|
559
598
|
|
|
560
|
-
expect(mockIssueRepository.update).
|
|
599
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
561
600
|
expect.objectContaining({
|
|
562
|
-
status: 'Awaiting
|
|
601
|
+
status: 'Awaiting Workspace',
|
|
563
602
|
}),
|
|
564
603
|
mockProject,
|
|
565
604
|
);
|
|
605
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalled();
|
|
566
606
|
});
|
|
567
607
|
|
|
568
|
-
it('should reject when
|
|
608
|
+
it('should reject when last comment has REPORT_HAS_NEXT_STEP', async () => {
|
|
569
609
|
const issue = createMockIssue({
|
|
570
610
|
url: 'https://github.com/user/repo/issues/1',
|
|
571
611
|
status: 'Preparation',
|
|
@@ -574,9 +614,22 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
574
614
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
575
615
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
576
616
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
577
|
-
createMockComment({
|
|
617
|
+
createMockComment({
|
|
618
|
+
content:
|
|
619
|
+
'From: Agent report\n```json\n{"nextStep": "Fix the tests"}\n```',
|
|
620
|
+
}),
|
|
621
|
+
]);
|
|
622
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
623
|
+
{
|
|
624
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
625
|
+
isConflicted: false,
|
|
626
|
+
isPassedAllCiJob: true,
|
|
627
|
+
isCiStateSuccess: true,
|
|
628
|
+
isResolvedAllReviewComments: true,
|
|
629
|
+
isBranchOutOfDate: false,
|
|
630
|
+
missingRequiredCheckNames: [],
|
|
631
|
+
},
|
|
578
632
|
]);
|
|
579
|
-
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
580
633
|
|
|
581
634
|
await useCase.run({
|
|
582
635
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -589,20 +642,16 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
589
642
|
});
|
|
590
643
|
|
|
591
644
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
592
|
-
expect.objectContaining({
|
|
593
|
-
status: 'Awaiting Workspace',
|
|
594
|
-
}),
|
|
645
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
595
646
|
mockProject,
|
|
596
647
|
);
|
|
597
648
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
598
|
-
expect.objectContaining({
|
|
599
|
-
|
|
600
|
-
}),
|
|
601
|
-
expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
|
|
649
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
650
|
+
expect.stringContaining('REPORT_HAS_NEXT_STEP'),
|
|
602
651
|
);
|
|
603
652
|
});
|
|
604
653
|
|
|
605
|
-
it('should reject when
|
|
654
|
+
it('should not reject when last comment has nextStep set to null', async () => {
|
|
606
655
|
const issue = createMockIssue({
|
|
607
656
|
url: 'https://github.com/user/repo/issues/1',
|
|
608
657
|
status: 'Preparation',
|
|
@@ -611,7 +660,9 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
611
660
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
612
661
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
613
662
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
614
|
-
createMockComment({
|
|
663
|
+
createMockComment({
|
|
664
|
+
content: 'From: Agent report\n```json\n{"nextStep": null}\n```',
|
|
665
|
+
}),
|
|
615
666
|
]);
|
|
616
667
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
617
668
|
{
|
|
@@ -623,15 +674,6 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
623
674
|
isBranchOutOfDate: false,
|
|
624
675
|
missingRequiredCheckNames: [],
|
|
625
676
|
},
|
|
626
|
-
{
|
|
627
|
-
url: 'https://github.com/user/repo/pull/2',
|
|
628
|
-
isConflicted: false,
|
|
629
|
-
isPassedAllCiJob: true,
|
|
630
|
-
isCiStateSuccess: true,
|
|
631
|
-
isResolvedAllReviewComments: true,
|
|
632
|
-
isBranchOutOfDate: false,
|
|
633
|
-
missingRequiredCheckNames: [],
|
|
634
|
-
},
|
|
635
677
|
]);
|
|
636
678
|
|
|
637
679
|
await useCase.run({
|
|
@@ -645,20 +687,12 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
645
687
|
});
|
|
646
688
|
|
|
647
689
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
648
|
-
expect.objectContaining({
|
|
649
|
-
status: 'Awaiting Workspace',
|
|
650
|
-
}),
|
|
690
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
651
691
|
mockProject,
|
|
652
692
|
);
|
|
653
|
-
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
654
|
-
expect.objectContaining({
|
|
655
|
-
url: 'https://github.com/user/repo/issues/1',
|
|
656
|
-
}),
|
|
657
|
-
expect.stringContaining('MULTIPLE_PULL_REQUESTS_FOUND'),
|
|
658
|
-
);
|
|
659
693
|
});
|
|
660
694
|
|
|
661
|
-
it('should
|
|
695
|
+
it('should auto-escalate to Awaiting Quality Check after threshold rejections', async () => {
|
|
662
696
|
const issue = createMockIssue({
|
|
663
697
|
url: 'https://github.com/user/repo/issues/1',
|
|
664
698
|
status: 'Preparation',
|
|
@@ -667,19 +701,11 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
667
701
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
668
702
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
669
703
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
670
|
-
createMockComment({ content: '
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
{
|
|
674
|
-
url: 'https://github.com/user/repo/pull/1',
|
|
675
|
-
isConflicted: true,
|
|
676
|
-
isPassedAllCiJob: true,
|
|
677
|
-
isCiStateSuccess: true,
|
|
678
|
-
isResolvedAllReviewComments: true,
|
|
679
|
-
isBranchOutOfDate: false,
|
|
680
|
-
missingRequiredCheckNames: [],
|
|
681
|
-
},
|
|
704
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
705
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
706
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
682
707
|
]);
|
|
708
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
683
709
|
|
|
684
710
|
await useCase.run({
|
|
685
711
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
@@ -693,7 +719,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
693
719
|
|
|
694
720
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
695
721
|
expect.objectContaining({
|
|
696
|
-
status: 'Awaiting
|
|
722
|
+
status: 'Awaiting Quality Check',
|
|
697
723
|
}),
|
|
698
724
|
mockProject,
|
|
699
725
|
);
|
|
@@ -701,27 +727,43 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
701
727
|
expect.objectContaining({
|
|
702
728
|
url: 'https://github.com/user/repo/issues/1',
|
|
703
729
|
}),
|
|
704
|
-
expect.stringContaining('
|
|
730
|
+
expect.stringContaining('Auto Status Check:'),
|
|
731
|
+
);
|
|
732
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
733
|
+
expect.objectContaining({
|
|
734
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
735
|
+
}),
|
|
736
|
+
expect.stringContaining(
|
|
737
|
+
'Failed to pass the check automatically for 3 times',
|
|
738
|
+
),
|
|
705
739
|
);
|
|
706
740
|
});
|
|
707
741
|
|
|
708
|
-
it('should
|
|
742
|
+
it('should use APPROVED escalation wording and set PR next action date when current check passes but threshold is met', async () => {
|
|
743
|
+
jest.useFakeTimers();
|
|
744
|
+
const now = new Date('2026-02-01T00:00:00Z');
|
|
745
|
+
jest.setSystemTime(now);
|
|
746
|
+
|
|
709
747
|
const issue = createMockIssue({
|
|
710
748
|
url: 'https://github.com/user/repo/issues/1',
|
|
711
749
|
status: 'Preparation',
|
|
712
750
|
});
|
|
751
|
+
const prUrl = 'https://github.com/user/repo/pull/1';
|
|
713
752
|
|
|
714
753
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
715
754
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
716
755
|
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
717
|
-
createMockComment({ content: '
|
|
756
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
757
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
758
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
759
|
+
createMockComment({ content: 'From: Agent final report' }),
|
|
718
760
|
]);
|
|
719
761
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
720
762
|
{
|
|
721
|
-
url:
|
|
763
|
+
url: prUrl,
|
|
722
764
|
isConflicted: false,
|
|
723
|
-
isPassedAllCiJob:
|
|
724
|
-
isCiStateSuccess:
|
|
765
|
+
isPassedAllCiJob: true,
|
|
766
|
+
isCiStateSuccess: true,
|
|
725
767
|
isResolvedAllReviewComments: true,
|
|
726
768
|
isBranchOutOfDate: false,
|
|
727
769
|
missingRequiredCheckNames: [],
|
|
@@ -739,15 +781,398 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
739
781
|
});
|
|
740
782
|
|
|
741
783
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
742
|
-
expect.objectContaining({
|
|
743
|
-
status: 'Awaiting Workspace',
|
|
744
|
-
}),
|
|
784
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
745
785
|
mockProject,
|
|
746
786
|
);
|
|
747
787
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
748
|
-
expect.objectContaining({
|
|
749
|
-
|
|
750
|
-
|
|
788
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
789
|
+
expect.stringContaining(
|
|
790
|
+
'Auto Status Check: APPROVED (escalated due to prior failures)',
|
|
791
|
+
),
|
|
792
|
+
);
|
|
793
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
794
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
795
|
+
expect.stringContaining(
|
|
796
|
+
'Failed to pass the check automatically for 3 times',
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
const expectedDate = new Date(now);
|
|
800
|
+
expectedDate.setMonth(expectedDate.getMonth() + 1);
|
|
801
|
+
expect(mockIssueRepository.updateNextActionDate).toHaveBeenCalledWith(
|
|
802
|
+
prUrl,
|
|
803
|
+
mockProject,
|
|
804
|
+
expectedDate,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
jest.useRealTimers();
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('should not auto-escalate when rejections are below threshold', async () => {
|
|
811
|
+
const issue = createMockIssue({
|
|
812
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
813
|
+
status: 'Preparation',
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
817
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
818
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
819
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
820
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
821
|
+
]);
|
|
822
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
823
|
+
{
|
|
824
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
825
|
+
isConflicted: false,
|
|
826
|
+
isPassedAllCiJob: true,
|
|
827
|
+
isCiStateSuccess: true,
|
|
828
|
+
isResolvedAllReviewComments: true,
|
|
829
|
+
isBranchOutOfDate: false,
|
|
830
|
+
missingRequiredCheckNames: [],
|
|
831
|
+
},
|
|
832
|
+
]);
|
|
833
|
+
|
|
834
|
+
await useCase.run({
|
|
835
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
836
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
837
|
+
preparationStatus: 'Preparation',
|
|
838
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
839
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
840
|
+
thresholdForAutoReject: 3,
|
|
841
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
845
|
+
expect.objectContaining({
|
|
846
|
+
status: 'Awaiting Workspace',
|
|
847
|
+
}),
|
|
848
|
+
mockProject,
|
|
849
|
+
);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('should not auto-escalate when failed-to-pass-check comment exists even if threshold met', async () => {
|
|
853
|
+
const issue = createMockIssue({
|
|
854
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
855
|
+
status: 'Preparation',
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
859
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
860
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
861
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
862
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
863
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
864
|
+
createMockComment({
|
|
865
|
+
content:
|
|
866
|
+
'Auto Status Check: REJECTED\n\nFailed to pass the check automatically for 3 times',
|
|
867
|
+
}),
|
|
868
|
+
]);
|
|
869
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
870
|
+
{
|
|
871
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
872
|
+
isConflicted: false,
|
|
873
|
+
isPassedAllCiJob: true,
|
|
874
|
+
isCiStateSuccess: true,
|
|
875
|
+
isResolvedAllReviewComments: true,
|
|
876
|
+
isBranchOutOfDate: false,
|
|
877
|
+
missingRequiredCheckNames: [],
|
|
878
|
+
},
|
|
879
|
+
]);
|
|
880
|
+
|
|
881
|
+
await useCase.run({
|
|
882
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
883
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
884
|
+
preparationStatus: 'Preparation',
|
|
885
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
886
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
887
|
+
thresholdForAutoReject: 3,
|
|
888
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
892
|
+
expect.objectContaining({
|
|
893
|
+
status: 'Awaiting Quality Check',
|
|
894
|
+
}),
|
|
895
|
+
mockProject,
|
|
896
|
+
);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('should handle case-insensitive failed-to-pass-check comment', async () => {
|
|
900
|
+
const issue = createMockIssue({
|
|
901
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
902
|
+
status: 'Preparation',
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
906
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
907
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
908
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
909
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
910
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
911
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - fourth' }),
|
|
912
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - fifth' }),
|
|
913
|
+
createMockComment({
|
|
914
|
+
content:
|
|
915
|
+
'AUTO STATUS CHECK: APPROVED\n\nFailed to pass the check automatically for 5 times',
|
|
916
|
+
}),
|
|
917
|
+
]);
|
|
918
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
919
|
+
{
|
|
920
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
921
|
+
isConflicted: false,
|
|
922
|
+
isPassedAllCiJob: true,
|
|
923
|
+
isCiStateSuccess: true,
|
|
924
|
+
isResolvedAllReviewComments: true,
|
|
925
|
+
isBranchOutOfDate: false,
|
|
926
|
+
missingRequiredCheckNames: [],
|
|
927
|
+
},
|
|
928
|
+
]);
|
|
929
|
+
|
|
930
|
+
await useCase.run({
|
|
931
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
932
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
933
|
+
preparationStatus: 'Preparation',
|
|
934
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
935
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
936
|
+
thresholdForAutoReject: 3,
|
|
937
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
941
|
+
expect.objectContaining({
|
|
942
|
+
status: 'Awaiting Quality Check',
|
|
943
|
+
}),
|
|
944
|
+
mockProject,
|
|
945
|
+
);
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it('should not auto-escalate when new-format escalation comment with Auto Status Check prefix exists', async () => {
|
|
949
|
+
const issue = createMockIssue({
|
|
950
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
951
|
+
status: 'Preparation',
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
955
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
956
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
957
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
|
|
958
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
|
|
959
|
+
createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
|
|
960
|
+
createMockComment({
|
|
961
|
+
content:
|
|
962
|
+
'Auto Status Check: APPROVED (escalated due to prior failures)\n\nFailed to pass the check automatically for 3 times',
|
|
963
|
+
}),
|
|
964
|
+
]);
|
|
965
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
966
|
+
{
|
|
967
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
968
|
+
isConflicted: false,
|
|
969
|
+
isPassedAllCiJob: true,
|
|
970
|
+
isCiStateSuccess: true,
|
|
971
|
+
isResolvedAllReviewComments: true,
|
|
972
|
+
isBranchOutOfDate: false,
|
|
973
|
+
missingRequiredCheckNames: [],
|
|
974
|
+
},
|
|
975
|
+
]);
|
|
976
|
+
|
|
977
|
+
await useCase.run({
|
|
978
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
979
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
980
|
+
preparationStatus: 'Preparation',
|
|
981
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
982
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
983
|
+
thresholdForAutoReject: 3,
|
|
984
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
988
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
989
|
+
mockProject,
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it('should reject when PR is not found', async () => {
|
|
994
|
+
const issue = createMockIssue({
|
|
995
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
996
|
+
status: 'Preparation',
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1000
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1001
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1002
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1003
|
+
]);
|
|
1004
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
1005
|
+
|
|
1006
|
+
await useCase.run({
|
|
1007
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1008
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1009
|
+
preparationStatus: 'Preparation',
|
|
1010
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1011
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1012
|
+
thresholdForAutoReject: 3,
|
|
1013
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1017
|
+
expect.objectContaining({
|
|
1018
|
+
status: 'Awaiting Workspace',
|
|
1019
|
+
}),
|
|
1020
|
+
mockProject,
|
|
1021
|
+
);
|
|
1022
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1023
|
+
expect.objectContaining({
|
|
1024
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1025
|
+
}),
|
|
1026
|
+
expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
|
|
1027
|
+
);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
it('should reject when multiple PRs are found', async () => {
|
|
1031
|
+
const issue = createMockIssue({
|
|
1032
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1033
|
+
status: 'Preparation',
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1037
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1038
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1039
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1040
|
+
]);
|
|
1041
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1042
|
+
{
|
|
1043
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1044
|
+
isConflicted: false,
|
|
1045
|
+
isPassedAllCiJob: true,
|
|
1046
|
+
isCiStateSuccess: true,
|
|
1047
|
+
isResolvedAllReviewComments: true,
|
|
1048
|
+
isBranchOutOfDate: false,
|
|
1049
|
+
missingRequiredCheckNames: [],
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
url: 'https://github.com/user/repo/pull/2',
|
|
1053
|
+
isConflicted: false,
|
|
1054
|
+
isPassedAllCiJob: true,
|
|
1055
|
+
isCiStateSuccess: true,
|
|
1056
|
+
isResolvedAllReviewComments: true,
|
|
1057
|
+
isBranchOutOfDate: false,
|
|
1058
|
+
missingRequiredCheckNames: [],
|
|
1059
|
+
},
|
|
1060
|
+
]);
|
|
1061
|
+
|
|
1062
|
+
await useCase.run({
|
|
1063
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1064
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1065
|
+
preparationStatus: 'Preparation',
|
|
1066
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1067
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1068
|
+
thresholdForAutoReject: 3,
|
|
1069
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1073
|
+
expect.objectContaining({
|
|
1074
|
+
status: 'Awaiting Workspace',
|
|
1075
|
+
}),
|
|
1076
|
+
mockProject,
|
|
1077
|
+
);
|
|
1078
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1079
|
+
expect.objectContaining({
|
|
1080
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1081
|
+
}),
|
|
1082
|
+
expect.stringContaining('MULTIPLE_PULL_REQUESTS_FOUND'),
|
|
1083
|
+
);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it('should reject when PR is conflicted', async () => {
|
|
1087
|
+
const issue = createMockIssue({
|
|
1088
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1089
|
+
status: 'Preparation',
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1093
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1094
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1095
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1096
|
+
]);
|
|
1097
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1098
|
+
{
|
|
1099
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1100
|
+
isConflicted: true,
|
|
1101
|
+
isPassedAllCiJob: true,
|
|
1102
|
+
isCiStateSuccess: true,
|
|
1103
|
+
isResolvedAllReviewComments: true,
|
|
1104
|
+
isBranchOutOfDate: false,
|
|
1105
|
+
missingRequiredCheckNames: [],
|
|
1106
|
+
},
|
|
1107
|
+
]);
|
|
1108
|
+
|
|
1109
|
+
await useCase.run({
|
|
1110
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1111
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1112
|
+
preparationStatus: 'Preparation',
|
|
1113
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1114
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1115
|
+
thresholdForAutoReject: 3,
|
|
1116
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1120
|
+
expect.objectContaining({
|
|
1121
|
+
status: 'Awaiting Workspace',
|
|
1122
|
+
}),
|
|
1123
|
+
mockProject,
|
|
1124
|
+
);
|
|
1125
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1126
|
+
expect.objectContaining({
|
|
1127
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1128
|
+
}),
|
|
1129
|
+
expect.stringContaining('PULL_REQUEST_CONFLICTED'),
|
|
1130
|
+
);
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
it('should reject when CI job failed', async () => {
|
|
1134
|
+
const issue = createMockIssue({
|
|
1135
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1136
|
+
status: 'Preparation',
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1140
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1141
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1142
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1143
|
+
]);
|
|
1144
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1145
|
+
{
|
|
1146
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1147
|
+
isConflicted: false,
|
|
1148
|
+
isPassedAllCiJob: false,
|
|
1149
|
+
isCiStateSuccess: false,
|
|
1150
|
+
isResolvedAllReviewComments: true,
|
|
1151
|
+
isBranchOutOfDate: false,
|
|
1152
|
+
missingRequiredCheckNames: [],
|
|
1153
|
+
},
|
|
1154
|
+
]);
|
|
1155
|
+
|
|
1156
|
+
await useCase.run({
|
|
1157
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1158
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1159
|
+
preparationStatus: 'Preparation',
|
|
1160
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1161
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1162
|
+
thresholdForAutoReject: 3,
|
|
1163
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1167
|
+
expect.objectContaining({
|
|
1168
|
+
status: 'Awaiting Workspace',
|
|
1169
|
+
}),
|
|
1170
|
+
mockProject,
|
|
1171
|
+
);
|
|
1172
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1173
|
+
expect.objectContaining({
|
|
1174
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1175
|
+
}),
|
|
751
1176
|
expect.stringContaining('ANY_CI_JOB_FAILED_OR_IN_PROGRESS'),
|
|
752
1177
|
);
|
|
753
1178
|
});
|
|
@@ -1012,20 +1437,120 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1012
1437
|
workflowBlockerResolvedWebhookUrl: null,
|
|
1013
1438
|
});
|
|
1014
1439
|
|
|
1015
|
-
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
1440
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
1441
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1442
|
+
expect.objectContaining({
|
|
1443
|
+
status: 'Awaiting Quality Check',
|
|
1444
|
+
}),
|
|
1445
|
+
mockProject,
|
|
1446
|
+
);
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
it('should still check for report comment even when issue has category label', async () => {
|
|
1450
|
+
const issue = createMockIssue({
|
|
1451
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1452
|
+
status: 'Preparation',
|
|
1453
|
+
labels: ['category:backend'],
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1457
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1458
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1459
|
+
createMockComment({
|
|
1460
|
+
content: 'Auto Status Check: REJECTED\n["NO_REPORT"]',
|
|
1461
|
+
}),
|
|
1462
|
+
]);
|
|
1463
|
+
|
|
1464
|
+
await useCase.run({
|
|
1465
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1466
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1467
|
+
preparationStatus: 'Preparation',
|
|
1468
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1469
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1470
|
+
thresholdForAutoReject: 3,
|
|
1471
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
1475
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1476
|
+
expect.objectContaining({
|
|
1477
|
+
status: 'Awaiting Workspace',
|
|
1478
|
+
}),
|
|
1479
|
+
mockProject,
|
|
1480
|
+
);
|
|
1481
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1482
|
+
expect.objectContaining({
|
|
1483
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1484
|
+
}),
|
|
1485
|
+
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
1486
|
+
);
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
it('should skip PR checks and update to Awaiting Quality Check when issue has llm-agent label', async () => {
|
|
1490
|
+
const issue = createMockIssue({
|
|
1491
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1492
|
+
status: 'Preparation',
|
|
1493
|
+
labels: ['llm-agent'],
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1497
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1498
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1499
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1500
|
+
]);
|
|
1501
|
+
|
|
1502
|
+
await useCase.run({
|
|
1503
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1504
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1505
|
+
preparationStatus: 'Preparation',
|
|
1506
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1507
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1508
|
+
thresholdForAutoReject: 3,
|
|
1509
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
1513
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1514
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1515
|
+
mockProject,
|
|
1516
|
+
);
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
it('should skip PR checks when issue has llm-agent: prefixed label', async () => {
|
|
1520
|
+
const issue = createMockIssue({
|
|
1521
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1522
|
+
status: 'Preparation',
|
|
1523
|
+
labels: ['llm-agent:claude'],
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1527
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1528
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1529
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1530
|
+
]);
|
|
1531
|
+
|
|
1532
|
+
await useCase.run({
|
|
1533
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1534
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1535
|
+
preparationStatus: 'Preparation',
|
|
1536
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1537
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1538
|
+
thresholdForAutoReject: 3,
|
|
1539
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1540
|
+
});
|
|
1541
|
+
|
|
1542
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
1016
1543
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1017
|
-
expect.objectContaining({
|
|
1018
|
-
status: 'Awaiting Quality Check',
|
|
1019
|
-
}),
|
|
1544
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1020
1545
|
mockProject,
|
|
1021
1546
|
);
|
|
1022
1547
|
});
|
|
1023
1548
|
|
|
1024
|
-
it('should still check for report comment even when issue has
|
|
1549
|
+
it('should still check for report comment even when issue has llm-agent:research label', async () => {
|
|
1025
1550
|
const issue = createMockIssue({
|
|
1026
1551
|
url: 'https://github.com/user/repo/issues/1',
|
|
1027
1552
|
status: 'Preparation',
|
|
1028
|
-
labels: ['
|
|
1553
|
+
labels: ['llm-agent:research'],
|
|
1029
1554
|
});
|
|
1030
1555
|
|
|
1031
1556
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
@@ -1048,19 +1573,57 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1048
1573
|
|
|
1049
1574
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
1050
1575
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1051
|
-
expect.objectContaining({
|
|
1052
|
-
status: 'Awaiting Workspace',
|
|
1053
|
-
}),
|
|
1576
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
1054
1577
|
mockProject,
|
|
1055
1578
|
);
|
|
1056
1579
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1057
|
-
expect.objectContaining({
|
|
1058
|
-
url: 'https://github.com/user/repo/issues/1',
|
|
1059
|
-
}),
|
|
1580
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/issues/1' }),
|
|
1060
1581
|
expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
|
|
1061
1582
|
);
|
|
1062
1583
|
});
|
|
1063
1584
|
|
|
1585
|
+
it('should use getOpenPullRequest when issue is a PR item', async () => {
|
|
1586
|
+
const prIssue = createMockIssue({
|
|
1587
|
+
url: 'https://github.com/user/repo/pull/10',
|
|
1588
|
+
status: 'Preparation',
|
|
1589
|
+
isPr: true,
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1593
|
+
mockIssueRepository.get.mockResolvedValue(prIssue);
|
|
1594
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1595
|
+
createMockComment({ content: 'From: Agent report' }),
|
|
1596
|
+
]);
|
|
1597
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue({
|
|
1598
|
+
url: 'https://github.com/user/repo/pull/10',
|
|
1599
|
+
isConflicted: false,
|
|
1600
|
+
isPassedAllCiJob: true,
|
|
1601
|
+
isCiStateSuccess: true,
|
|
1602
|
+
isResolvedAllReviewComments: true,
|
|
1603
|
+
isBranchOutOfDate: false,
|
|
1604
|
+
missingRequiredCheckNames: [],
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
await useCase.run({
|
|
1608
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1609
|
+
issueUrl: 'https://github.com/user/repo/pull/10',
|
|
1610
|
+
preparationStatus: 'Preparation',
|
|
1611
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1612
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1613
|
+
thresholdForAutoReject: 3,
|
|
1614
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
|
|
1618
|
+
'https://github.com/user/repo/pull/10',
|
|
1619
|
+
);
|
|
1620
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
1621
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1622
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1623
|
+
mockProject,
|
|
1624
|
+
);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1064
1627
|
describe('workflow blocker webhook notification', () => {
|
|
1065
1628
|
const createWorkflowBlockerStoryObjectMap = (
|
|
1066
1629
|
issueUrl: string,
|
|
@@ -1161,6 +1724,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1161
1724
|
content: 'Auto Status Check: REJECTED - third',
|
|
1162
1725
|
}),
|
|
1163
1726
|
]);
|
|
1727
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
1164
1728
|
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
1165
1729
|
createWorkflowBlockerStoryObjectMap(
|
|
1166
1730
|
'https://github.com/user/repo/issues/1',
|
|
@@ -1257,7 +1821,6 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1257
1821
|
workflowBlockerResolvedWebhookUrl: null,
|
|
1258
1822
|
});
|
|
1259
1823
|
|
|
1260
|
-
expect(mockIssueRepository.getStoryObjectMap).not.toHaveBeenCalled();
|
|
1261
1824
|
expect(mockWebhookRepository.sendGetRequest).not.toHaveBeenCalled();
|
|
1262
1825
|
});
|
|
1263
1826
|
|
|
@@ -1372,4 +1935,257 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1372
1935
|
);
|
|
1373
1936
|
});
|
|
1374
1937
|
});
|
|
1938
|
+
|
|
1939
|
+
it('should continue and not enrich dependedIssueUrls when getStoryObjectMap throws', async () => {
|
|
1940
|
+
const issue = createMockIssue({
|
|
1941
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1942
|
+
status: 'Preparation',
|
|
1943
|
+
dependedIssueUrls: [],
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1947
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1948
|
+
mockIssueRepository.getStoryObjectMap.mockRejectedValue(
|
|
1949
|
+
new Error('Story map unavailable'),
|
|
1950
|
+
);
|
|
1951
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1952
|
+
createMockComment({ content: 'From: Test report' }),
|
|
1953
|
+
]);
|
|
1954
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
1955
|
+
{
|
|
1956
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
1957
|
+
isConflicted: false,
|
|
1958
|
+
isPassedAllCiJob: true,
|
|
1959
|
+
isCiStateSuccess: true,
|
|
1960
|
+
isResolvedAllReviewComments: true,
|
|
1961
|
+
isBranchOutOfDate: false,
|
|
1962
|
+
missingRequiredCheckNames: [],
|
|
1963
|
+
},
|
|
1964
|
+
]);
|
|
1965
|
+
|
|
1966
|
+
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
|
1967
|
+
|
|
1968
|
+
await useCase.run({
|
|
1969
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1970
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1971
|
+
preparationStatus: 'Preparation',
|
|
1972
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
1973
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
1974
|
+
thresholdForAutoReject: 3,
|
|
1975
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
1979
|
+
'Failed to enrich dependedIssueUrls from story object map:',
|
|
1980
|
+
expect.any(Error),
|
|
1981
|
+
);
|
|
1982
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
1983
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
1984
|
+
mockProject,
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
consoleWarnSpy.mockRestore();
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
it('should return no PRs when getOpenPullRequest returns null for a PR item', async () => {
|
|
1991
|
+
const prIssue = createMockIssue({
|
|
1992
|
+
url: 'https://github.com/user/repo/pull/10',
|
|
1993
|
+
status: 'Preparation',
|
|
1994
|
+
isPr: true,
|
|
1995
|
+
});
|
|
1996
|
+
|
|
1997
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1998
|
+
mockIssueRepository.get.mockResolvedValue(prIssue);
|
|
1999
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
2000
|
+
createMockComment({ content: 'From: Agent report' }),
|
|
2001
|
+
]);
|
|
2002
|
+
mockIssueRepository.getOpenPullRequest.mockResolvedValue(null);
|
|
2003
|
+
|
|
2004
|
+
await useCase.run({
|
|
2005
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
2006
|
+
issueUrl: 'https://github.com/user/repo/pull/10',
|
|
2007
|
+
preparationStatus: 'Preparation',
|
|
2008
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2009
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
2010
|
+
thresholdForAutoReject: 3,
|
|
2011
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
|
|
2015
|
+
'https://github.com/user/repo/pull/10',
|
|
2016
|
+
);
|
|
2017
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
2018
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
2019
|
+
mockProject,
|
|
2020
|
+
);
|
|
2021
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
2022
|
+
expect.objectContaining({ url: 'https://github.com/user/repo/pull/10' }),
|
|
2023
|
+
expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
|
|
2024
|
+
);
|
|
2025
|
+
});
|
|
2026
|
+
|
|
2027
|
+
it('should not reject REPORT_HAS_NEXT_STEP when report JSON is invalid', async () => {
|
|
2028
|
+
const issue = createMockIssue({
|
|
2029
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2030
|
+
status: 'Preparation',
|
|
2031
|
+
});
|
|
2032
|
+
|
|
2033
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2034
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
2035
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
2036
|
+
createMockComment({
|
|
2037
|
+
content: 'From: Agent report\n```json\n{invalid json}\n```',
|
|
2038
|
+
}),
|
|
2039
|
+
]);
|
|
2040
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
2041
|
+
{
|
|
2042
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
2043
|
+
isConflicted: false,
|
|
2044
|
+
isPassedAllCiJob: true,
|
|
2045
|
+
isCiStateSuccess: true,
|
|
2046
|
+
isResolvedAllReviewComments: true,
|
|
2047
|
+
isBranchOutOfDate: false,
|
|
2048
|
+
missingRequiredCheckNames: [],
|
|
2049
|
+
},
|
|
2050
|
+
]);
|
|
2051
|
+
|
|
2052
|
+
await useCase.run({
|
|
2053
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
2054
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
2055
|
+
preparationStatus: 'Preparation',
|
|
2056
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2057
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
2058
|
+
thresholdForAutoReject: 3,
|
|
2059
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
2060
|
+
});
|
|
2061
|
+
|
|
2062
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
2063
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
2064
|
+
mockProject,
|
|
2065
|
+
);
|
|
2066
|
+
});
|
|
2067
|
+
|
|
2068
|
+
it('should not reject REPORT_HAS_NEXT_STEP when report JSON is null', async () => {
|
|
2069
|
+
const issue = createMockIssue({
|
|
2070
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2071
|
+
status: 'Preparation',
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2075
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
2076
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
2077
|
+
createMockComment({
|
|
2078
|
+
content: 'From: Agent report\n```json\nnull\n```',
|
|
2079
|
+
}),
|
|
2080
|
+
]);
|
|
2081
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
2082
|
+
{
|
|
2083
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
2084
|
+
isConflicted: false,
|
|
2085
|
+
isPassedAllCiJob: true,
|
|
2086
|
+
isCiStateSuccess: true,
|
|
2087
|
+
isResolvedAllReviewComments: true,
|
|
2088
|
+
isBranchOutOfDate: false,
|
|
2089
|
+
missingRequiredCheckNames: [],
|
|
2090
|
+
},
|
|
2091
|
+
]);
|
|
2092
|
+
|
|
2093
|
+
await useCase.run({
|
|
2094
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
2095
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
2096
|
+
preparationStatus: 'Preparation',
|
|
2097
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2098
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
2099
|
+
thresholdForAutoReject: 3,
|
|
2100
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
2104
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
2105
|
+
mockProject,
|
|
2106
|
+
);
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
it('should not reject REPORT_HAS_NEXT_STEP when report JSON has no nextStep property', async () => {
|
|
2110
|
+
const issue = createMockIssue({
|
|
2111
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2112
|
+
status: 'Preparation',
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2116
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
2117
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
2118
|
+
createMockComment({
|
|
2119
|
+
content:
|
|
2120
|
+
'From: Agent report\n```json\n{"status": "done", "result": "success"}\n```',
|
|
2121
|
+
}),
|
|
2122
|
+
]);
|
|
2123
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
2124
|
+
{
|
|
2125
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
2126
|
+
isConflicted: false,
|
|
2127
|
+
isPassedAllCiJob: true,
|
|
2128
|
+
isCiStateSuccess: true,
|
|
2129
|
+
isResolvedAllReviewComments: true,
|
|
2130
|
+
isBranchOutOfDate: false,
|
|
2131
|
+
missingRequiredCheckNames: [],
|
|
2132
|
+
},
|
|
2133
|
+
]);
|
|
2134
|
+
|
|
2135
|
+
await useCase.run({
|
|
2136
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
2137
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
2138
|
+
preparationStatus: 'Preparation',
|
|
2139
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2140
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
2141
|
+
thresholdForAutoReject: 3,
|
|
2142
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
2143
|
+
});
|
|
2144
|
+
|
|
2145
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
2146
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
2147
|
+
mockProject,
|
|
2148
|
+
);
|
|
2149
|
+
});
|
|
2150
|
+
|
|
2151
|
+
it('should not reject REPORT_HAS_NEXT_STEP when report JSON is a non-object value', async () => {
|
|
2152
|
+
const issue = createMockIssue({
|
|
2153
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
2154
|
+
status: 'Preparation',
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2158
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
2159
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
2160
|
+
createMockComment({
|
|
2161
|
+
content: 'From: Agent report\n```json\n"just a string"\n```',
|
|
2162
|
+
}),
|
|
2163
|
+
]);
|
|
2164
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
2165
|
+
{
|
|
2166
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
2167
|
+
isConflicted: false,
|
|
2168
|
+
isPassedAllCiJob: true,
|
|
2169
|
+
isCiStateSuccess: true,
|
|
2170
|
+
isResolvedAllReviewComments: true,
|
|
2171
|
+
isBranchOutOfDate: false,
|
|
2172
|
+
missingRequiredCheckNames: [],
|
|
2173
|
+
},
|
|
2174
|
+
]);
|
|
2175
|
+
|
|
2176
|
+
await useCase.run({
|
|
2177
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
2178
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
2179
|
+
preparationStatus: 'Preparation',
|
|
2180
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
2181
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
2182
|
+
thresholdForAutoReject: 3,
|
|
2183
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
2184
|
+
});
|
|
2185
|
+
|
|
2186
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
2187
|
+
expect.objectContaining({ status: 'Awaiting Quality Check' }),
|
|
2188
|
+
mockProject,
|
|
2189
|
+
);
|
|
2190
|
+
});
|
|
1375
2191
|
});
|