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.
Files changed (58) hide show
  1. package/.github/workflows/create-pr.yml +12 -5
  2. package/.github/workflows/umino-project.yml +5 -4
  3. package/CHANGELOG.md +26 -0
  4. package/README.md +21 -9
  5. package/bin/adapter/entry-points/cli/index.js +45 -10
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +32 -8
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/repositories/NodeLocalCommandRunner.js +3 -3
  10. package/bin/adapter/repositories/NodeLocalCommandRunner.js.map +1 -1
  11. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +446 -11
  12. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -2
  14. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  15. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +111 -16
  16. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +7 -2
  18. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  19. package/bin/domain/usecases/StartPreparationUseCase.js +107 -72
  20. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/adapter/entry-points/cli/index.test.ts +26 -13
  23. package/src/adapter/entry-points/cli/index.ts +74 -13
  24. package/src/adapter/repositories/NodeLocalCommandRunner.test.ts +12 -12
  25. package/src/adapter/repositories/NodeLocalCommandRunner.ts +7 -4
  26. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -0
  27. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +689 -12
  28. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +3 -0
  29. package/src/domain/entities/Issue.ts +1 -0
  30. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  31. package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -3
  32. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +983 -167
  33. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +177 -26
  34. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +22 -9
  35. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +8 -3
  36. package/src/domain/usecases/StartPreparationUseCase.test.ts +1696 -290
  37. package/src/domain/usecases/StartPreparationUseCase.ts +171 -126
  38. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -4
  39. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +4 -1
  40. package/types/adapter/entry-points/cli/index.d.ts +4 -1
  41. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  42. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts +1 -1
  43. package/types/adapter/repositories/NodeLocalCommandRunner.d.ts.map +1 -1
  44. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +7 -6
  45. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  46. package/types/domain/entities/Issue.d.ts +1 -0
  47. package/types/domain/entities/Issue.d.ts.map +1 -1
  48. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -1
  49. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  50. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +5 -1
  51. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  52. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  53. package/types/domain/usecases/StartPreparationUseCase.d.ts +10 -18
  54. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  55. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -3
  56. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  57. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +1 -1
  58. 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 reject and set status to Awaiting Workspace when last comment starts with Auto Status Check:', async () => {
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
- url: 'https://github.com/user/repo/issues/1',
306
- }),
307
- expect.stringContaining('Auto Status Check: REJECTED'),
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 reject when last comment does not start with From:', async () => {
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
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
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
- url: 'https://github.com/user/repo/issues/1',
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 reject and set status to Awaiting Workspace when no comments exist', async () => {
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).toHaveBeenCalled();
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 auto-escalate to Awaiting Quality Check after threshold rejections', async () => {
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
- url: 'https://github.com/user/repo/issues/1',
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 not auto-escalate when rejections are below threshold', async () => {
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({ content: 'Auto Status Check: REJECTED - first' }),
448
- createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
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 not auto-escalate when retry comment exists even if threshold met', async () => {
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: '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' }),
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).not.toHaveBeenCalledWith(
554
+ expect(mockIssueRepository.update).toHaveBeenCalledWith(
517
555
  expect.objectContaining({
518
- status: 'Awaiting Quality Check',
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 handle case-insensitive retry comment', async () => {
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).not.toHaveBeenCalledWith(
599
+ expect(mockIssueRepository.update).toHaveBeenCalledWith(
561
600
  expect.objectContaining({
562
- status: 'Awaiting Quality Check',
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 PR is not found', async () => {
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({ content: 'From: Test report' }),
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
- url: 'https://github.com/user/repo/issues/1',
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 multiple PRs are found', async () => {
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({ content: 'From: Test report' }),
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 reject when PR is conflicted', async () => {
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: 'From: Test report' }),
671
- ]);
672
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
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 Workspace',
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('PULL_REQUEST_CONFLICTED'),
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 reject when CI job failed', async () => {
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: 'From: Test report' }),
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: 'https://github.com/user/repo/pull/1',
763
+ url: prUrl,
722
764
  isConflicted: false,
723
- isPassedAllCiJob: false,
724
- isCiStateSuccess: false,
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
- url: 'https://github.com/user/repo/issues/1',
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 category label', async () => {
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: ['category:backend'],
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
  });