github-issue-tower-defence-management 1.46.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +70 -25
  3. package/bin/adapter/entry-points/cli/index.js +2 -104
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +0 -15
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +28 -62
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/situationFileWriter.js +98 -0
  10. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -0
  11. package/bin/adapter/repositories/GraphqlProjectRepository.js +37 -0
  12. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  13. package/bin/domain/entities/WorkflowStatus.js +36 -0
  14. package/bin/domain/entities/WorkflowStatus.js.map +1 -0
  15. package/bin/domain/usecases/AnalyzeStoriesUseCase.js +2 -1
  16. package/bin/domain/usecases/AnalyzeStoriesUseCase.js.map +1 -1
  17. package/bin/domain/usecases/ChangeStatusByStoryColorUseCase.js +4 -3
  18. package/bin/domain/usecases/ChangeStatusByStoryColorUseCase.js.map +1 -1
  19. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js +2 -1
  20. package/bin/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.js.map +1 -1
  21. package/bin/domain/usecases/CreateEstimationIssueUseCase.js +2 -1
  22. package/bin/domain/usecases/CreateEstimationIssueUseCase.js.map +1 -1
  23. package/bin/domain/usecases/CreateNewStoryByLabelUseCase.js.map +1 -1
  24. package/bin/domain/usecases/HandleScheduledEventUseCase.js +9 -17
  25. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  26. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +13 -15
  27. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  28. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +4 -5
  29. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  30. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +47 -0
  31. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -0
  32. package/bin/domain/usecases/StartPreparationUseCase.js +7 -8
  33. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  34. package/bin/domain/usecases/UpdateIssueStatusByLabelUseCase.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/adapter/entry-points/cli/index.test.ts +8 -258
  37. package/src/adapter/entry-points/cli/index.ts +6 -106
  38. package/src/adapter/entry-points/cli/projectConfig.ts +0 -33
  39. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +24 -58
  40. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +36 -41
  41. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +417 -0
  42. package/src/adapter/entry-points/handlers/situationFileWriter.ts +168 -0
  43. package/src/adapter/repositories/GraphqlProjectRepository.ts +55 -1
  44. package/src/domain/entities/WorkflowStatus.ts +41 -0
  45. package/src/domain/usecases/AnalyzeStoriesUseCase.ts +2 -2
  46. package/src/domain/usecases/ChangeStatusByStoryColorUseCase.test.ts +5 -10
  47. package/src/domain/usecases/ChangeStatusByStoryColorUseCase.ts +4 -4
  48. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.test.ts +0 -11
  49. package/src/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.ts +2 -2
  50. package/src/domain/usecases/CreateEstimationIssueUseCase.ts +2 -2
  51. package/src/domain/usecases/CreateNewStoryByLabelUseCase.test.ts +0 -4
  52. package/src/domain/usecases/CreateNewStoryByLabelUseCase.ts +0 -1
  53. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -41
  54. package/src/domain/usecases/HandleScheduledEventUseCase.ts +9 -27
  55. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +0 -202
  56. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +18 -31
  57. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +13 -101
  58. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +10 -10
  59. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +187 -0
  60. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +69 -0
  61. package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -151
  62. package/src/domain/usecases/StartPreparationUseCase.ts +11 -20
  63. package/src/domain/usecases/UpdateIssueStatusByLabelUseCase.test.ts +2 -47
  64. package/src/domain/usecases/UpdateIssueStatusByLabelUseCase.ts +1 -5
  65. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -1
  66. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  67. package/types/adapter/entry-points/cli/projectConfig.d.ts +0 -3
  68. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  69. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  70. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +30 -0
  71. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -0
  72. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +4 -1
  73. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  74. package/types/domain/entities/WorkflowStatus.d.ts +13 -0
  75. package/types/domain/entities/WorkflowStatus.d.ts.map +1 -0
  76. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts +0 -1
  77. package/types/domain/usecases/AnalyzeStoriesUseCase.d.ts.map +1 -1
  78. package/types/domain/usecases/ChangeStatusByStoryColorUseCase.d.ts +0 -1
  79. package/types/domain/usecases/ChangeStatusByStoryColorUseCase.d.ts.map +1 -1
  80. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts +0 -1
  81. package/types/domain/usecases/ConvertCheckboxToIssueInStoryIssueUseCase.d.ts.map +1 -1
  82. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts +0 -1
  83. package/types/domain/usecases/CreateEstimationIssueUseCase.d.ts.map +1 -1
  84. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts +0 -1
  85. package/types/domain/usecases/CreateNewStoryByLabelUseCase.d.ts.map +1 -1
  86. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -8
  87. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  88. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -4
  89. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  90. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +0 -3
  91. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  92. package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts +10 -0
  93. package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -0
  94. package/types/domain/usecases/StartPreparationUseCase.d.ts +1 -3
  95. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  96. package/types/domain/usecases/UpdateIssueStatusByLabelUseCase.d.ts +0 -1
  97. package/types/domain/usecases/UpdateIssueStatusByLabelUseCase.d.ts.map +1 -1
  98. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -1
  99. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
