github-issue-tower-defence-management 1.104.0 → 1.104.2

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 (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +83 -58
  3. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  4. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -2
  5. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  6. package/bin/domain/usecases/IssueRejectionEvaluator.js +21 -2
  7. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  8. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +36 -6
  9. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  10. package/bin/domain/usecases/StartPreparationUseCase.js +2 -1
  11. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  12. package/bin/domain/usecases/resolveAllowedIssueAuthors.js +8 -0
  13. package/bin/domain/usecases/resolveAllowedIssueAuthors.js.map +1 -0
  14. package/package.json +1 -1
  15. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +36 -0
  16. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +23 -1
  17. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +109 -0
  18. package/src/domain/usecases/HandleScheduledEventUseCase.ts +8 -2
  19. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +182 -0
  20. package/src/domain/usecases/IssueRejectionEvaluator.ts +35 -1
  21. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +303 -75
  22. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +42 -7
  23. package/src/domain/usecases/StartPreparationUseCase.test.ts +202 -88
  24. package/src/domain/usecases/StartPreparationUseCase.ts +2 -1
  25. package/src/domain/usecases/resolveAllowedIssueAuthors.test.ts +51 -0
  26. package/src/domain/usecases/resolveAllowedIssueAuthors.ts +6 -0
  27. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  28. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  29. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  30. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +5 -1
  31. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  32. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -0
  33. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  34. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  35. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts +5 -0
  36. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts.map +1 -0
@@ -79,7 +79,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
79
79
  isInProgress: false,
80
80
  isClosed: false,
81
81
  createdAt: new Date(),
82
- author: '',
82
+ author: 'owner',
83
83
  closingIssueReferenceUrls: [],
84
84
  ...overrides,
85
85
  });
@@ -93,7 +93,20 @@ const createMockPullRequest = (overrides: Partial<Issue> = {}): Issue =>
93
93
  ...overrides,
94
94
  });
95
95
 
