github-issue-tower-defence-management 1.60.1 → 1.63.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 (123) hide show
  1. package/.github/workflows/publish.yml +13 -0
  2. package/.github/workflows/test.yml +0 -4
  3. package/CHANGELOG.md +14 -0
  4. package/README.md +53 -10
  5. package/bin/adapter/entry-points/cli/index.js +11 -11
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
  8. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
  12. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
  13. package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
  14. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
  15. package/bin/adapter/proxy/TokenListLoader.js +21 -6
  16. package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
  17. package/bin/adapter/proxy/proxyEntry.js +1 -0
  18. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  19. package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
  20. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  21. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
  22. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  23. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
  24. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  25. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js +19 -9
  26. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js +15 -3
  28. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  29. package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  31. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
  32. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  33. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
  34. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  35. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
  36. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
  37. package/bin/domain/usecases/StartPreparationUseCase.js +91 -12
  38. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  39. package/package.json +1 -4
  40. package/src/adapter/entry-points/cli/index.test.ts +16 -16
  41. package/src/adapter/entry-points/cli/index.ts +8 -11
  42. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
  43. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
  44. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
  45. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
  46. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
  47. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
  48. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
  49. package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
  50. package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
  51. package/src/adapter/proxy/TokenListLoader.ts +25 -5
  52. package/src/adapter/proxy/proxyEntry.test.ts +270 -1
  53. package/src/adapter/proxy/proxyEntry.ts +2 -1
  54. package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
  55. package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
  56. package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
  57. package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
  58. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
  59. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
  60. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
  61. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
  62. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
  63. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
  64. package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
  65. package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +196 -11
  66. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +32 -15
  67. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  68. package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -5
  69. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
  70. package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
  71. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
  72. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
  73. package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
  74. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
  75. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
  76. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
  77. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
  78. package/src/domain/usecases/StartPreparationUseCase.test.ts +404 -21
  79. package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
  80. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
  81. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  82. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  83. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  84. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
  85. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
  86. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
  87. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
  88. package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
  89. package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
  90. package/types/adapter/proxy/proxyEntry.d.ts +2 -1
  91. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  92. package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
  93. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  94. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  95. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
  96. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  97. package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
  98. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  99. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +4 -2
  100. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  101. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  102. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  103. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
  104. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  105. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  106. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
  107. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
  108. package/types/domain/usecases/StartPreparationUseCase.d.ts +15 -1
  109. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  110. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
  111. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  112. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
  113. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
  114. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
  115. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
  116. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
  117. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
  118. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
  119. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
  120. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
  121. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
  122. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
  123. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
@@ -26,27 +26,51 @@ describe('CreateNewStoryByLabelUseCase', () => {
26
26
  },
27
27
  };
28
28
 
29
- const issueWithNewStoryLabel = {
29
+ const issueWithNewStoryLabel: Issue = {
30
30
  ...mock<Issue>(),
31
+ url: 'https://github.com/org/repo/issues/999',
31
32
  title: 'New Feature Request',
32
33
  number: 999,
34
+ story: 'Existing Story',
33
35
  labels: ['bug', 'new-story', 'priority-high'],
34
36
  };
35
37
 
36
- const issueWithNewStoryLabelVariant = {
38
+ const issueWithNewStoryLabelVariant: Issue = {
37
39
  ...mock<Issue>(),
40
+ url: 'https://github.com/org/repo/issues/1000',
38
41
  title: 'Another New Story',
39
42
  number: 1000,
43
+ story: 'Existing Story',
40
44
  labels: ['newstory', 'enhancement'],
41
45
  };
42
46
 
43
- const regularIssue = {
47
+ const regularIssue: Issue = {
44
48
  ...mock<Issue>(),
49
+ url: 'https://github.com/org/repo/issues/888',
45
50
  title: 'Regular Issue',
46
51
  number: 888,
52
+ story: 'Existing Story',
47
53
  labels: ['bug', 'priority-low'],
48
54
  };
49
55
 
56
+ const unassignedIssueWithNewStoryLabel: Issue = {
57
+ ...mock<Issue>(),
58
+ url: 'https://github.com/org/repo/issues/1383',
59
+ title: 'Unassigned Story Issue',
60
+ number: 1383,
61
+ story: null,
62
+ labels: ['new-story'],
63
+ };
64
+
65
+ const unassignedRegularIssue: Issue = {
66
+ ...mock<Issue>(),
67
+ url: 'https://github.com/org/repo/issues/1384',
68
+ title: 'Unassigned Regular Issue',
69
+ number: 1384,
70
+ story: null,
71
+ labels: ['bug'],
72
+ };
73
+
50
74
  const storyObjectWithNewStoryIssues: StoryObject = {
51
75
  story: {
52
76
  ...mock<StoryOption>(),
@@ -85,6 +109,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
85
109
  org: 'testOrg',
86
110
  repo: 'testRepo',
87
111
  storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
112
+ issues: [],
88
113
  });
89
114
 
90
115
  expect(mockProjectRepository.updateStoryList).not.toHaveBeenCalled();
@@ -107,6 +132,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
107
132
  storyObjectMap: new Map([
108
133
  ['Story 1', storyObjectWithoutNewStoryIssues],
109
134
  ]),
135
+ issues: [regularIssue, unassignedRegularIssue],
110
136
  });
111
137
 
112
138
  expect(mockProjectRepository.updateStoryList).not.toHaveBeenCalled();
@@ -141,6 +167,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
141
167
  org: 'testOrg',
142
168
  repo: 'testRepo',
143
169
  storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
170
+ issues: [],
144
171
  });