@@ -158,9 +158,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
158
158
 
159
159
  await useCase.run({
160
160
  projectUrl: 'https://github.com/user/repo',
161
- preparationStatus: 'Preparation',
162
- awaitingWorkspaceStatus: 'Awaiting Workspace',
163
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
164
161
  allowIssueCacheMinutes: 60,
165
162
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
166
163
  });
@@ -206,9 +203,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
206
203
 
207
204
  await useCase.run({
208
205
  projectUrl: 'https://github.com/user/repo',
209
- preparationStatus: 'Preparation',
210
- awaitingWorkspaceStatus: 'Awaiting Workspace',
211
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
212
206
  allowIssueCacheMinutes: 60,
213
207
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
214
208
  });
@@ -247,9 +241,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
247
241
 
248
242
  await useCase.run({
249
243
  projectUrl: 'https://github.com/user/repo',
250
- preparationStatus: 'Preparation',
251
- awaitingWorkspaceStatus: 'Awaiting Workspace',
252
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
253
244
  allowIssueCacheMinutes: 60,
254
245
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
255
246
  });
@@ -283,46 +274,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
283
274
 
284
275
  await useCase.run({
285
276
  projectUrl: 'https://github.com/user/repo',
286
- preparationStatus: 'Preparation',
287
- awaitingWorkspaceStatus: 'Awaiting Workspace',
288
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
289
- allowIssueCacheMinutes: 60,
290
- preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
291
- });
292
-
293
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
294
- expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
295
- });
296
-
297
- it('should revert orphaned issue to Awaiting Workspace when awaitingQualityCheckStatus is not provided', async () => {
298
- const stuckIssue = createMockIssue({
299
- url: 'https://github.com/user/repo/issues/10',
300
- status: 'Preparation',
301
- });
302
- mockIssueRepository.getAllIssues.mockResolvedValue({
303
- issues: [stuckIssue],
304
- cacheUsed: false,
305
- });
306
- mockLocalCommandRunner.runCommand.mockResolvedValue({
307
- stdout: '',
308
- stderr: '',
309
- exitCode: 1,
310
- });
311
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
312
- {
313
- author: 'bot',
314
- content: 'From: agent report',
315
- createdAt: new Date(),
316
- },
317
- ]);
318
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
319
- createPassingPr(),
320
- ]);
321
-
322
- await useCase.run({
323
- projectUrl: 'https://github.com/user/repo',
324
- preparationStatus: 'Preparation',
325
- awaitingWorkspaceStatus: 'Awaiting Workspace',
326
277
  allowIssueCacheMinutes: 60,
327
278
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
328
279
  });
@@ -356,9 +307,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
356
307
 
357
308
  await useCase.run({
358
309
  projectUrl: 'https://github.com/user/repo',
359
- preparationStatus: 'Preparation',
360
- awaitingWorkspaceStatus: 'Awaiting Workspace',
361
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
362
310
  allowIssueCacheMinutes: 60,
363
311
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
364
312
  });
@@ -393,9 +341,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
393
341
 
394
342
  await useCase.run({
395
343
  projectUrl: 'https://github.com/user/repo',
396
- preparationStatus: 'Preparation',
397
- awaitingWorkspaceStatus: 'Awaiting Workspace',
398
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
399
344
  allowIssueCacheMinutes: 60,
400
345
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
401
346
  });
@@ -422,9 +367,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
422
367
 
423
368
  await useCase.run({
424
369
  projectUrl: 'https://github.com/user/repo',
425
- preparationStatus: 'Preparation',
426
- awaitingWorkspaceStatus: 'Awaiting Workspace',
427
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
428
370
  allowIssueCacheMinutes: 60,
429
371
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
430
372
  });
@@ -449,9 +391,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
449
391
 