96
- const createReadyPr = (url = 'https://github.com/user/repo/pull/1') => ({
96
+ type RelatedPrLike = {
97
+ url: string;
98
+ isConflicted: boolean;
99
+ isPassedAllCiJob: boolean;
100
+ isCiStateSuccess: boolean;
101
+ isResolvedAllReviewComments: boolean;
102
+ isBranchOutOfDate: boolean;
103
+ missingRequiredCheckNames: string[];
104
+ isDraft?: boolean;
105
+ };
106
+
107
+ const createReadyPr = (
108
+ url = 'https://github.com/user/repo/pull/1',
109
+ ): RelatedPrLike => ({
97
110
  url,
98
111
  isConflicted: false,
99
112
  isPassedAllCiJob: true,
@@ -103,6 +116,37 @@ const createReadyPr = (url = 'https://github.com/user/repo/pull/1') => ({
103
116
  missingRequiredCheckNames: [],
104
117
  });
105
118
 
119
+ // Builds the in-memory linkage the production code now relies on instead of the
120
+ // per-issue findRelatedOpenPRs timeline query: each related pull request becomes
121
+ // an open PR project item whose closingIssueReferenceUrls points at the AQC
122
+ // issue, and getOpenPullRequest is routed by URL to return that PR's status.
123
+ // This proves the review-readiness outcome is identical to the previous
124
+ // findRelatedOpenPRs-based path while removing the per-issue timeline call.
125
+ const linkRelatedOpenPrsToIssue = (
126
+ mockIssueRepository: {
127
+ getAllIssues: jest.Mock;
128
+ getOpenPullRequest: jest.Mock;
129
+ },
130
+ issue: Issue,
131
+ relatedPrs: RelatedPrLike[],
132
+ ): void => {
133
+ const prItems = relatedPrs.map((pr, index) =>
134
+ createMockPullRequest({
135
+ url: pr.url,
136
+ number: 1000 + index,
137
+ closingIssueReferenceUrls: [issue.url],
138
+ }),
139
+ );
140
+ mockIssueRepository.getAllIssues.mockResolvedValue({
141
+ issues: [issue, ...prItems],
142
+ cacheUsed: false,
143
+ });
144
+ const prByUrl = new Map(relatedPrs.map((pr) => [pr.url, pr]));
145
+ mockIssueRepository.getOpenPullRequest.mockImplementation((prUrl: string) =>
146
+ Promise.resolve(prByUrl.get(prUrl) ?? null),
147
+ );
148
+ };
149
+
106
150
  describe('RevertNotReadyReviewQueueIssueUseCase', () => {
107
151
  let mockProjectRepository: {
108
152
  findProjectIdByUrl: jest.Mock;
@@ -171,6 +215,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
171
215
  await useCase.run({
172
216
  projectUrl: 'https://github.com/users/user/projects/1',
173
217
  allowIssueCacheMinutes: 10,
218
+ allowedIssueAuthors: ['owner'],
174
219
  });
175
220
 
176
221
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -190,6 +235,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
190
235
  await useCase.run({
191
236
  projectUrl: 'https://github.com/users/user/projects/1',
192
237
  allowIssueCacheMinutes: 10,
238
+ allowedIssueAuthors: ['owner'],
193
239
  });
194
240
 
195
241
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -204,11 +250,11 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
204
250
  issues: [issue],
205
251
  cacheUsed: false,
206
252
  });
207
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
208
253
 
209
254
  await useCase.run({
210
255
  projectUrl: 'https://github.com/users/user/projects/1',
211
256
  allowIssueCacheMinutes: 10,
257
+ allowedIssueAuthors: ['owner'],
212
258
  });
213
259
 
214
260
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -235,12 +281,12 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
235
281
  issues: [issue],
236
282
  cacheUsed: false,
237
283
  });
238
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
239
284
 
240
285
  await useCase.run({
241
286
  projectUrl: 'https://github.com/users/user/projects/1',
242
287
  allowIssueCacheMinutes: 10,
243
288
  labelsAsLlmAgentName: ['story', 'chore', 'accounting'],
289
+ allowedIssueAuthors: ['owner'],
244
290
  });
245
291
 
246
292
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -256,12 +302,12 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
256
302
  issues: [issue],
257
303
  cacheUsed: false,
258
304
  });
259
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
260
305
 
261
306
  await useCase.run({
262
307
  projectUrl: 'https://github.com/users/user/projects/1',
263
308
  allowIssueCacheMinutes: 10,
264
309
  labelsAsLlmAgentName: ['story', 'chore'],
310
+ allowedIssueAuthors: ['owner'],
265
311
  });
266
312
 
267
313
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -277,11 +323,11 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
277
323
  issues: [issue],
278
324
  cacheUsed: false,
279
325
  });
280
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
281
326
 
282
327
  await useCase.run({
283
328
  projectUrl: 'https://github.com/users/user/projects/1',
284
329
  allowIssueCacheMinutes: 10,
330
+ allowedIssueAuthors: ['owner'],
285
331
  });
286
332
 
287
333
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -304,12 +350,12 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
304
350
  issues: [issue],
305
351
  cacheUsed: false,
306
352
  });
307
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
308
353
 
309
354
  await useCase.run({
310
355
  projectUrl: 'https://github.com/users/user/projects/1',
311
356
  allowIssueCacheMinutes: 10,
312
357
  labelsAsLlmAgentName: null,
358
+ allowedIssueAuthors: ['owner'],
313
359
  });
314
360
 
315
361
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -323,17 +369,12 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
323
369
  const issue = createMockIssue({
324
370
  status: 'Awaiting Quality Check',
325
371
  });
326
- mockIssueRepository.getAllIssues.mockResolvedValue({
327
- issues: [issue],
328
- cacheUsed: false,
329
- });
330
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
331
- createReadyPr(),
332
- ]);
372
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [createReadyPr()]);
333
373
 
334
374
  await useCase.run({
335
375
  projectUrl: 'https://github.com/users/user/projects/1',
336
376
  allowIssueCacheMinutes: 10,
377
+ allowedIssueAuthors: ['owner'],
337
378
  });
