github-issue-tower-defence-management 1.185.1 → 1.186.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 (144) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/cli/index.js +2 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
  6. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
  8. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  10. package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  11. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  12. package/bin/adapter/entry-points/console/webServer.js +3 -0
  13. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
  15. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  17. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
  19. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  20. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
  21. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  22. package/bin/domain/entities/Project.js +11 -1
  23. package/bin/domain/entities/Project.js.map +1 -1
  24. package/bin/domain/entities/RequiredProjectField.js +3 -2
  25. package/bin/domain/entities/RequiredProjectField.js.map +1 -1
  26. package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  28. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
  29. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
  31. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  32. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
  33. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
  34. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
  35. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
  36. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
  37. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  38. package/bin/domain/usecases/StartPreparationUseCase.js +5 -2
  39. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/adapter/entry-points/cli/index.ts +14 -10
  42. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
  43. package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
  44. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
  45. package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
  46. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
  47. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
  48. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
  52. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
  54. package/src/adapter/entry-points/console/ui/src/index.css +35 -0
  55. package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  56. package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  57. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  58. package/src/adapter/entry-points/console/webServer.ts +7 -0
  59. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  60. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  61. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
  62. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
  63. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  64. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  65. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
  66. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  67. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
  68. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
  69. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  70. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  71. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
  72. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
  73. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
  74. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
  75. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  76. package/src/domain/entities/Issue.ts +1 -0
  77. package/src/domain/entities/Project.ts +11 -2
  78. package/src/domain/entities/RequiredProjectField.ts +3 -1
  79. package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
  80. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  81. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  82. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
  83. package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
  84. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  85. package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
  86. package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
  87. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
  88. package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
  89. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  90. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
  91. package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
  92. package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
  93. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
  94. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
  95. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
  96. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
  97. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  98. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
  99. package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
  100. package/src/domain/usecases/StartPreparationUseCase.test.ts +111 -1
  101. package/src/domain/usecases/StartPreparationUseCase.ts +6 -2
  102. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
  103. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
  104. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
  105. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
  106. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
  107. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  108. package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
  109. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  110. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
  111. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  112. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
  113. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  114. package/types/adapter/entry-points/console/webServer.d.ts +2 -0
  115. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  116. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  117. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  118. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
  119. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  120. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  121. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  122. package/types/domain/entities/Issue.d.ts +1 -0
  123. package/types/domain/entities/Issue.d.ts.map +1 -1
  124. package/types/domain/entities/Project.d.ts +2 -1
  125. package/types/domain/entities/Project.d.ts.map +1 -1
  126. package/types/domain/entities/RequiredProjectField.d.ts +1 -0
  127. package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
  128. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
  129. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  130. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
  131. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
  132. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
  133. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  134. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
  135. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
  136. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
  137. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  138. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  139. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
  140. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  141. package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  142. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
  143. package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  144. package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
@@ -858,6 +858,126 @@ describe('IssueRejectionEvaluator', () => {
858
858
  });
859
859
  });
860
860
 