450
392
  await useCase.run({
451
393
  projectUrl: 'https://github.com/user/repo',
452
- preparationStatus: 'Preparation',
453
- awaitingWorkspaceStatus: 'Awaiting Workspace',
454
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
455
394
  allowIssueCacheMinutes: 60,
456
395
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
457
396
  });
@@ -484,9 +423,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
484
423
 
485
424
  await useCase.run({
486
425
  projectUrl: 'https://github.com/user/repo',
487
- preparationStatus: 'Preparation',
488
- awaitingWorkspaceStatus: 'Awaiting Workspace',
489
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
490
426
  allowIssueCacheMinutes: 60,
491
427
  preparationProcessCheckCommand: 'check {URL}',
492
428
  });
@@ -526,9 +462,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
526
462
 
527
463
  await useCase.run({
528
464
  projectUrl: 'https://github.com/user/repo',
529
- preparationStatus: 'Preparation',
530
- awaitingWorkspaceStatus: 'Awaiting Workspace',
531
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
532
465
  allowIssueCacheMinutes: 60,
533
466
  preparationProcessCheckCommand: 'check {URL}',
534
467
  });
@@ -543,9 +476,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
543
476
  await expect(
544
477
  useCase.run({
545
478
  projectUrl: 'https://github.com/user/repo',
546
- preparationStatus: 'Preparation',
547
- awaitingWorkspaceStatus: 'Awaiting Workspace',
548
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
549
479
  allowIssueCacheMinutes: 0,
550
480
  preparationProcessCheckCommand: 'check {URL}',
551
481
  }),
@@ -559,20 +489,29 @@ describe('RevertOrphanedPreparationUseCase', () => {
559
489
  await expect(
560
490
  useCase.run({
561
491
  projectUrl: 'https://github.com/user/repo',
562
- preparationStatus: 'Preparation',
563
- awaitingWorkspaceStatus: 'Awaiting Workspace',
564
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
565
492
  allowIssueCacheMinutes: 0,
566
493
  preparationProcessCheckCommand: 'check {URL}',
567
494
  }),
568
495
  ).rejects.toThrow('Project not found. projectId: project-1');
569
496
  });
570
497
 
571
- it('should do nothing when awaitingWorkspaceStatus is not found in project statuses', async () => {
498
+ it('should do nothing when Awaiting Workspace status is not found in project statuses', async () => {
572
499
  const preparationIssue = createMockIssue({
573
500
  url: 'https://github.com/user/repo/issues/10',
574
501
  status: 'Preparation',
575
502
  });
503
+ const projectWithoutAwaitingWorkspace = {
504
+ ...mockProject,
505
+ status: {
506
+ ...mockProject.status,
507
+ statuses: mockProject.status.statuses.filter(
508
+ (s) => s.name !== 'Awaiting Workspace',
509
+ ),
510
+ },
511
+ };
512
+ mockProjectRepository.getProject.mockResolvedValue(
513
+ projectWithoutAwaitingWorkspace,
514
+ );
576
515
  mockIssueRepository.getAllIssues.mockResolvedValue({
577
516
  issues: [preparationIssue],
578
517
  cacheUsed: false,
@@ -585,9 +524,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
585
524
 
586
525
  await useCase.run({
587
526
  projectUrl: 'https://github.com/user/repo',
588
- preparationStatus: 'Preparation',
589
- awaitingWorkspaceStatus: 'NonExistentStatus',
590
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
591
527
  allowIssueCacheMinutes: 0,
592
528
  preparationProcessCheckCommand: 'check {URL}',
593
529
  });
@@ -606,9 +542,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
606
542
 
607
543
  await useCase.run({
608
544
  projectUrl: 'https://github.com/user/repo',
609
- preparationStatus: 'Preparation',
610
- awaitingWorkspaceStatus: 'Awaiting Workspace',
611
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
612
545
  allowIssueCacheMinutes: 60,
613
546
  preparationProcessCheckCommand: 'check {URL}',
614
547
  });
@@ -645,9 +578,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
645
578
 
646
579
  await useCase.run({
647
580
  projectUrl: 'https://github.com/user/repo',
648
- preparationStatus: 'Preparation',
649
- awaitingWorkspaceStatus: 'Awaiting Workspace',
650
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
651
581
  allowIssueCacheMinutes: 60,
652
582
  preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
653
583
  awLogDirectoryPath: '/home/user/logs-aw',
@@ -710,9 +640,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
710
640
 
711
641
  await useCase.run({
712
642
  projectUrl: 'https://github.com/user/repo',
713
- preparationStatus: 'Preparation',
714
- awaitingWorkspaceStatus: 'Awaiting Workspace',
715
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
716
643
  allowIssueCacheMinutes: 60,
717
644
  preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
718
645
  awLogDirectoryPath: '/home/user/logs-aw',
@@ -747,9 +674,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
747
674
 
748
675
  await useCase.run({
749
676
  projectUrl: 'https://github.com/user/repo',
750
- preparationStatus: 'Preparation',
751
- awaitingWorkspaceStatus: 'Awaiting Workspace',
752
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
753
677
  allowIssueCacheMinutes: 60,
754
678
  preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
755
679
  awLogDirectoryPath: '/home/user/logs-aw',
@@ -780,9 +704,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
780
704
 
781
705
  await useCase.run({
782
706
  projectUrl: 'https://github.com/user/repo',
783
- preparationStatus: 'Preparation',
784
- awaitingWorkspaceStatus: 'Awaiting Workspace',
785
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
786
707
  allowIssueCacheMinutes: 60,
787
708
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
788
709
  });
@@ -812,9 +733,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
812
733
 
813
734
  await useCase.run({
814
735
  projectUrl: 'https://github.com/user/repo',
815
- preparationStatus: 'Preparation',
816
- awaitingWorkspaceStatus: 'Awaiting Workspace',
817
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
818
736
  allowIssueCacheMinutes: 60,
819
737
  preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
820
738
  awLogDirectoryPath: '/home/user/logs-aw',
@@ -842,9 +760,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
842
760
 
843
761
  await useCase.run({
844
762
  projectUrl: 'https://github.com/user/repo',
845
- preparationStatus: 'Preparation',
846
- awaitingWorkspaceStatus: 'Awaiting Workspace',
847
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
848
763
  allowIssueCacheMinutes: 0,
849
764
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
850
765
  });
@@ -877,9 +792,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
877
792
 
878
793
  await useCase.run({
879
794
  projectUrl: 'https://github.com/user/repo',
880
- preparationStatus: 'Preparation',
881
- awaitingWorkspaceStatus: 'Awaiting Workspace',
882
- awaitingQualityCheckStatus: 'Awaiting Quality Check',
883
795
  allowIssueCacheMinutes: 60,
884
796
  preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
885
797
  });
@@ -6,6 +6,11 @@ import { IssueCommentRepository } from './adapter-interfaces/IssueCommentReposit
6
6
  import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
7
7
  import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
8
8
  import { Issue } from '../entities/Issue';
9
+ import {
10
+ AWAITING_QUALITY_CHECK_STATUS_NAME,
11
+ AWAITING_WORKSPACE_STATUS_NAME,
12
+ PREPARATION_STATUS_NAME,
13
+ } from '../entities/WorkflowStatus';
9
14
 
10
15
  export class RevertOrphanedPreparationUseCase {
11
16
  constructor(
@@ -29,9 +34,6 @@ export class RevertOrphanedPreparationUseCase {
29
34
 
30
35
  run = async (params: {
31
36
  projectUrl: string;
32
- preparationStatus: string;
33
- awaitingWorkspaceStatus: string;
34
- awaitingQualityCheckStatus?: string;
35
37
  allowIssueCacheMinutes: number;
36
38
  preparationProcessCheckCommand: string;
37
39
  awLogDirectoryPath?: string;
@@ -55,21 +57,19 @@ export class RevertOrphanedPreparationUseCase {
55
57
  );
56
58
 
57
59
  const preparationIssues = issues.filter(
58
- (issue) => issue.status === params.preparationStatus,
60
+ (issue) => issue.status === PREPARATION_STATUS_NAME,
59
61
  );
60
62
 
61
63
  const awaitingWorkspaceStatusOption = project.status.statuses.find(
62
- (s) => s.name === params.awaitingWorkspaceStatus,
64
+ (s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
63
65
  );
64
66
  if (!awaitingWorkspaceStatusOption) {
65
67
  return;
66
68
  }
67
69
 
68
- const awaitingQualityCheckStatusOption = params.awaitingQualityCheckStatus
69
- ? project.status.statuses.find(
70
- (s) => s.name === params.awaitingQualityCheckStatus,
71
- )
72
- : null;
70
+ const awaitingQualityCheckStatusOption = project.status.statuses.find(
71
+ (s) => s.name === AWAITING_QUALITY_CHECK_STATUS_NAME,
72
+ );
73
73
 
74
74
  for (const issue of preparationIssues) {
75
75
  const isOrphaned = await this.isOrphanedIssue(issue, params);
@@ -0,0 +1,187 @@
1
+ import { mock } from 'jest-mock-extended';
2
+ import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
3
+ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
4
+ import { FieldOption, Project } from '../entities/Project';
5
+ import {
6
+ AWAITING_QUALITY_CHECK_STATUS_NAME,
7
+ AWAITING_WORKSPACE_STATUS_NAME,
8
+ DEFAULT_STATUS_NAME,
9
+ DISABLED_STATUS_NAME,
10
+ PREPARATION_STATUS_NAME,
11
+ REQUIRED_WORKFLOW_STATUSES,
12
+ } from '../entities/WorkflowStatus';
13
+
14
+ const buildProject = (statuses: FieldOption[]): Project => ({
15
+ id: 'project-1',
16
+ url: 'https://github.com/orgs/test-org/projects/1',
17
+ databaseId: 1,
18
+ name: 'test-project',
19
+ status: {
20
+ name: 'Status',
21
+ fieldId: 'status-field-1',
22
+ statuses,
23
+ },
24
+ nextActionDate: null,
25
+ nextActionHour: null,
26
+ story: null,
27
+ remainingEstimationMinutes: null,
28
+ dependedIssueUrlSeparatedByComma: null,
29
+ completionDate50PercentConfidence: null,
30
+ });
31
+
32
+ const buildCanonicalStatuses = (): FieldOption[] =>
33
+ REQUIRED_WORKFLOW_STATUSES.map((required, index) => ({
34
+ id: `id-${index}`,
35
+ name: required.name,
36
+ color: required.color,
37
+ description: required.description,
38
+ }));
39
+
40
+ describe('SetupTowerDefenceProjectUseCase', () => {
41
+ it('should skip update when project already has required statuses in canonical order', async () => {
42
+ const mockProjectRepository =
43
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
44
+ const project = buildProject(buildCanonicalStatuses());
45
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
46
+
47
+ const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
48
+ await useCase.run({ projectUrl: project.url });
49
+
50
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('should skip update when project already has required statuses plus extras after them', async () => {
54
+ const mockProjectRepository =
55
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
56
+ const statuses = [
57
+ ...buildCanonicalStatuses(),
58
+ {
59
+ id: 'extra-1',
60
+ name: 'Done',
61
+ color: 'GREEN' as const,
62
+ description: '',
63
+ },
64
+ ];
65
+ const project = buildProject(statuses);
66
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
67
+
68
+ const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
69
+ await useCase.run({ projectUrl: project.url });
70
+
71
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
72
+ });
73
+
74
+ it('should add missing required statuses while preserving existing custom statuses', async () => {
75
+ const mockProjectRepository =
76
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
77
+ const statuses: FieldOption[] = [
78
+ {
79
+ id: 'unread-id',
80
+ name: DEFAULT_STATUS_NAME,
81
+ color: 'GRAY',
82
+ description: 'Default fallback status for issues before triage',
83
+ },
84
+ {
85
+ id: 'extra-1',
86
+ name: 'Done',
87
+ color: 'GREEN',
88
+ description: '',
89
+ },
90
+ ];
91
+ const project = buildProject(statuses);
92
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
93
+ mockProjectRepository.updateStatusList.mockResolvedValue([]);
94
+
95
+ const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
96
+ await useCase.run({ projectUrl: project.url });
97
+
98
+ expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
99
+ expect(mockProjectRepository.updateStatusList).toHaveBeenCalledWith(
100
+ project,
101
+ [
102
+ {
103
+ id: 'unread-id',
104
+ name: DEFAULT_STATUS_NAME,
105
+ color: 'GRAY',
106
+ description: 'Default fallback status for issues before triage',
107
+ },
108
+ {
109
+ id: null,
110
+ name: AWAITING_WORKSPACE_STATUS_NAME,
111
+ color: 'YELLOW',
112
+ description: 'Issue is ready and waiting for an agent workspace',
113
+ },
114
+ {
115
+ id: null,
116
+ name: PREPARATION_STATUS_NAME,
117
+ color: 'ORANGE',
118
+ description: 'Agent is preparing the issue',
119
+ },
120
+ {
121
+ id: null,
122
+ name: AWAITING_QUALITY_CHECK_STATUS_NAME,
123
+ color: 'BLUE',
124
+ description: 'Awaiting human quality check',
125
+ },
126
+ {
127
+ id: null,
128
+ name: DISABLED_STATUS_NAME,
129
+ color: 'GRAY',
130
+ description: 'Disabled and excluded from the active workflow',
131
+ },
132
+ {
133
+ id: 'extra-1',
134
+ name: 'Done',
135
+ color: 'GREEN',
136
+ description: '',
137
+ },
138
+ ],
139
+ );
140
+ });
141
+
142
+ it('should reorder existing required statuses into canonical order when out of order', async () => {
143
+ const mockProjectRepository =
144
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
145
+ const reversedStatuses: FieldOption[] = REQUIRED_WORKFLOW_STATUSES.slice()
146
+ .reverse()
147
+ .map((required, index) => ({
148
+ id: `reversed-id-${index}`,
149
+ name: required.name,
150
+ color: required.color,
151
+ description: required.description,
152
+ }));
153
+ const project = buildProject(reversedStatuses);
154
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
155
+ mockProjectRepository.updateStatusList.mockResolvedValue([]);
156
+
157
+ const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
158
+ await useCase.run({ projectUrl: project.url });
159
+
160
+ expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
161
+ const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
162
+ expect(payload.map((status) => status.name)).toEqual([
163
+ DEFAULT_STATUS_NAME,
164
+ AWAITING_WORKSPACE_STATUS_NAME,
165
+ PREPARATION_STATUS_NAME,
166
+ AWAITING_QUALITY_CHECK_STATUS_NAME,
167
+ DISABLED_STATUS_NAME,
168
+ ]);
169
+ });
170
+
171
+ it('should fix color when an existing required status has wrong color', async () => {
172
+ const mockProjectRepository =
173
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
174
+ const statuses = buildCanonicalStatuses();
175
+ statuses[2] = { ...statuses[2], color: 'RED' };
176
+ const project = buildProject(statuses);
177
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
178
+ mockProjectRepository.updateStatusList.mockResolvedValue([]);
179
+
180
+ const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
181
+ await useCase.run({ projectUrl: project.url });
182
+
183
+ expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
184
+ const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
185
+ expect(payload[2].color).toBe('ORANGE');
186
+ });
187
+ });
@@ -0,0 +1,69 @@
1
+ import { FieldOption } from '../entities/Project';
2
+ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
3
+ import {
4
+ REQUIRED_WORKFLOW_STATUSES,
5
+ WorkflowStatusDefinition,
6
+ } from '../entities/WorkflowStatus';
7
+
8
+ export class SetupTowerDefenceProjectUseCase {
9
+ constructor(
10
+ private readonly projectRepository: Pick<
11
+ ProjectRepository,
12
+ 'getByUrl' | 'updateStatusList'
13
+ >,
14
+ ) {}
15
+
16
+ run = async (params: { projectUrl: string }): Promise<void> => {
17
+ const project = await this.projectRepository.getByUrl(params.projectUrl);
18
+ const existing = project.status.statuses;
19
+
20
+ if (
21
+ SetupTowerDefenceProjectUseCase.hasRequiredStatusesInCanonicalOrder(
22
+ existing,
23
+ )
24
+ ) {
25
+ return;
26
+ }
27
+
28
+ const requiredNames = new Set(
29
+ REQUIRED_WORKFLOW_STATUSES.map((s) => s.name),
30
+ );
31
+ const others = existing.filter((status) => !requiredNames.has(status.name));
32
+
33
+ const newStatusList: (Omit<FieldOption, 'id'> & {
34
+ id: FieldOption['id'] | null;
35
+ })[] = [
36
+ ...REQUIRED_WORKFLOW_STATUSES.map((required) => {
37
+ const found = existing.find((status) => status.name === required.name);
38
+ return {
39
+ id: found ? found.id : null,
40
+ name: required.name,
41
+ color: required.color,
42
+ description: required.description,
43
+ };
44
+ }),
45
+ ...others.map((other) => ({
46
+ id: other.id,
47
+ name: other.name,
48
+ color: other.color,
49
+ description: other.description,
50
+ })),
51
+ ];
52
+
53
+ await this.projectRepository.updateStatusList(project, newStatusList);
54
+ };
55
+
56
+ private static hasRequiredStatusesInCanonicalOrder = (
57
+ existing: FieldOption[],
58
+ ): boolean => {
59
+ if (existing.length < REQUIRED_WORKFLOW_STATUSES.length) {
60
+ return false;
61
+ }
62
+ return REQUIRED_WORKFLOW_STATUSES.every(
63
+ (required: WorkflowStatusDefinition, index: number) => {
64
+ const actual = existing[index];
65
+ return actual.name === required.name && actual.color === required.color;
66
+ },
67
+ );
68
+ };
69
+ }