338
379
 
339
380
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -344,17 +385,14 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
344
385
  const issue = createMockIssue({
345
386
  status: 'Awaiting Quality Check',
346
387
  });
347
- mockIssueRepository.getAllIssues.mockResolvedValue({
348
- issues: [issue],
349
- cacheUsed: false,
350
- });
351
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
388
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
352
389
  { ...createReadyPr(), isConflicted: true },
353
390
  ]);
354
391
 
355
392
  await useCase.run({
356
393
  projectUrl: 'https://github.com/users/user/projects/1',
357
394
  allowIssueCacheMinutes: 10,
395
+ allowedIssueAuthors: ['owner'],
358
396
  });
359
397
 
360
398
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -376,11 +414,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
376
414
  const issue = createMockIssue({
377
415
  status: 'Awaiting Quality Check',
378
416
  });
379
- mockIssueRepository.getAllIssues.mockResolvedValue({
380
- issues: [issue],
381
- cacheUsed: false,
382
- });
383
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
417
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
384
418
  {
385
419
  ...createReadyPr(),
386
420
  isPassedAllCiJob: false,
@@ -391,6 +425,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
391
425
  await useCase.run({
392
426
  projectUrl: 'https://github.com/users/user/projects/1',
393
427
  allowIssueCacheMinutes: 10,
428
+ allowedIssueAuthors: ['owner'],
394
429
  });
395
430
 
396
431
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -412,17 +447,14 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
412
447
  const issue = createMockIssue({
413
448
  status: 'Awaiting Quality Check',
414
449
  });
415
- mockIssueRepository.getAllIssues.mockResolvedValue({
416
- issues: [issue],
417
- cacheUsed: false,
418
- });
419
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
450
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
420
451
  { ...createReadyPr(), isResolvedAllReviewComments: false },
421
452
  ]);
422
453
 
423
454
  await useCase.run({
424
455
  projectUrl: 'https://github.com/users/user/projects/1',
425
456
  allowIssueCacheMinutes: 10,
457
+ allowedIssueAuthors: ['owner'],
426
458
  });
427
459
 
428
460
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -444,17 +476,14 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
444
476
  const issue = createMockIssue({
445
477
  status: 'Awaiting Quality Check',
446
478
  });
447
- mockIssueRepository.getAllIssues.mockResolvedValue({
448
- issues: [issue],
449
- cacheUsed: false,
450
- });
451
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
479
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
452
480
  { ...createReadyPr(), isDraft: true },
453
481
  ]);
454
482
 
455
483
  await useCase.run({
456
484
  projectUrl: 'https://github.com/users/user/projects/1',
457
485
  allowIssueCacheMinutes: 10,
486
+ allowedIssueAuthors: ['owner'],
458
487
  });
459
488
 
460
489
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -476,11 +505,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
476
505
  const issue = createMockIssue({
477
506
  status: 'Awaiting Quality Check',
478
507
  });
479
- mockIssueRepository.getAllIssues.mockResolvedValue({
480
- issues: [issue],
481
- cacheUsed: false,
482
- });
483
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
508
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
484
509
  createReadyPr('https://github.com/user/repo/pull/1'),
485
510
  createReadyPr('https://github.com/user/repo/pull/2'),
486
511
  ]);
@@ -488,6 +513,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
488
513
  await useCase.run({
489
514
  projectUrl: 'https://github.com/users/user/projects/1',
490
515
  allowIssueCacheMinutes: 10,
516
+ allowedIssueAuthors: ['owner'],
491
517
  });
492
518
 
493
519
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -509,11 +535,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
509
535
  const issue = createMockIssue({
510
536
  status: 'Awaiting Quality Check',
511
537
  });
512
- mockIssueRepository.getAllIssues.mockResolvedValue({
513
- issues: [issue],
514
- cacheUsed: false,
515
- });
516
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
538
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
517
539
  {
518
540
  ...createReadyPr(),
519
541
  isPassedAllCiJob: false,
@@ -525,6 +547,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
525
547
  await useCase.run({
526
548
  projectUrl: 'https://github.com/users/user/projects/1',
527
549
  allowIssueCacheMinutes: 10,
550
+ allowedIssueAuthors: ['owner'],
528
551
  });
529
552
 
530
553
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -542,17 +565,151 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
542
565
  );