861
+ describe('detectConflictEvenIfEvaluationSkipped option', () => {
862
+ it('should call findRelatedOpenPRs and return PULL_REQUEST_CONFLICTED when relatedOpenPrUrls is null and PR is conflicted', async () => {
863
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
864
+ createReadyPr('https://github.com/user/repo/pull/1', {
865
+ isConflicted: true,
866
+ }),
867
+ ]);
868
+
869
+ const result = await evaluator.evaluate(
870
+ {
871
+ url: 'https://github.com/user/repo/issues/1',
872
+ labels: [],
873
+ isPr: false,
874
+ agent: 'chore',
875
+ },
876
+ [],
877
+ {
878
+ detectConflictEvenIfEvaluationSkipped: true,
879
+ relatedOpenPrUrls: null,
880
+ },
881
+ );
882
+
883
+ expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
884
+ 'https://github.com/user/repo/issues/1',
885
+ );
886
+ expect(result.rejections).toHaveLength(1);
887
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
888
+ expect(result.approvedPrUrl).toBeNull();
889
+ });
890
+
891
+ it('should call findRelatedOpenPRs and return no rejections when relatedOpenPrUrls is null and no PR is conflicted', async () => {
892
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
893
+ createReadyPr('https://github.com/user/repo/pull/1'),
894
+ ]);
895
+
896
+ const result = await evaluator.evaluate(
897
+ {
898
+ url: 'https://github.com/user/repo/issues/1',
899
+ labels: [],
900
+ isPr: false,
901
+ agent: 'chore',
902
+ },
903
+ [],
904
+ {
905
+ detectConflictEvenIfEvaluationSkipped: true,
906
+ relatedOpenPrUrls: null,
907
+ },
908
+ );
909
+
910
+ expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalledWith(
911
+ 'https://github.com/user/repo/issues/1',
912
+ );
913
+ expect(result.rejections).toHaveLength(0);
914
+ expect(result.approvedPrUrl).toBeNull();
915
+ });
916
+
917
+ it('should use resolveOpenPrsFromUrls path and not call findRelatedOpenPRs when relatedOpenPrUrls is provided', async () => {
918
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue(
919
+ createReadyPr('https://github.com/user/repo/pull/5', {
920
+ isConflicted: true,
921
+ }),
922
+ );
923
+
924
+ const result = await evaluator.evaluate(
925
+ {
926
+ url: 'https://github.com/user/repo/issues/1',
927
+ labels: [],
928
+ isPr: false,
929
+ agent: 'chore',
930
+ },
931
+ [],
932
+ {
933
+ detectConflictEvenIfEvaluationSkipped: true,
934
+ relatedOpenPrUrls: ['https://github.com/user/repo/pull/5'],
935
+ },
936
+ );
937
+
938
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
939
+ expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
940
+ 'https://github.com/user/repo/pull/5',
941
+ );
942
+ expect(result.rejections).toHaveLength(1);
943
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
944
+ });
945
+
946
+ it('should emit only PULL_REQUEST_CONFLICTED and not PULL_REQUEST_IS_DRAFT or other types when evaluation is skipped', async () => {
947
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
948
+ createReadyPr('https://github.com/user/repo/pull/1', {
949
+ isConflicted: true,
950
+ isDraft: true,
951
+ isPassedAllCiJob: false,
952
+ isCiStateSuccess: false,
953
+ }),
954
+ ]);
955
+
956
+ const result = await evaluator.evaluate(
957
+ {
958
+ url: 'https://github.com/user/repo/issues/1',
959
+ labels: [],
960
+ isPr: false,
961
+ agent: 'chore',
962
+ },
963
+ [],
964
+ {
965
+ detectConflictEvenIfEvaluationSkipped: true,
966
+ relatedOpenPrUrls: null,
967
+ },
968
+ );
969
+
970
+ expect(result.rejections).toHaveLength(1);
971
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_CONFLICTED');
972
+ expect(result.rejections.map((r) => r.type)).not.toContain(
973
+ 'PULL_REQUEST_IS_DRAFT',
974
+ );
975
+ expect(result.rejections.map((r) => r.type)).not.toContain(
976
+ 'ANY_CI_JOB_FAILED_OR_IN_PROGRESS',
977
+ );
978
+ });
979
+ });
980
+
861
981
  describe('change-target-must: label behavior', () => {
862
982
  const prUrl = 'https://github.com/user/repo/pull/1';
863
983
 
@@ -37,6 +37,7 @@ export type EvaluateOptions = {
37
37
  // The agent name treated as the developer agent for PR/CI/conflict checks.
38
38
  // When null or undefined, defaults to 'developer'.
39
39
  developerAgentName?: string | null;
40
+ detectConflictEvenIfEvaluationSkipped?: boolean;
40
41
  };
41
42
 
42
43
  export class IssueRejectionEvaluator {
@@ -186,6 +187,26 @@ export class IssueRejectionEvaluator {
186
187
  approvedPrUrl = pr.url;
187
188
  }
188
189
  }
190
+ } else if (options.detectConflictEvenIfEvaluationSkipped) {
191
+ let prsToCheck: RelatedPullRequest[];
192
+ if (options.relatedOpenPrUrls != null) {
193
+ const resolved = await this.resolveOpenPrsFromUrls(
194
+ options.relatedOpenPrUrls,
195
+ options.resolvedOpenPrByUrl ?? null,
196
+ );
197
+ prsToCheck = resolved.prs;
198
+ } else {
199
+ prsToCheck = await this.issueRepository.findRelatedOpenPRs(issue.url);
200
+ }
201
+ for (const pr of prsToCheck) {
202
+ if (pr.isConflicted) {
203
+ rejections.push({
204
+ type: 'PULL_REQUEST_CONFLICTED',
205
+ detail: `PULL_REQUEST_CONFLICTED: ${pr.url}`,
206
+ });
207
+ break;
208
+ }
209
+ }
189
210
  }
190
211
 
191
212
  return { rejections, approvedPrUrl };
@@ -76,6 +76,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
76
76
  author: '',
77
77
  closingIssueReferenceUrls: [],
78
78
  agent: null,
79
+ stateReason: null,
79
80
  ...overrides,
80
81
  });
