github-issue-tower-defence-management 1.103.1 → 1.104.1

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 (64) 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/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -1
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  6. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +16 -0
  7. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  8. package/bin/domain/usecases/HandleScheduledEventUseCase.js +7 -2
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  10. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +4 -5
  11. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  12. package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js +30 -4
  13. package/bin/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.js.map +1 -1
  14. package/bin/domain/usecases/StartPreparationUseCase.js +2 -1
  15. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  16. package/bin/domain/usecases/resolveAllowedIssueAuthors.js +8 -0
  17. package/bin/domain/usecases/resolveAllowedIssueAuthors.js.map +1 -0
  18. package/package.json +1 -1
  19. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
  20. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +36 -0
  21. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +23 -1
  22. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  23. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
  24. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  25. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  26. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  27. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +9 -0
  28. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +10 -0
  29. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +24 -0
  30. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  31. package/src/domain/entities/Issue.ts +1 -0
  32. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  33. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  34. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +109 -0
  35. package/src/domain/usecases/HandleScheduledEventUseCase.ts +8 -2
  36. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  37. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  38. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +110 -12
  39. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +5 -7
  40. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  41. package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.test.ts +148 -115
  42. package/src/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.ts +35 -10
  43. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
  44. package/src/domain/usecases/StartPreparationUseCase.test.ts +203 -88
  45. package/src/domain/usecases/StartPreparationUseCase.ts +2 -1
  46. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
  47. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
  48. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  49. package/src/domain/usecases/resolveAllowedIssueAuthors.test.ts +51 -0
  50. package/src/domain/usecases/resolveAllowedIssueAuthors.ts +6 -0
  51. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  52. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  53. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  54. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  55. package/types/domain/entities/Issue.d.ts +1 -0
  56. package/types/domain/entities/Issue.d.ts.map +1 -1
  57. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  58. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  59. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  60. package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts +3 -2
  61. package/types/domain/usecases/SetDependedIssueUrlForOpenTaskPRsUseCase.d.ts.map +1 -1
  62. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  63. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts +5 -0
  64. package/types/domain/usecases/resolveAllowedIssueAuthors.d.ts.map +1 -0
@@ -543,6 +543,7 @@ export class ApiV3CheerioRestIssueRepository
543
543
  isClosed: item.state !== 'OPEN',
544
544
  createdAt: new Date(item.createdAt || '2000-01-01'),
545
545
  author: item.author,
546
+ closingIssueReferenceUrls: item.closingIssueReferenceUrls,
546
547
  };
547
548
  };
548
549
  getAllIssuesFromCache = async (
@@ -578,6 +579,14 @@ export class ApiV3CheerioRestIssueRepository
578
579
  !('createdAt' in issue) || typeof issue.createdAt !== 'string'
579
580
  ? new Date()
580
581
  : new Date(issue.createdAt);
582
+ const closingIssueReferenceUrls =
583
+ 'closingIssueReferenceUrls' in issue &&
584
+ Array.isArray(issue.closingIssueReferenceUrls) &&
585
+ issue.closingIssueReferenceUrls.every(
586
+ (url): url is string => typeof url === 'string',
587
+ )
588
+ ? issue.closingIssueReferenceUrls
589
+ : [];
581
590
 
582
591
  return {
583
592
  ...issue,
@@ -585,6 +594,7 @@ export class ApiV3CheerioRestIssueRepository
585
594
  completionDate50PercentConfidence:
586
595
  completionDate50PercentConfidence,
587
596
  createdAt: createdAt,
597
+ closingIssueReferenceUrls: closingIssueReferenceUrls,
588
598
  };
589
599
  });
590
600
 
@@ -14,6 +14,7 @@ export type ProjectItem = {
14
14
  assignees: string[];
15
15
  createdAt: string;
16
16
  author: string;
17
+ closingIssueReferenceUrls: string[];
17
18
  customFields: {
18
19
  name: string;
19
20
  value: string | null;
@@ -214,6 +215,11 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
214
215
  repository {
215
216
  nameWithOwner
216
217
  }
218
+ closingIssuesReferences(first: 50) {
219
+ nodes {
220
+ url
221
+ }
222
+ }
217
223
  }
218
224
  }