543
566
  });
544
567
 
545
- describe('change-target label auto-approve', () => {
546
- const setupReadyIssue = (labels: string[]) => {
568
+ describe('in-memory related-PR derivation (no per-issue timeline call)', () => {
569
+ it('should never call findRelatedOpenPRs while sweeping Awaiting Quality Check issues', async () => {
570
+ const readyIssue = createMockIssue({
571
+ status: 'Awaiting Quality Check',
572
+ url: 'https://github.com/user/repo/issues/1',
573
+ });
574
+ const notReadyIssue = createMockIssue({
575
+ status: 'Awaiting Quality Check',
576
+ number: 2,
577
+ url: 'https://github.com/user/repo/issues/2',
578
+ });
579
+ const readyPrItem = createMockPullRequest({
580
+ status: 'In Progress',
581
+ url: 'https://github.com/user/repo/pull/100',
582
+ number: 100,
583
+ closingIssueReferenceUrls: ['https://github.com/user/repo/issues/1'],
584
+ });
585
+ const conflictedPrItem = createMockPullRequest({
586
+ status: 'In Progress',
587
+ url: 'https://github.com/user/repo/pull/200',
588
+ number: 200,
589
+ closingIssueReferenceUrls: ['https://github.com/user/repo/issues/2'],
590
+ });
591
+ mockIssueRepository.getAllIssues.mockResolvedValue({
592
+ issues: [readyIssue, notReadyIssue, readyPrItem, conflictedPrItem],
593
+ cacheUsed: false,
594
+ });
595
+ mockIssueRepository.getOpenPullRequest.mockImplementation(
596
+ (prUrl: string) => {
597
+ if (prUrl === 'https://github.com/user/repo/pull/100') {
598
+ return Promise.resolve(createReadyPr(prUrl));
599
+ }
600
+ if (prUrl === 'https://github.com/user/repo/pull/200') {
601
+ return Promise.resolve({
602
+ ...createReadyPr(prUrl),
603
+ isConflicted: true,
604
+ });
605
+ }
606
+ return Promise.resolve(null);
607
+ },
608
+ );
609
+
610
+ await useCase.run({
611
+ projectUrl: 'https://github.com/users/user/projects/1',
612
+ allowIssueCacheMinutes: 10,
613
+ allowedIssueAuthors: ['owner'],
614
+ });
615
+
616
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
617
+ expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
618
+ 'https://github.com/user/repo/pull/100',
619
+ );
620
+ expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
621
+ 'https://github.com/user/repo/pull/200',
622
+ );
623
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(1);
624
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
625
+ mockProject,
626
+ notReadyIssue,
627
+ 'awaiting-workspace-id',
628
+ );
629
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
630
+ notReadyIssue,
631
+ expect.stringContaining('PULL_REQUEST_CONFLICTED'),
632
+ );
633
+ });
634
+
635
+ it('should match a PR to its issue via closingIssueReferenceUrls even when the PR item is listed before the issue', async () => {
547
636
  const issue = createMockIssue({
548
637
  status: 'Awaiting Quality Check',
549
- labels,
638
+ url: 'https://github.com/user/repo/issues/42',
639
+ number: 42,
640
+ });
641
+ const prItem = createMockPullRequest({
642
+ status: 'In Progress',
643
+ url: 'https://github.com/user/repo/pull/77',
644
+ number: 77,
645
+ closingIssueReferenceUrls: ['https://github.com/user/repo/issues/42'],
550
646
  });
551
647
  mockIssueRepository.getAllIssues.mockResolvedValue({
552
- issues: [issue],
648
+ issues: [prItem, issue],
553
649
  cacheUsed: false,
554
650
  });
555
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
651
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue(
652
+ createReadyPr('https://github.com/user/repo/pull/77'),
653
+ );
654
+
655
+ await useCase.run({
656
+ projectUrl: 'https://github.com/users/user/projects/1',
657
+ allowIssueCacheMinutes: 10,
658
+ allowedIssueAuthors: ['owner'],
659
+ });
660
+
661
+ expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
662
+ 'https://github.com/user/repo/pull/77',
663
+ );
664
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
665
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
666
+ });
667
+
668
+ it('should not match a closed PR even when its closingIssueReferenceUrls points at the issue', async () => {
669
+ const issue = createMockIssue({
670
+ status: 'Awaiting Quality Check',
671
+ url: 'https://github.com/user/repo/issues/9',
672
+ number: 9,
673
+ });
674
+ const closedPrItem = createMockPullRequest({
675
+ status: 'In Progress',
676
+ url: 'https://github.com/user/repo/pull/9',
677
+ number: 9,
678
+ isClosed: true,
679
+ state: 'CLOSED',
680
+ closingIssueReferenceUrls: ['https://github.com/user/repo/issues/9'],
681
+ });
682
+ mockIssueRepository.getAllIssues.mockResolvedValue({
683
+ issues: [issue, closedPrItem],
684
+ cacheUsed: false,
685
+ });
686
+
687
+ await useCase.run({
688
+ projectUrl: 'https://github.com/users/user/projects/1',
689
+ allowIssueCacheMinutes: 10,
690
+ allowedIssueAuthors: ['owner'],
691
+ });
692
+
693
+ expect(mockIssueRepository.getOpenPullRequest).not.toHaveBeenCalled();
694
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
695
+ mockProject,
696
+ issue,
697
+ 'awaiting-workspace-id',
698
+ );
699
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
700
+ issue,
701
+ expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
702
+ );
703
+ });
704
+ });
705
+
706
+ describe('change-target label auto-approve', () => {
707
+ const setupReadyIssue = (labels: string[]) => {
708
+ const issue = createMockIssue({
709
+ status: 'Awaiting Quality Check',
710
+ labels,
711
+ });
712
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
556
713
  createReadyPr(),
557
714
  ]);
558
715
  return issue;
@@ -562,6 +719,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
562
719
  useCase.run({
563
720
  projectUrl: 'https://github.com/users/user/projects/1',
564
721
  allowIssueCacheMinutes: 10,
722
+ allowedIssueAuthors: ['owner'],
565
723
  });
566
724
 
567
725
  it('should not approve PR when issue has no change-target label', async () => {
@@ -684,11 +842,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
684
842
  status: 'Awaiting Quality Check',
685
843
  labels: ['change-target:src/domain'],
686
844
  });
687
- mockIssueRepository.getAllIssues.mockResolvedValue({
688
- issues: [issue],
689
- cacheUsed: false,
690
- });
691
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
845
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, []);
692
846
 