81
82
 
@@ -238,6 +238,7 @@ describe('NotifySilentLiveSessionsUseCase', () => {
238
238
  author: 'HiromiShikata',
239
239
  closingIssueReferenceUrls: [],
240
240
  agent: null,
241
+ stateReason: null,
241
242
  ...overrides,
242
243
  });
243
244
 
@@ -35,6 +35,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
35
35
  author: '',
36
36
  closingIssueReferenceUrls: [],
37
37
  agent: null,
38
+ stateReason: null,
38
39
  ...overrides,
39
40
  });
40
41
 
@@ -67,6 +68,7 @@ const createMergedPr = (
67
68
  author: '',
68
69
  closingIssueReferenceUrls: [closingIssueUrl],
69
70
  agent: null,
71
+ stateReason: null,
70
72
  ...overrides,
71
73
  });
72
74
 
@@ -384,4 +386,36 @@ describe('QualityCheckAdvanceUseCase', () => {
384
386
  'done-id',
385
387
  );
386
388
  });
389
+
390
+ it('does not advance an issue with stateReason REOPENED even when it has a merged PR', async () => {
391
+ const issue = createMockIssue({ stateReason: 'REOPENED' });
392
+ const mergedPr = createMergedPr(issue.url);
393
+ const project = createMockProject();
394
+
395
+ await useCase.run({
396
+ project,
397
+ issues: [issue, mergedPr],
398
+ evaluatedAt: FIXED_NOW,
399
+ });
400
+
401
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
402
+ });
403
+
404
+ it('advances an issue whose stateReason is COMPLETED even though it was previously REOPENED', async () => {
405
+ const issue = createMockIssue({ stateReason: 'COMPLETED' });
406
+ const mergedPr = createMergedPr(issue.url);
407
+ const project = createMockProject();
408
+
409
+ await useCase.run({
410
+ project,
411
+ issues: [issue, mergedPr],
412
+ evaluatedAt: FIXED_NOW,
413
+ });
414
+
415
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
416
+ project,
417
+ issue,
418
+ 'done-id',
419
+ );
420
+ });
387
421
  });