219
225
  }
@@ -257,6 +263,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
257
263
  author: { login: string } | null;
258
264
  labels: { nodes: { name: string }[] };
259
265
  assignees: { nodes: { login: string }[] };
266
+ closingIssuesReferences?: { nodes: { url: string }[] };
260
267
  };
261
268
  }[];
262
269
  };
@@ -309,6 +316,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
309
316
  author: { login: string } | null;
310
317
  labels: { nodes: { name: string }[] };
311
318
  assignees: { nodes: { login: string }[] };
319
+ closingIssuesReferences?: { nodes: { url: string }[] };
312
320
  };
313
321
  }[];
314
322
  };
@@ -364,6 +372,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
364
372
  author: { login: string } | null;
365
373
  labels: { nodes: { name: string }[] };
366
374
  assignees: { nodes: { login: string }[] };
375
+ closingIssuesReferences?: { nodes: { url: string }[] };
367
376
  };
368
377
  }[];
369
378
  };
@@ -435,6 +444,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
435
444
  author: { login: string } | null;
436
445
  labels: { nodes: { name: string }[] };
437
446
  assignees: { nodes: { login: string }[] };
447
+ closingIssuesReferences?: { nodes: { url: string }[] };
438
448
  };
439
449
  }[] = pageNodes;
440
450
  projectItems.forEach((item) => {
@@ -453,6 +463,10 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!) {
453
463
  assignees: item.content.assignees?.nodes?.map((a) => a.login) || [],
454
464
  createdAt: item.content.createdAt || new Date().toISOString(),
455
465
  author: item.content.author?.login || '',
466
+ closingIssueReferenceUrls:
467
+ item.content.closingIssuesReferences?.nodes
468
+ ?.map((node) => node.url)
469
+ .filter((url) => url.length > 0) || [],
456
470
  customFields: item.fieldValues.nodes
457
471
  .filter((field) => !!field.field)
458
472
  .map((field) => {
@@ -768,6 +782,11 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
768
782
  repository {
769
783
  nameWithOwner
770
784
  }
785
+ closingIssuesReferences(first: 50) {
786
+ nodes {
787
+ url
788
+ }
789
+ }
771
790
  projectItems(first: 10) {
772
791
  nodes {
773
792
  id
@@ -843,6 +862,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
843
862
  labels: { nodes: { name: string }[] };
844
863
  assignees: { nodes: { login: string }[] };
845
864
  repository: { nameWithOwner: string };
865
+ closingIssuesReferences?: { nodes: { url: string }[] };
846
866
  projectItems: {
847
867
  nodes: {
848
868
  id: string;
@@ -914,6 +934,10 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
914
934
  assignees: content.assignees?.nodes?.map((a) => a.login) || [],
915
935
  createdAt: content.createdAt || new Date().toISOString(),
916
936
  author: content.author?.login || '',
937
+ closingIssueReferenceUrls:
938
+ content.closingIssuesReferences?.nodes
939
+ ?.map((node) => node.url)
940
+ .filter((url) => url.length > 0) || [],
917
941
  customFields: item.fieldValues.nodes
918
942
  .filter((field) => !!field.field)
919
943
  .map((field) => {
@@ -50,6 +50,7 @@ const buildIssue = (overrides: Partial<Issue> = {}): Issue => ({
50
50
  isClosed: false,
51
51
  createdAt: new Date(),
52
52
  author: '',
53
+ closingIssueReferenceUrls: [],
53
54
  ...overrides,
54
55
  });
55
56
 
@@ -24,4 +24,5 @@ export type Issue = {
24
24
  isClosed: boolean;
25
25
  createdAt: Date;
26
26
  author: string;
27
+ closingIssueReferenceUrls: string[];
27
28
  };
@@ -27,6 +27,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
27
27
  isClosed: false,
28
28
  createdAt: new Date('2000-01-01T00:00:00Z'),
29
29
  author: 'test-user',
30
+ closingIssueReferenceUrls: [],
30
31
  ...overrides,
31
32
  });
32
33
 
@@ -67,6 +67,7 @@ describe('GetStoryObjectMapUseCase', () => {
67
67
  isClosed: false,
68
68
  createdAt: new Date(),
69
69
  author: '',
70
+ closingIssueReferenceUrls: [],
70
71
  ...overrides,
71
72
  });
72
73
 
@@ -379,6 +379,115 @@ describe('HandleScheduledEventUseCase', () => {
379
379
  ).toHaveBeenCalledTimes(1);
380
380
  });
381
381
 
382
+ it('should pass a top-level allowedIssueAuthors to revertNotReadyReviewQueueIssueUseCase', async () => {
383
+ const input = {
384
+ projectName: 'test-project',
385
+ org: 'test-org',
386
+ projectUrl: 'https://github.com/test-org/test-project',
387
+ manager: 'test-manager',
388
+ workingReport: {
389
+ repo: 'test-repo',
390
+ members: ['member1'],
391
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
392
+ },
393
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
394
+ disabled: false,
395
+ allowIssueCacheMinutes: 60,
396
+ allowedIssueAuthors: ['top-level-author'],
397
+ };
398
+
399
+ mockProjectRepository.getProject.mockResolvedValue(mock<Project>());
400
+ await useCase.run(input);
401
+
402
+ expect(
403
+ mockRevertNotReadyReviewQueueIssueUseCase.run,
404
+ ).toHaveBeenCalledWith(
405
+ expect.objectContaining({
406
+ allowedIssueAuthors: ['top-level-author'],
407
+ }),
408
+ );
409
+ });
410
+
411
+ it('should prefer a top-level allowedIssueAuthors over the startPreparation one', async () => {
412
+ const input = {
413
+ projectName: 'test-project',
414
+ org: 'test-org',
415
+ projectUrl: 'https://github.com/test-org/test-project',
416
+ manager: 'test-manager',
417
+ workingReport: {
418
+ repo: 'test-repo',
419
+ members: ['member1'],
420
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
421
+ },
422
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
423
+ disabled: false,
424
+ allowIssueCacheMinutes: 60,
425
+ allowedIssueAuthors: ['top-level-author'],
426
+ startPreparation: {
427
+ defaultAgentName: 'agent1',
428
+ configFilePath: '/path/to/config.yml',
429
+ maximumPreparingIssuesCount: null,
430
+ allowedIssueAuthors: ['nested-author'],
431
+ },
432
+ };
433
+
434
+ mockProjectRepository.getProject.mockResolvedValue(mock<Project>());
435
+ mockStartPreparationUseCase.run.mockResolvedValue({
436
+ rotationOrder: null,
437
+ });
438
+ await useCase.run(input);
439
+
440
+ expect(
441
+ mockRevertNotReadyReviewQueueIssueUseCase.run,
442
+ ).toHaveBeenCalledWith(
443
+ expect.objectContaining({
444
+ allowedIssueAuthors: ['top-level-author'],
445
+ }),
446
+ );
447
+ expect(mockStartPreparationUseCase.run).toHaveBeenCalledWith(
448
+ expect.objectContaining({
449
+ allowedIssueAuthors: ['top-level-author'],
450
+ }),
451
+ );
452
+ });
453
+
454
+ it('should fall back to startPreparation allowedIssueAuthors when no top-level value is set', async () => {
455
+ const input = {
456
+ projectName: 'test-project',
457
+ org: 'test-org',
458
+ projectUrl: 'https://github.com/test-org/test-project',
459
+ manager: 'test-manager',
460
+ workingReport: {
461
+ repo: 'test-repo',
462
+ members: ['member1'],
463
+ spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
464
+ },
465
+ urlOfStoryView: 'https://github.com/test-org/test-project/issues',
466
+ disabled: false,
467
+ allowIssueCacheMinutes: 60,
468
+ startPreparation: {
469
+ defaultAgentName: 'agent1',
470
+ configFilePath: '/path/to/config.yml',
471
+ maximumPreparingIssuesCount: null,
472
+ allowedIssueAuthors: ['nested-author'],
473
+ },
474
+ };
475
+
476
+ mockProjectRepository.getProject.mockResolvedValue(mock<Project>());
477
+ mockStartPreparationUseCase.run.mockResolvedValue({
478
+ rotationOrder: null,
479
+ });
480
+ await useCase.run(input);
481
+
482
+ expect(
483
+ mockRevertNotReadyReviewQueueIssueUseCase.run,
484
+ ).toHaveBeenCalledWith(
485
+ expect.objectContaining({
486
+ allowedIssueAuthors: ['nested-author'],
487
+ }),
488
+ );
489
+ });
490
+
382
491
  it('should invoke UpdateRateLimitCacheUseCase before StartPreparationUseCase when startPreparation is configured', async () => {
383
492
  const callOrder: string[] = [];
384
493
  mockUpdateRateLimitCacheUseCase.run.mockImplementation(async () => {
@@ -27,6 +27,7 @@ import {
27
27
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
28
28
  import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
29
29
  import { resolveLabelsAsLlmAgentName } from './resolveLabelsAsLlmAgentName';
30
+ import { resolveAllowedIssueAuthors } from './resolveAllowedIssueAuthors';
30
31
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
31
32
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
32
33
  import {
@@ -86,6 +87,7 @@ export class HandleScheduledEventUseCase {
86
87
  allowIssueCacheMinutes: number;
87
88
  labelsAsLlmAgentName?: string[] | null;
88
89
  changeTargetPathAliases?: Record<string, string> | null;
90
+ allowedIssueAuthors?: string[] | null;
89
91
  startPreparation?: {
90
92
  defaultAgentName: string;
91
93
  defaultLlmModelName?: string | null;
@@ -293,12 +295,16 @@ ${JSON.stringify(e)}
293
295
  topLevel: input.labelsAsLlmAgentName,
294
296
  startPreparation: input.startPreparation?.labelsAsLlmAgentName,
295
297
  });
298
+ const allowedIssueAuthors = resolveAllowedIssueAuthors({
299
+ topLevel: input.allowedIssueAuthors,
300
+ startPreparation: input.startPreparation?.allowedIssueAuthors,
301
+ });
296
302
  await this.revertNotReadyReviewQueueIssueUseCase.run({
297
303
  projectUrl: input.projectUrl,
298
304
  allowIssueCacheMinutes: input.allowIssueCacheMinutes,
299
305
  labelsAsLlmAgentName,
300
306
  changeTargetPathAliases: input.changeTargetPathAliases,
301
- allowedIssueAuthors: input.startPreparation?.allowedIssueAuthors ?? null,
307
+ allowedIssueAuthors,
302
308
  });
303
309
  if (this.dailySecurityScanUseCase !== null && input.dailySecurityScan) {
304
310
  await this.dailySecurityScanUseCase.run({
@@ -341,7 +347,7 @@ ${JSON.stringify(e)}
341
347
  input.startPreparation.maximumPreparingIssuesCount,
342
348
  utilizationPercentageThreshold:
343
349
  input.startPreparation.utilizationPercentageThreshold ?? 90,
344
- allowedIssueAuthors: input.startPreparation.allowedIssueAuthors ?? null,
350
+ allowedIssueAuthors,
345
351
  codexHomeCandidates: input.startPreparation.codexHomeCandidates ?? null,
346
352
  allowIssueCacheMinutes: input.allowIssueCacheMinutes,
347
353
  labelsAsLlmAgentName,
@@ -47,6 +47,7 @@ const issue = (url: string, status: string): Issue => ({
47
47
  isClosed: false,
48
48
  createdAt: new Date('2026-01-01T00:00:00Z'),
49
49
  author: 'hiromi',
50
+ closingIssueReferenceUrls: [],
50
51
  });
51
52
 
52
53
  describe('InTmuxByHumanSessionTokenCountUseCase', () => {
@@ -72,6 +72,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
72
72
  isClosed: false,
73
73
  createdAt: new Date(),
74
74
  author: '',
75
+ closingIssueReferenceUrls: [],
75
76
  ...overrides,
76
77
  });
77
78
 
@@ -79,7 +79,8 @@ 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
+ closingIssueReferenceUrls: [],
83
84
  ...overrides,
84
85
  });
85
86
 
@@ -170,6 +171,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
170
171
  await useCase.run({
171
172
  projectUrl: 'https://github.com/users/user/projects/1',
172
173
  allowIssueCacheMinutes: 10,
174
+ allowedIssueAuthors: ['owner'],
173
175
  });
174
176
 
175
177
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -189,6 +191,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
189
191
  await useCase.run({
190
192
  projectUrl: 'https://github.com/users/user/projects/1',
191
193
  allowIssueCacheMinutes: 10,
194
+ allowedIssueAuthors: ['owner'],
192
195
  });
193
196
 
194
197
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -208,6 +211,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
208
211
  await useCase.run({
209
212
  projectUrl: 'https://github.com/users/user/projects/1',
210
213
  allowIssueCacheMinutes: 10,
214
+ allowedIssueAuthors: ['owner'],
211
215
  });
212
216
 
213
217
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -240,6 +244,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
240
244
  projectUrl: 'https://github.com/users/user/projects/1',
241
245
  allowIssueCacheMinutes: 10,
242
246
  labelsAsLlmAgentName: ['story', 'chore', 'accounting'],
247
+ allowedIssueAuthors: ['owner'],
243
248
  });
244
249
 
245
250
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -261,6 +266,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
261
266
  projectUrl: 'https://github.com/users/user/projects/1',
262
267
  allowIssueCacheMinutes: 10,
263
268
  labelsAsLlmAgentName: ['story', 'chore'],
269
+ allowedIssueAuthors: ['owner'],
264
270
  });
265
271
 
266
272
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -281,6 +287,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
281
287
  await useCase.run({
282
288
  projectUrl: 'https://github.com/users/user/projects/1',
283
289
  allowIssueCacheMinutes: 10,
290
+ allowedIssueAuthors: ['owner'],
284
291
  });
285
292
 
286
293
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -309,6 +316,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
309
316
  projectUrl: 'https://github.com/users/user/projects/1',
310
317
  allowIssueCacheMinutes: 10,
311
318
  labelsAsLlmAgentName: null,
319
+ allowedIssueAuthors: ['owner'],
312
320
  });
313
321
 
314
322
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -333,6 +341,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
333
341
  await useCase.run({
334
342
  projectUrl: 'https://github.com/users/user/projects/1',
335
343
  allowIssueCacheMinutes: 10,
344
+ allowedIssueAuthors: ['owner'],
336
345
  });
337
346
 
338
347
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -354,6 +363,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
354
363
  await useCase.run({
355
364
  projectUrl: 'https://github.com/users/user/projects/1',
356
365
  allowIssueCacheMinutes: 10,
366
+ allowedIssueAuthors: ['owner'],
357
367
  });
358
368
 
359
369
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -390,6 +400,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
390
400
  await useCase.run({
391
401
  projectUrl: 'https://github.com/users/user/projects/1',
392
402
  allowIssueCacheMinutes: 10,
403
+ allowedIssueAuthors: ['owner'],
393
404
  });
394
405
 
395
406
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -422,6 +433,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
422
433
  await useCase.run({
423
434
  projectUrl: 'https://github.com/users/user/projects/1',
424
435
  allowIssueCacheMinutes: 10,
436
+ allowedIssueAuthors: ['owner'],
425
437
  });
426
438
 
427
439
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -454,6 +466,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
454
466
  await useCase.run({
455
467
  projectUrl: 'https://github.com/users/user/projects/1',
456
468
  allowIssueCacheMinutes: 10,
469
+ allowedIssueAuthors: ['owner'],
457
470
  });
458
471
 
459
472
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -487,6 +500,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
487
500
  await useCase.run({
488
501
  projectUrl: 'https://github.com/users/user/projects/1',
489
502
  allowIssueCacheMinutes: 10,
503
+ allowedIssueAuthors: ['owner'],
490
504
  });
491
505
 
492
506
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -524,6 +538,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
524
538
  await useCase.run({
525
539
  projectUrl: 'https://github.com/users/user/projects/1',
526
540
  allowIssueCacheMinutes: 10,
541
+ allowedIssueAuthors: ['owner'],
527
542
  });
528
543
 
529
544
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -561,6 +576,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
561
576
  useCase.run({
562
577
  projectUrl: 'https://github.com/users/user/projects/1',
563
578
  allowIssueCacheMinutes: 10,
579
+ allowedIssueAuthors: ['owner'],
564
580
  });
565
581
 
566
582
  it('should not approve PR when issue has no change-target label', async () => {
@@ -772,6 +788,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
772
788
  await useCase.run({
773
789
  projectUrl: 'https://github.com/users/user/projects/1',
774
790
  allowIssueCacheMinutes: 10,
791
+ allowedIssueAuthors: ['owner'],
775
792
  changeTargetPathAliases: {
776
793
  adapters: 'src/domain/usecases/adapter-interfaces',
777
794
  },
@@ -791,6 +808,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
791
808
  await useCase.run({
792
809
  projectUrl: 'https://github.com/users/user/projects/1',
793
810
  allowIssueCacheMinutes: 10,
811
+ allowedIssueAuthors: ['owner'],
794
812
  changeTargetPathAliases: {
795
813
  adapters: 'src/domain/usecases/adapter-interfaces',
796
814
  },
@@ -814,6 +832,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
814
832
  await useCase.run({
815
833
  projectUrl: 'https://github.com/users/user/projects/1',
816
834
  allowIssueCacheMinutes: 10,
835
+ allowedIssueAuthors: ['owner'],
817
836
  });
818
837
 
819
838
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -835,6 +854,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
835
854
  await useCase.run({
836
855
  projectUrl: 'https://github.com/users/user/projects/1',
837
856
  allowIssueCacheMinutes: 10,
857
+ allowedIssueAuthors: ['owner'],
838
858
  });
839
859
 
840
860
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -855,6 +875,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
855
875
  await useCase.run({
856
876
  projectUrl: 'https://github.com/users/user/projects/1',
857
877
  allowIssueCacheMinutes: 10,
878
+ allowedIssueAuthors: ['owner'],
858
879
  });
859
880
 
860
881
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -875,6 +896,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
875
896
  await useCase.run({
876
897
  projectUrl: 'https://github.com/users/user/projects/1',
877
898
  allowIssueCacheMinutes: 10,
899
+ allowedIssueAuthors: ['owner'],
878
900
  });
879
901
 
880
902
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -898,6 +920,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
898
920
  await useCase.run({
899
921
  projectUrl: 'https://github.com/users/user/projects/1',
900
922
  allowIssueCacheMinutes: 10,
923
+ allowedIssueAuthors: ['owner'],
901
924
  });
902
925
 
903
926
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -937,6 +960,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
937
960
  await useCase.run({
938
961
  projectUrl: 'https://github.com/users/user/projects/1',
939
962
  allowIssueCacheMinutes: 10,
963
+ allowedIssueAuthors: ['owner'],
940
964
  });
941
965
 
942
966
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -971,6 +995,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
971
995
  await useCase.run({
972
996
  projectUrl: 'https://github.com/users/user/projects/1',
973
997
  allowIssueCacheMinutes: 10,
998
+ allowedIssueAuthors: ['owner'],
974
999
  });
975
1000
 
976
1001
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1007,6 +1032,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1007
1032
  await useCase.run({
1008
1033
  projectUrl: 'https://github.com/users/user/projects/1',
1009
1034
  allowIssueCacheMinutes: 10,
1035
+ allowedIssueAuthors: ['owner'],
1010
1036
  });
1011
1037
 
1012
1038
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1041,6 +1067,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1041
1067
  await useCase.run({
1042
1068
  projectUrl: 'https://github.com/users/user/projects/1',
1043
1069
  allowIssueCacheMinutes: 10,
1070
+ allowedIssueAuthors: ['owner'],
1044
1071
  });
1045
1072
 
1046
1073
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1072,6 +1099,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1072
1099
  await useCase.run({
1073
1100
  projectUrl: 'https://github.com/users/user/projects/1',
1074
1101
  allowIssueCacheMinutes: 10,
1102
+ allowedIssueAuthors: ['owner'],
1075
1103
  });
1076
1104
 
1077
1105
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1109,6 +1137,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1109
1137
  await useCase.run({
1110
1138
  projectUrl: 'https://github.com/users/user/projects/1',
1111
1139
  allowIssueCacheMinutes: 10,
1140
+ allowedIssueAuthors: ['owner'],
1112
1141
  });
1113
1142
 
1114
1143
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
@@ -1153,6 +1182,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1153
1182
  await useCase.run({
1154
1183
  projectUrl: 'https://github.com/users/user/projects/1',
1155
1184
  allowIssueCacheMinutes: 10,
1185
+ allowedIssueAuthors: ['owner'],
1156
1186
  });
1157
1187
 
1158
1188
  expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
@@ -1222,7 +1252,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1222
1252
  );
1223
1253
  });
1224
1254
 
1225
- it('should act on an Unread pull request authored by a dependency-update bot not in allowedIssueAuthors', async () => {
1255
+ it('should skip an Unread pull request authored by a dependency-update bot not in allowedIssueAuthors', async () => {
1226
1256
  const pullRequest = createMockPullRequest({
1227
1257
  status: 'Unread',
1228
1258
  author: 'dependabot[bot]',
@@ -1242,6 +1272,31 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1242
1272
  allowedIssueAuthors: ['owner'],
1243
1273
  });
1244
1274
 
1275
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1276
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1277
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1278
+ });
1279
+
1280
+ it('should act on a dependency-update bot only when it is explicitly listed in allowedIssueAuthors', async () => {
1281
+ const pullRequest = createMockPullRequest({
1282
+ status: 'Unread',
1283
+ author: 'dependabot[bot]',
1284
+ });
1285
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1286
+ issues: [pullRequest],
1287
+ cacheUsed: false,
1288
+ });
1289
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1290
+ ...createReadyPr(),
1291
+ isConflicted: true,
1292
+ });
1293
+
1294
+ await useCase.run({
1295
+ projectUrl: 'https://github.com/users/user/projects/1',
1296
+ allowIssueCacheMinutes: 10,
1297
+ allowedIssueAuthors: ['owner', 'dependabot[bot]'],
1298
+ });
1299
+
1245
1300
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1246
1301
  mockProject,
1247
1302
  pullRequest,
@@ -1253,7 +1308,7 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1253
1308
  );
1254
1309
  });
1255
1310
 
1256
- it('should act on every author when allowedIssueAuthors is null', async () => {
1311
+ it('should process no author when allowedIssueAuthors is null', async () => {
1257
1312
  const pullRequest = createMockPullRequest({
1258
1313
  status: 'Unread',
1259
1314
  author: 'outside-contributor',
@@ -1273,15 +1328,58 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
1273
1328
  allowedIssueAuthors: null,
1274
1329
  });
1275
1330
 
1276
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1277
- mockProject,
1278
- pullRequest,
1279
- 'awaiting-workspace-id',
1280
- );
1281
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1282
- pullRequest,
1283
- expect.stringContaining('Auto Status Check: REJECTED'),
1284
- );
1331
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1332
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1333
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1334
+ });
1335
+
1336
+ it('should process no author when allowedIssueAuthors is undefined', async () => {
1337
+ const pullRequest = createMockPullRequest({
1338
+ status: 'Unread',
1339
+ author: 'owner',
1340
+ });
1341
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1342
+ issues: [pullRequest],
1343
+ cacheUsed: false,
1344
+ });
1345
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1346
+ ...createReadyPr(),
1347
+ isConflicted: true,
1348
+ });
1349
+
1350
+ await useCase.run({
1351
+ projectUrl: 'https://github.com/users/user/projects/1',
1352
+ allowIssueCacheMinutes: 10,
1353
+ });
1354
+
1355
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1356
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1357
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1358
+ });
1359
+
1360
+ it('should process no author when allowedIssueAuthors is an empty list', async () => {
1361
+ const pullRequest = createMockPullRequest({
1362
+ status: 'Unread',
1363
+ author: 'owner',
1364
+ });
1365
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1366
+ issues: [pullRequest],
1367
+ cacheUsed: false,
1368
+ });
1369
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1370
+ ...createReadyPr(),
1371
+ isConflicted: true,
1372
+ });
1373
+
1374
+ await useCase.run({
1375
+ projectUrl: 'https://github.com/users/user/projects/1',
1376
+ allowIssueCacheMinutes: 10,
1377
+ allowedIssueAuthors: [],
1378
+ });
1379
+
1380
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1381
+ expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
1382
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
1285
1383
  });
1286
1384
  });
1287
1385
  });