693
847
  await runCycle();
694
848
 
@@ -708,11 +862,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
708
862
  status: 'Awaiting Quality Check',
709
863
  labels: ['change-target:src/domain'],
710
864
  });
711
- mockIssueRepository.getAllIssues.mockResolvedValue({
712
- issues: [issue],
713
- cacheUsed: false,
714
- });
715
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
865
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
716
866
  { ...createReadyPr(), isConflicted: true },
717
867
  ]);
718
868
 
@@ -734,11 +884,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
734
884
  status: 'Awaiting Quality Check',
735
885
  labels: ['llm-agent', 'change-target:src/domain'],
736
886
  });
737
- mockIssueRepository.getAllIssues.mockResolvedValue({
738
- issues: [issue],
739
- cacheUsed: false,
740
- });
741
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
887
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
742
888
  createReadyPr(),
743
889
  ]);
744
890
 
@@ -773,6 +919,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
773
919
  await useCase.run({
774
920
  projectUrl: 'https://github.com/users/user/projects/1',
775
921
  allowIssueCacheMinutes: 10,
922
+ allowedIssueAuthors: ['owner'],
776
923
  changeTargetPathAliases: {
777
924
  adapters: 'src/domain/usecases/adapter-interfaces',
778
925
  },
@@ -792,6 +939,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
792
939
  await useCase.run({
793
940
  projectUrl: 'https://github.com/users/user/projects/1',
794
941
  allowIssueCacheMinutes: 10,
942
+ allowedIssueAuthors: ['owner'],
795
943
  changeTargetPathAliases: {
796
944
  adapters: 'src/domain/usecases/adapter-interfaces',
797
945
  },
@@ -815,6 +963,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
815
963
  await useCase.run({
816
964
  projectUrl: 'https://github.com/users/user/projects/1',
817
965
  allowIssueCacheMinutes: 10,
966
+ allowedIssueAuthors: ['owner'],
818
967
  });
819
968
 
820
969
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -836,6 +985,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
836
985
  await useCase.run({
837
986
  projectUrl: 'https://github.com/users/user/projects/1',
838
987
  allowIssueCacheMinutes: 10,
988
+ allowedIssueAuthors: ['owner'],
839
989
  });
840
990
 
841
991
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -856,6 +1006,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
856
1006
  await useCase.run({
857
1007
  projectUrl: 'https://github.com/users/user/projects/1',
858
1008
  allowIssueCacheMinutes: 10,
1009
+ allowedIssueAuthors: ['owner'],
859
1010
  });
860
1011
 
861
1012
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -876,6 +1027,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
876
1027
  await useCase.run({
877
1028
  projectUrl: 'https://github.com/users/user/projects/1',
878
1029
  allowIssueCacheMinutes: 10,
1030
+ allowedIssueAuthors: ['owner'],
879
1031
  });
880
1032
 
881
1033
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -899,6 +1051,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
899
1051
  await useCase.run({
900
1052
  projectUrl: 'https://github.com/users/user/projects/1',
901
1053
  allowIssueCacheMinutes: 10,
1054
+ allowedIssueAuthors: ['owner'],
902
1055
  });
903
1056
 
904
1057
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -938,6 +1091,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
938
1091
  await useCase.run({
939
1092
  projectUrl: 'https://github.com/users/user/projects/1',
940
1093
  allowIssueCacheMinutes: 10,
1094
+ allowedIssueAuthors: ['owner'],
941
1095
  });
942
1096
 
943
1097
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -972,6 +1126,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
972
1126
  await useCase.run({
973
1127
  projectUrl: 'https://github.com/users/user/projects/1',
974
1128
  allowIssueCacheMinutes: 10,
1129
+ allowedIssueAuthors: ['owner'],
975
1130
  });
976
1131
 
977
1132
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1008,6 +1163,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1008
1163
  await useCase.run({
1009
1164
  projectUrl: 'https://github.com/users/user/projects/1',
1010
1165
  allowIssueCacheMinutes: 10,
1166
+ allowedIssueAuthors: ['owner'],
1011
1167
  });
1012
1168
 
1013
1169
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1042,6 +1198,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1042
1198
  await useCase.run({
1043
1199
  projectUrl: 'https://github.com/users/user/projects/1',
1044
1200
  allowIssueCacheMinutes: 10,
1201
+ allowedIssueAuthors: ['owner'],
1045
1202
  });
1046
1203
 
1047
1204
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1073,6 +1230,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1073
1230
  await useCase.run({
1074
1231
  projectUrl: 'https://github.com/users/user/projects/1',
1075
1232
  allowIssueCacheMinutes: 10,
1233
+ allowedIssueAuthors: ['owner'],
1076
1234
  });
1077
1235
 
1078
1236
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1110,6 +1268,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1110
1268
  await useCase.run({
1111
1269
  projectUrl: 'https://github.com/users/user/projects/1',
1112
1270
  allowIssueCacheMinutes: 10,
1271
+ allowedIssueAuthors: ['owner'],
1113
1272
  });
1114
1273
 
1115
1274
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1154,6 +1313,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1154
1313
  await useCase.run({
1155
1314
  projectUrl: 'https://github.com/users/user/projects/1',
1156
1315
  allowIssueCacheMinutes: 10,
1316
+ allowedIssueAuthors: ['owner'],
1157
1317
  });
1158
1318
 
1159
1319
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -1223,7 +1383,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1223
1383
  );
1224
1384
  });
1225
1385
 
1226
- it('should act on an Unread pull request authored by a dependency-update bot not in allowedIssueAuthors', async () => {
1386
+ it('should skip an Unread pull request authored by a dependency-update bot not in allowedIssueAuthors', async () => {
1227
1387
  const pullRequest = createMockPullRequest({
1228
1388
  status: 'Unread',
1229
1389
  author: 'dependabot[bot]',
@@ -1243,6 +1403,31 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1243
1403
  allowedIssueAuthors: ['owner'],
1244
1404
  });
1245
1405
 
1406
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1407
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1408
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1409
+ });
1410
+
1411
+ it('should act on a dependency-update bot only when it is explicitly listed in allowedIssueAuthors', async () => {
1412
+ const pullRequest = createMockPullRequest({
1413
+ status: 'Unread',
1414
+ author: 'dependabot[bot]',
1415
+ });
1416
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1417
+ issues: [pullRequest],
1418
+ cacheUsed: false,
1419
+ });
1420
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1421
+ ...createReadyPr(),
1422
+ isConflicted: true,
1423
+ });
1424
+
1425
+ await useCase.run({
1426
+ projectUrl: 'https://github.com/users/user/projects/1',
1427
+ allowIssueCacheMinutes: 10,
1428
+ allowedIssueAuthors: ['owner', 'dependabot[bot]'],
1429
+ });
1430
+
1246
1431
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1247
1432
  mockProject,
1248
1433
  pullRequest,
@@ -1254,7 +1439,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1254
1439
  );