145
172
 
146
173
  expect(mockProjectRepository.updateStoryList).toHaveBeenCalledTimes(1);
@@ -207,11 +234,84 @@ describe('CreateNewStoryByLabelUseCase', () => {
207
234
  );
208
235
  });
209
236
 
237
+ it('should create new story for an unassigned issue with new-story label', async () => {
238
+ const savedStories: FieldOption[] = [
239
+ { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
240
+ {
241
+ id: 'newStoryId1',
242
+ name: 'Unassigned Story Issue',
243
+ description: '',
244
+ color: 'RED',
245
+ },
246
+ { id: 'story2', name: 'Middle Story', color: 'GREEN', description: '' },
247
+ { id: 'story3', name: 'Last Story', color: 'YELLOW', description: '' },
248
+ ];
249
+
250
+ mockProjectRepository.updateStoryList.mockResolvedValue(savedStories);
251
+
252
+ const emptyStoryObjectMap = new Map<string, StoryObject>();
253
+
254
+ await useCase.run({
255
+ project: basicProject,
256
+ cacheUsed: false,
257
+ org: 'testOrg',
258
+ repo: 'testRepo',
259
+ storyObjectMap: emptyStoryObjectMap,
260
+ issues: [unassignedIssueWithNewStoryLabel, unassignedRegularIssue],
261
+ });
262
+
263
+ expect(mockProjectRepository.updateStoryList).toHaveBeenCalledTimes(1);
264
+ expect(mockProjectRepository.updateStoryList).toHaveBeenCalledWith(
265
+ basicProject,
266
+ [
267
+ {
268
+ id: 'story1',
269
+ name: 'First Story',
270
+ color: 'BLUE',
271
+ description: '',
272
+ },
273
+ {
274
+ id: null,
275
+ name: 'Unassigned Story Issue',
276
+ description: '',
277
+ color: 'RED',
278
+ },
279
+ {
280
+ id: 'story2',
281
+ name: 'Middle Story',
282
+ color: 'GREEN',
283
+ description: '',
284
+ },
285
+ {
286
+ id: 'story3',
287
+ name: 'Last Story',
288
+ color: 'YELLOW',
289
+ description: '',
290
+ },
291
+ ],
292
+ );
293
+
294
+ expect(mockIssueRepository.updateStory).toHaveBeenCalledTimes(1);
295
+ expect(mockIssueRepository.updateStory).toHaveBeenCalledWith(
296
+ { ...basicProject, story: basicProject.story },
297
+ unassignedIssueWithNewStoryLabel,
298
+ 'newStoryId1',
299
+ );
300
+
301
+ expect(mockIssueRepository.updateLabels).toHaveBeenCalledTimes(1);
302
+ expect(mockIssueRepository.updateLabels).toHaveBeenCalledWith(
303
+ unassignedIssueWithNewStoryLabel,
304
+ [],
305
+ );
306
+ });
307
+
210
308
  it('should skip issues when no matching story is found in saved list', async () => {
211
- const issueWithUnmatchedTitle = {
309
+ const issueWithUnmatchedTitle: Issue = {
212
310
  ...mock<Issue>(),
311
+ url: 'https://github.com/org/repo/issues/1001',
213
312
  title: 'Unmatched Title',
214
313
  number: 1001,
314
+ story: 'Existing Story',
215
315
  labels: ['new-story'],
216
316
  };
217
317
 
@@ -237,6 +337,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
237
337
  org: 'testOrg',
238
338
  repo: 'testRepo',
239
339
  storyObjectMap: new Map([['Story 1', storyObject]]),
340
+ issues: [],
240
341
  });
241
342
 
242
343
  expect(mockProjectRepository.updateStoryList).toHaveBeenCalledTimes(1);
@@ -246,24 +347,30 @@ describe('CreateNewStoryByLabelUseCase', () => {
246
347
  });
247
348
 
248
349
  describe('findNewStoryIssues', () => {
249
- const issueWithNoHyphen = {
350
+ const issueWithNoHyphen: Issue = {
250
351
  ...mock<Issue>(),
352
+ url: 'https://github.com/org/repo/issues/1002',
251
353
  title: 'No Hyphen Issue',
252
354
  number: 1002,
355
+ story: 'Some Story',
253
356
  labels: ['newstory'],
254
357
  };
255
358
 
256
- const issueWithMixedCase = {
359
+ const issueWithMixedCase: Issue = {
257
360
  ...mock<Issue>(),
361
+ url: 'https://github.com/org/repo/issues/1003',
258
362
  title: 'Mixed Case Issue',
259
363
  number: 1003,
364
+ story: 'Some Story',
260
365
  labels: ['NEW-STORY'],
261
366
  };
262
367
 
263
- const anotherIssueWithLabel = {
368
+ const anotherIssueWithLabel: Issue = {
264
369
  ...mock<Issue>(),
370
+ url: 'https://github.com/org/repo/issues/1004',
265
371
  title: 'Another Issue',
266
372
  number: 1004,
373
+ story: 'Some Story',
267
374
  labels: ['newstory'],
268
375
  };
269
376
 
@@ -294,6 +401,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
294
401
  const testCases: Array<{
295
402
  description: string;
296
403
  storyObjectMap: Map<string, StoryObject>;
404
+ issues: Issue[];
297
405
  expectedLength: number;
298
406
  expectedIssues: Issue[];
299
407
  }> = [
@@ -301,24 +409,28 @@ describe('CreateNewStoryByLabelUseCase', () => {
301
409
  description:
302
410
  'should return empty array when no issues have new-story label',
303
411
  storyObjectMap: new Map([['Story 1', storyObjectWithRegularIssue]]),
412
+ issues: [unassignedRegularIssue],
304
413
  expectedLength: 0,
305
414
  expectedIssues: [],
306
415
  },
307
416
  {
308
417
  description: 'should find issues with "new-story" label (with hyphen)',
309
418
  storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
419
+ issues: [],
310
420
  expectedLength: 2,
311
421
  expectedIssues: [issueWithNewStoryLabel, issueWithNewStoryLabelVariant],
312
422
  },
313
423
  {
314
424
  description: 'should find issues with "newstory" label (no hyphen)',
315
425
  storyObjectMap: new Map([['Story 1', storyObjectWithNoHyphen]]),
426
+ issues: [],
316
427
  expectedLength: 1,
317
428
  expectedIssues: [issueWithNoHyphen],
318
429
  },
319
430
  {
320
431
  description: 'should find issues with mixed case "NEW-STORY" label',
321
432
  storyObjectMap: new Map([['Story 1', storyObjectWithMixedCase]]),
433
+ issues: [],
322
434
  expectedLength: 1,
323
435
  expectedIssues: [issueWithMixedCase],
324
436
  },
@@ -328,6 +440,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
328
440
  ['Story 1', storyObjectWithNewStoryIssues],
329
441
  ['Story 2', storyObjectWithAnotherIssue],
330
442
  ]),
443
+ issues: [],
331
444
  expectedLength: 3,
332
445
  expectedIssues: [
333
446
  issueWithNewStoryLabel,
@@ -335,12 +448,32 @@ describe('CreateNewStoryByLabelUseCase', () => {
335
448
  anotherIssueWithLabel,
336
449
  ],
337
450
  },
451
+ {
452
+ description:
453
+ 'should find unassigned issues with new-story label not present in storyObjectMap',
454
+ storyObjectMap: new Map<string, StoryObject>(),
455
+ issues: [unassignedIssueWithNewStoryLabel, unassignedRegularIssue],
456
+ expectedLength: 1,
457
+ expectedIssues: [unassignedIssueWithNewStoryLabel],
458
+ },
459
+ {
460
+ description:
461
+ 'should not duplicate issue that appears in both storyObjectMap and issues array',
462
+ storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
463
+ issues: [issueWithNewStoryLabel, unassignedIssueWithNewStoryLabel],
464
+ expectedLength: 3,
465
+ expectedIssues: [
466
+ issueWithNewStoryLabel,
467
+ issueWithNewStoryLabelVariant,
468
+ unassignedIssueWithNewStoryLabel,
469
+ ],
470
+ },
338
471
  ];
339
472
 
340
473
  test.each(testCases)(
341
474
  '$description',
342
- ({ storyObjectMap, expectedLength, expectedIssues }) => {
343
- const result = useCase.findNewStoryIssues(storyObjectMap);
475
+ ({ storyObjectMap, issues, expectedLength, expectedIssues }) => {
476
+ const result = useCase.findNewStoryIssues(storyObjectMap, issues);
344
477
 
345
478
  expect(result).toHaveLength(expectedLength);
346
479
  expectedIssues.forEach((issue) => {
@@ -367,6 +500,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
367
500
  description: string;
368
501
  projectStory: NonNullable<Project['story']>;
369
502
  storyObjectMap: Map<string, StoryObject>;
503
+ issues: Issue[];
370
504
  expected: (Omit<FieldOption, 'id'> & { id: FieldOption['id'] | null })[];
371
505
  }> = [
372
506
  {
@@ -399,6 +533,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
399
533
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
400
534
  },
401
535
  storyObjectMap: new Map([['Story 1', storyObjectWithSingleNewStory]]),
536
+ issues: [],
402
537
  expected: [
403
538
  { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
404
539
  {
@@ -450,6 +585,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
450
585
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
451
586
  },
452
587
  storyObjectMap: new Map([['Story 1', storyObjectWithNewStoryIssues]]),
588
+ issues: [],
453
589
  expected: [
454
590
  { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
455
591
  {
@@ -488,6 +624,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
488
624
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
489
625
  },
490
626
  storyObjectMap: new Map([['Story 1', storyObjectWithSingleNewStory]]),
627
+ issues: [],
491
628
  expected: [
492
629
  {
493
630
  id: null,
@@ -514,6 +651,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
514
651
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
515
652
  },
516
653
  storyObjectMap: new Map([['Story 1', storyObjectWithSingleNewStory]]),
654
+ issues: [],
517
655
  expected: [
518
656
  { id: 'story1', name: 'Only Story', color: 'BLUE', description: '' },
519
657
  {
@@ -547,6 +685,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
547
685
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
548
686
  },
549
687
  storyObjectMap: new Map([['Story 1', storyObjectWithSingleNewStory]]),
688
+ issues: [],
550
689
  expected: [
551
690
  { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
552
691
  {
@@ -592,6 +731,7 @@ describe('CreateNewStoryByLabelUseCase', () => {
592
731
  workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
593
732
  },
594
733
  storyObjectMap: new Map([['Story 1', storyObjectWithRegularIssueOnly]]),
734
+ issues: [unassignedRegularIssue],
595
735
  expected: [
596
736
  { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
597
737
  {
@@ -608,12 +748,57 @@ describe('CreateNewStoryByLabelUseCase', () => {
608
748
  },
609
749
  ],
610
750
  },
751
+ {
752
+ description:
753
+ 'should include unassigned issue with new-story label in new story list',
754
+ projectStory: {
755
+ name: 'Story Field',
756
+ fieldId: 'storyFieldId',
757
+ databaseId: 123,
758
+ stories: [
759
+ {
760
+ id: 'story1',
761
+ name: 'First Story',
762
+ color: 'BLUE',
763
+ description: '',
764
+ },
765
+ {
766
+ id: 'story2',
767
+ name: 'Middle Story',
768
+ color: 'GREEN',
769
+ description: '',
770
+ },
771
+ ],
772
+ workflowManagementStory: { id: 'workflow1', name: 'Workflow Story' },
773
+ },
774
+ storyObjectMap: new Map<string, StoryObject>(),
775
+ issues: [unassignedIssueWithNewStoryLabel],
776
+ expected: [
777
+ { id: 'story1', name: 'First Story', color: 'BLUE', description: '' },
778
+ {
779
+ id: null,
780
+ name: 'Unassigned Story Issue',
781
+ color: 'RED',
782
+ description: '',
783
+ },
784
+ {
785
+ id: 'story2',
786
+ name: 'Middle Story',
787
+ color: 'GREEN',
788
+ description: '',
789
+ },
790
+ ],
791
+ },
611
792
  ];
612
793
 
613
794
  test.each(testCases)(
614
795
  '$description',
615
- ({ projectStory, storyObjectMap, expected }) => {
616
- const result = useCase.createNewStoryList(projectStory, storyObjectMap);
796
+ ({ projectStory, storyObjectMap, issues, expected }) => {
797
+ const result = useCase.createNewStoryList(
798
+ projectStory,
799
+ storyObjectMap,
800
+ issues,
801
+ );
617
802
 
618
803
  expect(result).toEqual(expected);
619
804
  },
@@ -19,18 +19,23 @@ export class CreateNewStoryByLabelUseCase {
19
19
  org: string;
20
20
  repo: string;
21
21
  storyObjectMap: StoryObjectMap;
22
+ issues: Issue[];
22
23
  }): Promise<void> => {
23
24
  const projectStory = input.project.story;
24
25
  if (!projectStory) {
25
26
  return;
26
27
  }
27
- const newStoryIssues = this.findNewStoryIssues(input.storyObjectMap);
28
+ const newStoryIssues = this.findNewStoryIssues(
29
+ input.storyObjectMap,
30
+ input.issues,
31
+ );
28
32
  if (newStoryIssues.length === 0) {
29
33
  return;
30
34
  }
31
35
  const newStoryList = this.createNewStoryList(
32
36
  projectStory,
33
37
  input.storyObjectMap,
38
+ input.issues,
34
39
  );
35
40
  const savedNewStoryList = await this.projectRepository.updateStoryList(
36
41
  input.project,
@@ -55,26 +60,38 @@ export class CreateNewStoryByLabelUseCase {
55
60
  );
56
61
  }
57
62
  };
58
- findNewStoryIssues = (storyObjectMap: StoryObjectMap): Issue[] => {
59
- return Array.from(storyObjectMap.values())
63
+
64
+ hasNewStoryLabel = (issue: Issue): boolean =>
65
+ issue.labels?.some(
66
+ (label) => label.toLowerCase().replace('-', '') === 'newstory',
67
+ ) ?? false;
68
+
69
+ findNewStoryIssues = (
70
+ storyObjectMap: StoryObjectMap,
71
+ issues: Issue[],
72
+ ): Issue[] => {
73
+ const issuesInMap = Array.from(storyObjectMap.values())
60
74
  .flatMap((storyObject) => storyObject.issues)
61
- .filter((issue) =>
62
- issue.labels?.some(
63
- (label) => label.toLowerCase().replace('-', '') === 'newstory',
64
- ),
65
- );
75
+ .filter(this.hasNewStoryLabel);
76
+ const unassignedIssuesWithLabel = issues
77
+ .filter((issue) => issue.story === null)
78
+ .filter(this.hasNewStoryLabel);
79
+ const seen = new Set<string>();
80
+ return [...issuesInMap, ...unassignedIssuesWithLabel].filter((issue) => {
81
+ if (seen.has(issue.url)) {
82
+ return false;
83
+ }
84
+ seen.add(issue.url);
85
+ return true;
86
+ });
66
87
  };
88
+
67
89
  createNewStoryList = (
68
90
  projectStory: NonNullable<Project['story']>,
69
91
  storyObjectMap: StoryObjectMap,
92
+ issues: Issue[],
70
93
  ): (Omit<FieldOption, 'id'> & { id: FieldOption['id'] | null })[] => {
71
- const newStoryIssues = Array.from(storyObjectMap.values())
72
- .flatMap((storyObject) => storyObject.issues)
73
- .filter((issue) =>
74
- issue.labels?.some(
75
- (label) => label.toLowerCase().replace('-', '') === 'newstory',
76
- ),
77
- );
94
+ const newStoryIssues = this.findNewStoryIssues(storyObjectMap, issues);
78
95
  const newStoryList: (Omit<FieldOption, 'id'> & {
79
96
  id: FieldOption['id'] | null;
80
97
  })[] = [];
@@ -159,6 +159,9 @@ describe('HandleScheduledEventUseCase', () => {
159
159
  ['LastExecutionDateTime'],
160
160
  ['2024-01-01T00:00:00Z'],
161
161
  ]);
162
+ mockStartPreparationUseCase.run.mockResolvedValue({
163
+ rotationOrder: null,
164
+ });
162
165
  });
163
166
 
164
167
  it('should call AnalyzeProblemByIssueUseCase with correct parameters', async () => {
@@ -377,6 +380,7 @@ describe('HandleScheduledEventUseCase', () => {
377
380
  });
378
381
  mockStartPreparationUseCase.run.mockImplementation(async () => {
379
382
  callOrder.push('startPreparation');
383
+ return { rotationOrder: null };
380
384
  });
381
385
 
382
386
  const input = {
@@ -19,7 +19,10 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
19
19
  import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
20
20
  import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
21
21
  import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
22
- import { StartPreparationUseCase } from './StartPreparationUseCase';
22
+ import {
23
+ RotationOrderEntry,
24
+ StartPreparationUseCase,
25
+ } from './StartPreparationUseCase';
23
26
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
24
27
  import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
25
28
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
@@ -96,6 +99,7 @@ export class HandleScheduledEventUseCase {
96
99
  cacheUsed: boolean;
97
100
  targetDateTimes: Date[];
98
101
  storyIssues: StoryObjectMap;
102
+ rotationOrder: RotationOrderEntry[] | null;
99
103
  } | null> => {
100
104
  if (input.disabled) {
101
105
  return null;
@@ -205,8 +209,9 @@ export class HandleScheduledEventUseCase {
205
209
  now,
206
210
  );
207
211
 
212
+ let rotationOrder: RotationOrderEntry[] | null = null;
208
213
  try {
209
- await this.runEachUseCases(
214
+ const useCaseResult = await this.runEachUseCases(
210
215
  input,
211
216
  project,
212
217
  issues,
@@ -215,6 +220,7 @@ export class HandleScheduledEventUseCase {
215
220
  storyIssues,
216
221
  runSlowSweep,
217
222
  );
223
+ rotationOrder = useCaseResult.rotationOrder;
218
224
  } catch (e) {
219
225
  if (!(e instanceof Error)) {
220
226
  throw e;
@@ -238,7 +244,14 @@ ${JSON.stringify(e)}
238
244
  throw e;
239
245
  }
240
246
 
241
- return { project, issues, cacheUsed, targetDateTimes, storyIssues };
247
+ return {
248
+ project,
249
+ issues,
250
+ cacheUsed,
251
+ targetDateTimes,
252
+ storyIssues,
253
+ rotationOrder,
254
+ };
242
255
  };
243
256
  runEachUseCases = async (
244
257
  input: Parameters<HandleScheduledEventUseCase['run']>[0],
@@ -248,7 +261,7 @@ ${JSON.stringify(e)}
248
261
  targetDateTimes: Date[],
249
262
  storyObjectMap: StoryObjectMap,
250
263
  runSlowSweep: boolean,
251
- ): Promise<void> => {
264
+ ): Promise<{ rotationOrder: RotationOrderEntry[] | null }> => {
252
265
  if (runSlowSweep) {
253
266
  await this.runSlowSweepUseCases(
254
267
  input,
@@ -284,7 +297,7 @@ ${JSON.stringify(e)}
284
297
  input.startPreparation.awaitingQualityCheckStatus ?? undefined,
285
298
  });
286
299
  }
287
- await this.startPreparationUseCase.run({
300
+ const preparationResult = await this.startPreparationUseCase.run({
288
301
  projectUrl: input.projectUrl,
289
302
  defaultAgentName: input.startPreparation.defaultAgentName,
290
303
  defaultLlmModelName: input.startPreparation.defaultLlmModelName ?? null,
@@ -298,7 +311,9 @@ ${JSON.stringify(e)}
298
311
  codexHomeCandidates: input.startPreparation.codexHomeCandidates ?? null,
299
312
  allowIssueCacheMinutes: input.allowIssueCacheMinutes,
300
313
  });
314
+ return { rotationOrder: preparationResult.rotationOrder };
301
315
  }
316
+ return { rotationOrder: null };
302
317
  };
303
318
  runSlowSweepUseCases = async (
304
319
  input: Parameters<HandleScheduledEventUseCase['run']>[0],
@@ -388,6 +403,7 @@ ${JSON.stringify(e)}
388
403
  org: input.org,
389
404
  repo: input.workingReport.repo,
390
405
  storyObjectMap: storyObjectMap,
406
+ issues: issues,
391
407
  });
392
408
  await this.assignNoAssigneeIssueToManagerUseCase.run({
393
409
  issues,