@@ -45,6 +45,7 @@ export class QualityCheckAdvanceUseCase {
45
45
  (issue) =>
46
46
  issue.status === qualityCheckStatusName &&
47
47
  !issue.isClosed &&
48
+ issue.stateReason !== 'REOPENED' &&
48
49
  issue.dependedIssueUrls.length === 0 &&
49
50
  !issueReactivationTriggerIsPending(issue, evaluatedAt) &&
50
51
  issueUrlsWithMergedPr.has(issue.url),
@@ -0,0 +1,188 @@
1
+ import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
2
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
3
+ import { Issue } from '../entities/Issue';
4
+ import { Project } from '../entities/Project';
5
+ import {
6
+ AWAITING_WORKSPACE_STATUS_NAME,
7
+ DONE_STATUS_NAME,
8
+ } from '../entities/WorkflowStatus';
9
+
10
+ type Mocked<T> = jest.Mocked<T> & jest.MockedObject<T>;
11
+
12
+ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
13
+ nameWithOwner: 'user/repo',
14
+ number: 1,
15
+ title: 'Test Issue',
16
+ state: 'OPEN',
17
+ status: DONE_STATUS_NAME,
18
+ story: 'Default Story',
19
+ nextActionDate: null,
20
+ nextActionHour: null,
21
+ estimationMinutes: null,
22
+ dependedIssueUrls: [],
23
+ completionDate50PercentConfidence: null,
24
+ url: 'https://github.com/user/repo/issues/1',
25
+ assignees: [],
26
+ labels: [],
27
+ org: 'user',
28
+ repo: 'repo',
29
+ body: '',
30
+ itemId: 'item-1',
31
+ isPr: false,
32
+ isInProgress: false,
33
+ isClosed: false,
34
+ createdAt: new Date(),
35
+ author: '',
36
+ closingIssueReferenceUrls: [],
37
+ agent: null,
38
+ stateReason: 'REOPENED',
39
+ ...overrides,
40
+ });
41
+
42
+ const createMockProject = (): Project => ({
43
+ id: 'project-1',
44
+ url: 'https://github.com/orgs/user/projects/1',
45
+ databaseId: 1,
46
+ name: 'Test Project',
47
+ status: {
48
+ name: 'Status',
49
+ fieldId: 'status-field-id',
50
+ statuses: [
51
+ {
52
+ id: 'awaiting-workspace-id',
53
+ name: AWAITING_WORKSPACE_STATUS_NAME,
54
+ color: 'BLUE',
55
+ description: '',
56
+ },
57
+ {
58
+ id: 'preparation-id',
59
+ name: 'Preparation',
60
+ color: 'YELLOW',
61
+ description: '',
62
+ },
63
+ {
64
+ id: 'done-id',
65
+ name: DONE_STATUS_NAME,
66
+ color: 'PURPLE',
67
+ description: '',
68
+ },
69
+ ],
70
+ },
71
+ nextActionDate: null,
72
+ nextActionHour: null,
73
+ story: null,
74
+ remainingEstimationMinutes: null,
75
+ dependedIssueUrlSeparatedByComma: null,
76
+ completionDate50PercentConfidence: null,
77
+ agent: null,
78
+ });
79
+
80
+ describe('ReopenedDoneIssueRevertUseCase', () => {
81
+ let useCase: ReopenedDoneIssueRevertUseCase;
82
+ let mockIssueRepository: Mocked<Pick<IssueRepository, 'updateStatus'>>;
83
+
84
+ beforeEach(() => {
85
+ mockIssueRepository = {
86
+ updateStatus: jest.fn(),
87
+ };
88
+ useCase = new ReopenedDoneIssueRevertUseCase(mockIssueRepository);
89
+ });
90
+
91
+ it('resets a Done issue with stateReason REOPENED to Awaiting Workspace', async () => {
92
+ const issue = createMockIssue();
93
+ const project = createMockProject();
94
+
95
+ await useCase.run({ project, issues: [issue] });
96
+
97
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
98
+ project,
99
+ issue,
100
+ 'awaiting-workspace-id',
101
+ );
102
+ });
103
+
104
+ it('does not reset a Done issue with stateReason COMPLETED', async () => {
105
+ const issue = createMockIssue({ stateReason: 'COMPLETED' });
106
+ const project = createMockProject();
107
+
108
+ await useCase.run({ project, issues: [issue] });
109
+
110
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it('does not reset a Done issue with stateReason null', async () => {
114
+ const issue = createMockIssue({ stateReason: null });
115
+ const project = createMockProject();
116
+
117
+ await useCase.run({ project, issues: [issue] });
118
+
119
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
120
+ });
121
+
122
+ it('does not reset a PR item even when Done and stateReason is REOPENED', async () => {
123
+ const prIssue = createMockIssue({
124
+ isPr: true,
125
+ url: 'https://github.com/user/repo/pull/10',
126
+ });
127
+ const project = createMockProject();
128
+
129
+ await useCase.run({ project, issues: [prIssue] });
130
+
131
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
132
+ });
133
+
134
+ it('does not reset an issue that is not in Done status', async () => {
135
+ const issue = createMockIssue({ status: 'Awaiting Workspace' });
136
+ const project = createMockProject();
137
+
138
+ await useCase.run({ project, issues: [issue] });
139
+
140
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
141
+ });
142
+
143
+ it('logs an error and returns 0 when Awaiting Workspace status option is missing', async () => {
144
+ const issue = createMockIssue();
145
+ const project = createMockProject();
146
+ project.status.statuses = project.status.statuses.filter(
147
+ (s) => s.name !== AWAITING_WORKSPACE_STATUS_NAME,
148
+ );
149
+ const consoleSpy = jest
150
+ .spyOn(console, 'error')
151
+ .mockImplementation(() => {});
152
+
153
+ const result = await useCase.run({ project, issues: [issue] });
154
+
155
+ expect(result).toBe(0);
156
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
157
+ consoleSpy.mockRestore();
158
+ });
159
+
160
+ it('collects updateStatus failures and re-throws as AggregateError after processing all items', async () => {
161
+ const issue1 = createMockIssue({
162
+ number: 1,
163
+ url: 'https://github.com/user/repo/issues/1',
164
+ itemId: 'item-1',
165
+ });
166
+ const issue2 = createMockIssue({
167
+ number: 2,
168
+ url: 'https://github.com/user/repo/issues/2',
169
+ itemId: 'item-2',
170
+ });
171
+ const project = createMockProject();
172
+ const updateError = new Error('updateStatus failed for issue1');
173
+ mockIssueRepository.updateStatus
174
+ .mockRejectedValueOnce(updateError)
175
+ .mockResolvedValueOnce(undefined);
176
+
177
+ await expect(
178
+ useCase.run({ project, issues: [issue1, issue2] }),
179
+ ).rejects.toBeInstanceOf(AggregateError);
180
+
181
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(2);
182
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
183
+ project,
184
+ issue2,
185
+ 'awaiting-workspace-id',
186
+ );
187
+ });
188
+ });
@@ -0,0 +1,57 @@
1
+ import { Issue } from '../entities/Issue';
2
+ import { Project } from '../entities/Project';
3
+ import { IssueRepository } from './adapter-interfaces/IssueRepository';
4
+ import {
5
+ AWAITING_WORKSPACE_STATUS_NAME,
6
+ DONE_STATUS_NAME,
7
+ } from '../entities/WorkflowStatus';
8
+
9
+ export class ReopenedDoneIssueRevertUseCase {
10
+ constructor(
11
+ private readonly issueRepository: Pick<IssueRepository, 'updateStatus'>,
12
+ ) {}
13
+
14
+ run = async (params: {
15
+ project: Project;
16
+ issues: Issue[];
17
+ }): Promise<number> => {
18
+ const awaitingWorkspaceStatusOption = params.project.status.statuses.find(
19
+ (s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
20
+ );
21
+ if (!awaitingWorkspaceStatusOption) {
22
+ console.error(
23
+ `Awaiting Workspace status option '${AWAITING_WORKSPACE_STATUS_NAME}' not found in project.`,
24
+ );
25
+ return 0;
26
+ }
27
+
28
+ const itemsToRevert = params.issues.filter(
29
+ (issue) =>
30
+ issue.status === DONE_STATUS_NAME &&
31
+ issue.stateReason === 'REOPENED' &&
32
+ !issue.isPr,
33
+ );
34
+
35
+ let revertedCount = 0;
36
+ const errors: unknown[] = [];
37
+ for (const issue of itemsToRevert) {
38
+ try {
39
+ await this.issueRepository.updateStatus(
40
+ params.project,
41
+ issue,
42
+ awaitingWorkspaceStatusOption.id,
43
+ );
44
+ revertedCount++;
45
+ } catch (error) {
46
+ errors.push(error);
47
+ }
48
+ }
49
+ if (errors.length > 0) {
50
+ throw new AggregateError(
51
+ errors,
52
+ `Failed to revert ${errors.length} issue(s) from ${DONE_STATUS_NAME} to ${AWAITING_WORKSPACE_STATUS_NAME}`,
53
+ );
54
+ }
55
+ return revertedCount;
56
+ };
57
+ }
@@ -78,6 +78,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
78
78
  author: 'owner',
79
79
  closingIssueReferenceUrls: [],
80
80
  agent: null,
81
+ stateReason: null,
81
82
  ...overrides,
82
83
  });