1255
1440
  });
1256
1441
 
1257
- it('should act on every author when allowedIssueAuthors is null', async () => {
1442
+ it('should process no author when allowedIssueAuthors is null', async () => {
1258
1443
  const pullRequest = createMockPullRequest({
1259
1444
  status: 'Unread',
1260
1445
  author: 'outside-contributor',
@@ -1274,15 +1459,58 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1274
1459
  allowedIssueAuthors: null,
1275
1460
  });
1276
1461
 
1277
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1278
- mockProject,
1279
- pullRequest,
1280
- 'awaiting-workspace-id',
1281
- );
1282
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1283
- pullRequest,
1284
- expect.stringContaining('Auto Status Check: REJECTED'),
1285
- );
1462
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1463
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1464
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1465
+ });
1466
+
1467
+ it('should process no author when allowedIssueAuthors is undefined', async () => {
1468
+ const pullRequest = createMockPullRequest({
1469
+ status: 'Unread',
1470
+ author: 'owner',
1471
+ });
1472
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1473
+ issues: [pullRequest],
1474
+ cacheUsed: false,
1475
+ });
1476
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1477
+ ...createReadyPr(),
1478
+ isConflicted: true,
1479
+ });
1480
+
1481
+ await useCase.run({
1482
+ projectUrl: 'https://github.com/users/user/projects/1',
1483
+ allowIssueCacheMinutes: 10,
1484
+ });
1485
+
1486
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1487
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1488
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1489
+ });
1490
+
1491
+ it('should process no author when allowedIssueAuthors is an empty list', async () => {
1492
+ const pullRequest = createMockPullRequest({
1493
+ status: 'Unread',
1494
+ author: 'owner',
1495
+ });
1496
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1497
+ issues: [pullRequest],
1498
+ cacheUsed: false,
1499
+ });
1500
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1501
+ ...createReadyPr(),
1502
+ isConflicted: true,
1503
+ });
1504
+
1505
+ await useCase.run({
1506
+ projectUrl: 'https://github.com/users/user/projects/1',
1507
+ allowIssueCacheMinutes: 10,
1508
+ allowedIssueAuthors: [],
1509
+ });
1510
+
1511
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1512
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1513
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1286
1514
  });
1287
1515
  });
1288
1516
  });