83
84
 
@@ -249,6 +250,49 @@ describe('RevertNotReadyReviewQueueIssueUseCase', () => {
249
250
  expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
250
251
  });
251
252
 
253
+ it('should revert chore agent issue to Awaiting Workspace when its linked PR is conflicted', async () => {
254
+ const issue = createMockIssue({
255
+ status: 'Awaiting Quality Check',
256
+ agent: 'chore',
257
+ });
258
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [
259
+ { ...createReadyPr(), isConflicted: true },
260
+ ]);
261
+
262
+ await useCase.run({
263
+ manager: 'manager-user',
264
+ projectUrl: 'https://github.com/users/user/projects/1',
265
+ allowedIssueAuthors: ['owner'],
266
+ });
267
+
268
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
269
+ mockProject,
270
+ issue,
271
+ 'awaiting-workspace-id',
272
+ );
273
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
274
+ issue,
275
+ expect.stringContaining('PULL_REQUEST_CONFLICTED'),
276
+ );
277
+ });
278
+
279
+ it('should not revert chore agent issue when its linked PR is not conflicted', async () => {
280
+ const issue = createMockIssue({
281
+ status: 'Awaiting Quality Check',
282
+ agent: 'chore',
283
+ });
284
+ linkRelatedOpenPrsToIssue(mockIssueRepository, issue, [createReadyPr()]);
285
+
286
+ await useCase.run({
287
+ manager: 'manager-user',
288
+ projectUrl: 'https://github.com/users/user/projects/1',
289
+ allowedIssueAuthors: ['owner'],
290
+ });
291
+
292
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
293
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
294
+ });
295
+
252
296
  it('should revert Awaiting Quality Check issue with developer agent field and no linked PR', async () => {
253
297
  const issue = createMockIssue({
254
298
  status: 'Awaiting Quality Check',
@@ -98,22 +98,13 @@ export class RevertNotReadyReviewQueueIssueUseCase {
98
98
 
99
99
  const labelsNotRequiringPullRequest =
100
100
  resolveLabelsNotRequiringPullRequest(params);
101
- const willBeEvaluated = (item: Issue): boolean =>
102
- isAuthorAuthorizedForAutoStatusCheck(item.author, allowedIssueAuthors) &&
103
- this.issueRejectionEvaluator.requiresPullRequestEvaluation(
104
- item,
105
- labelsNotRequiringPullRequest,
106
- params.developerAgentName,
107
- );
108
101
 
109
102
  const resolvedOpenPrByUrl = await this.issueRepository.getOpenPullRequests(
110
103
  Array.from(
111
104
  new Set([
112
- ...awaitingQualityCheckIssues
113
- .filter(willBeEvaluated)
114
- .flatMap(
115
- (issue) => relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? [],
116
- ),
105
+ ...awaitingQualityCheckIssues.flatMap(
106
+ (issue) => relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? [],
107
+ ),
117
108
  ]),
118
109
  ),
119
110
  );
@@ -135,6 +126,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
135
126
  relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? null,
136
127
  resolvedOpenPrByUrl,
137
128
  developerAgentName: params.developerAgentName,
129
+ detectConflictEvenIfEvaluationSkipped: true,
138
130
  },
139
131
  );
140
132
  if (
@@ -34,6 +34,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
34
34
  author: '',
35
35
  closingIssueReferenceUrls: [],
36
36
  agent: null,
37
+ stateReason: null,
37
38
  ...overrides,
38
39
  });
39
40
 
@@ -75,6 +75,7 @@ const buildIssue = (overrides: Partial<Issue>): Issue => ({
75
75
  author: 'user',
76
76
  closingIssueReferenceUrls: [],
77
77
  agent: null,
78
+ stateReason: null,
78
79
  ...overrides,
79
80
  });
80
81
 
@@ -61,6 +61,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => {
61
61
  author: 'testuser',
62
62
  closingIssueReferenceUrls: [],
63
63
  agent: null,
64
+ stateReason: null,
64
65
  ...overrides,
65
66
  };
66
67
  };