github-issue-tower-defence-management 1.185.1 → 1.186.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.
- package/CHANGELOG.md +10 -0
- package/README.md +2 -0
- package/bin/adapter/entry-points/cli/index.js +2 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/console/webServer.js +3 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
- package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/entities/Project.js +11 -1
- package/bin/domain/entities/Project.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
- package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
- package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
- package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.ts +14 -10
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
- package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
- package/src/adapter/entry-points/console/ui/src/index.css +35 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/console/webServer.ts +7 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
- package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
- package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
- package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
- package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
- package/src/domain/entities/Issue.ts +1 -0
- package/src/domain/entities/Project.ts +11 -2
- package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
- package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
- package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
- package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
- package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
- package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
- package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
- package/src/domain/usecases/StartPreparationUseCase.test.ts +1 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
- package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
- package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -0
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/entities/Issue.d.ts +1 -0
- package/types/domain/entities/Issue.d.ts.map +1 -1
- package/types/domain/entities/Project.d.ts +2 -1
- package/types/domain/entities/Project.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
- package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
- package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
|
@@ -18,6 +18,7 @@ export type ProjectItem = {
|
|
|
18
18
|
author: string;
|
|
19
19
|
closingIssueReferenceUrls: string[];
|
|
20
20
|
isRepoArchived: boolean;
|
|
21
|
+
stateReason: 'COMPLETED' | 'NOT_PLANNED' | 'REOPENED' | null;
|
|
21
22
|
customFields: {
|
|
22
23
|
name: string;
|
|
23
24
|
value: string | null;
|
|
@@ -51,6 +52,7 @@ type ProjectV2ItemContentNode = {
|
|
|
51
52
|
labels: { nodes: { name: string }[] };
|
|
52
53
|
assignees: { nodes: { login: string }[] };
|
|
53
54
|
closingIssuesReferences?: { nodes: { url: string }[] };
|
|
55
|
+
stateReason?: string | null;
|
|
54
56
|
};
|
|
55
57
|
type ProjectV2ItemNode = {
|
|
56
58
|
id: string;
|
|
@@ -121,6 +123,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
|
|
|
121
123
|
number
|
|
122
124
|
title
|
|
123
125
|
state
|
|
126
|
+
stateReason
|
|
124
127
|
url
|
|
125
128
|
body
|
|
126
129
|
createdAt
|
|
@@ -186,6 +189,19 @@ export const RATE_LIMIT_MAX_BACKOFF_MS = 300000;
|
|
|
186
189
|
const sleep = (ms: number): Promise<void> =>
|
|
187
190
|
new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
191
|
|
|
192
|
+
const toStateReason = (
|
|
193
|
+
value: string | null | undefined,
|
|
194
|
+
): 'COMPLETED' | 'NOT_PLANNED' | 'REOPENED' | null => {
|
|
195
|
+
if (
|
|
196
|
+
value === 'COMPLETED' ||
|
|
197
|
+
value === 'NOT_PLANNED' ||
|
|
198
|
+
value === 'REOPENED'
|
|
199
|
+
) {
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
|
|
189
205
|
const parseNonNegativeIntegerHeader = (value: string | null): number | null => {
|
|
190
206
|
if (value === null) {
|
|
191
207
|
return null;
|
|
@@ -634,6 +650,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
|
|
|
634
650
|
?.map((node) => node.url)
|
|
635
651
|
.filter((url) => url.length > 0) || [],
|
|
636
652
|
isRepoArchived: item.content.repository.isArchived ?? false,
|
|
653
|
+
stateReason: toStateReason(item.content.stateReason),
|
|
637
654
|
customFields: item.fieldValues.nodes
|
|
638
655
|
.filter((field) => !!field.field)
|
|
639
656
|
.map((field) => {
|
|
@@ -1049,6 +1066,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1049
1066
|
number
|
|
1050
1067
|
title
|
|
1051
1068
|
state
|
|
1069
|
+
stateReason
|
|
1052
1070
|
url
|
|
1053
1071
|
body
|
|
1054
1072
|
createdAt
|
|
@@ -1222,6 +1240,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1222
1240
|
number: number;
|
|
1223
1241
|
title: string;
|
|
1224
1242
|
state: string;
|
|
1243
|
+
stateReason?: string | null;
|
|
1225
1244
|
url: string;
|
|
1226
1245
|
body: string;
|
|
1227
1246
|
createdAt: string;
|
|
@@ -1309,6 +1328,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
1309
1328
|
?.map((node) => node.url)
|
|
1310
1329
|
.filter((url) => url.length > 0) || [],
|
|
1311
1330
|
isRepoArchived: content.repository.isArchived ?? false,
|
|
1331
|
+
stateReason: toStateReason(content.stateReason),
|
|
1312
1332
|
customFields: item.fieldValues.nodes
|
|
1313
1333
|
.filter((field) => !!field.field)
|
|
1314
1334
|
.map((field) => {
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
export const FIELD_OPTION_COLORS = [
|
|
2
|
+
'GRAY',
|
|
3
|
+
'BLUE',
|
|
4
|
+
'GREEN',
|
|
5
|
+
'YELLOW',
|
|
6
|
+
'ORANGE',
|
|
7
|
+
'RED',
|
|
8
|
+
'PINK',
|
|
9
|
+
'PURPLE',
|
|
10
|
+
] as const;
|
|
1
11
|
export type StoryOption = FieldOption;
|
|
2
12
|
export type FieldOption = {
|
|
3
13
|
id: string;
|
|
4
14
|
name: string;
|
|
5
|
-
color:
|
|
6
|
-
'GRAY' | 'BLUE' | 'GREEN' | 'YELLOW' | 'ORANGE' | 'RED' | 'PINK' | 'PURPLE';
|
|
15
|
+
color: (typeof FIELD_OPTION_COLORS)[number];
|
|
7
16
|
description: string;
|
|
8
17
|
};
|
|
9
18
|
|
|
@@ -21,6 +21,7 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
|
|
|
21
21
|
import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
|
|
22
22
|
import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
|
|
23
23
|
import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
|
|
24
|
+
import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
|
|
24
25
|
import { StartPreparationUseCase } from './StartPreparationUseCase';
|
|
25
26
|
import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
|
|
26
27
|
import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
|
|
@@ -31,6 +32,7 @@ import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCa
|
|
|
31
32
|
import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
|
|
32
33
|
import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
|
|
33
34
|
import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
|
|
35
|
+
import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
|
|
34
36
|
|
|
35
37
|
describe('HandleScheduledEventUseCase', () => {
|
|
36
38
|
describe('createTargetDateTimes', () => {
|
|
@@ -121,6 +123,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
121
123
|
mock<AssignNoAssigneeIssueToManagerUseCase>();
|
|
122
124
|
const mockUpdateIssueStatusByLabelUseCase =
|
|
123
125
|
mock<UpdateIssueStatusByLabelUseCase>();
|
|
126
|
+
const mockIssueNoStatusUpdateUseCase = mock<IssueNoStatusUpdateUseCase>();
|
|
124
127
|
const mockStartPreparationUseCase = mock<StartPreparationUseCase>();
|
|
125
128
|
const mockRevertOrphanedPreparationUseCase =
|
|
126
129
|
mock<RevertOrphanedPreparationUseCase>();
|
|
@@ -133,6 +136,8 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
133
136
|
const mockUpdateRateLimitCacheUseCase = mock<UpdateRateLimitCacheUseCase>();
|
|
134
137
|
const mockDailySecurityScanUseCase = mock<DailySecurityScanUseCase>();
|
|
135
138
|
const mockAdvanceQualityCheckUseCase = mock<QualityCheckAdvanceUseCase>();
|
|
139
|
+
const mockReopenedDoneIssueRevertUseCase =
|
|
140
|
+
mock<ReopenedDoneIssueRevertUseCase>();
|
|
136
141
|
const mockDateRepository = mock<DateRepository>();
|
|
137
142
|
const mockSpreadsheetRepository = mock<SpreadsheetRepository>();
|
|
138
143
|
const mockProjectRepository = mock<ProjectRepository>();
|
|
@@ -156,6 +161,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
156
161
|
mockCreateNewStoryByLabelUseCase,
|
|
157
162
|
mockAssignNoAssigneeIssueToManagerUseCase,
|
|
158
163
|
mockUpdateIssueStatusByLabelUseCase,
|
|
164
|
+
mockIssueNoStatusUpdateUseCase,
|
|
159
165
|
mockStartPreparationUseCase,
|
|
160
166
|
mockRevertOrphanedPreparationUseCase,
|
|
161
167
|
mockRevertNotReadyReviewQueueIssueUseCase,
|
|
@@ -164,6 +170,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
164
170
|
mockUpdateRateLimitCacheUseCase,
|
|
165
171
|
mockDailySecurityScanUseCase,
|
|
166
172
|
mockAdvanceQualityCheckUseCase,
|
|
173
|
+
mockReopenedDoneIssueRevertUseCase,
|
|
167
174
|
mockDateRepository,
|
|
168
175
|
mockSpreadsheetRepository,
|
|
169
176
|
mockProjectRepository,
|
|
@@ -240,6 +247,41 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
240
247
|
});
|
|
241
248
|
});
|
|
242
249
|
|
|
250
|
+
it('should call IssueNoStatusUpdateUseCase with project and issues when startPreparation is configured', async () => {
|
|
251
|
+
const input = {
|
|
252
|
+
projectName: 'test-project',
|
|
253
|
+
org: 'test-org',
|
|
254
|
+
projectUrl: 'https://github.com/test-org/test-project',
|
|
255
|
+
manager: 'test-manager',
|
|
256
|
+
workingReport: {
|
|
257
|
+
repo: 'test-repo',
|
|
258
|
+
members: ['member1'],
|
|
259
|
+
spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
|
|
260
|
+
},
|
|
261
|
+
urlOfStoryView: 'https://github.com/test-org/test-project/issues',
|
|
262
|
+
disabled: false,
|
|
263
|
+
startPreparation: {
|
|
264
|
+
defaultAgentName: 'agent1',
|
|
265
|
+
configFilePath: '/path/to/config.yml',
|
|
266
|
+
maximumPreparingIssuesCount: null,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const mockProject = mock<Project>();
|
|
271
|
+
const mockIssues = [mock<Issue>()];
|
|
272
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
273
|
+
issues: mockIssues,
|
|
274
|
+
project: mockProject,
|
|
275
|
+
cacheUsed: false,
|
|
276
|
+
});
|
|
277
|
+
await useCase.run(input);
|
|
278
|
+
|
|
279
|
+
expect(mockIssueNoStatusUpdateUseCase.run).toHaveBeenCalledWith({
|
|
280
|
+
project: mockProject,
|
|
281
|
+
issues: mockIssues,
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
243
285
|
it('should call TriagerApprovalDispatchUseCase with project URL', async () => {
|
|
244
286
|
const input = {
|
|
245
287
|
projectName: 'test-project',
|
|
@@ -1616,6 +1658,105 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
1616
1658
|
expect(mockStartPreparationUseCase.run).toHaveBeenCalled();
|
|
1617
1659
|
});
|
|
1618
1660
|
});
|
|
1661
|
+
|
|
1662
|
+
describe('reopenedDoneIssueRevertUseCase', () => {
|
|
1663
|
+
const baseInput = {
|
|
1664
|
+
projectName: 'test-project',
|
|
1665
|
+
org: 'test-org',
|
|
1666
|
+
projectUrl: 'https://github.com/test-org/test-project',
|
|
1667
|
+
manager: 'test-manager',
|
|
1668
|
+
workingReport: {
|
|
1669
|
+
repo: 'test-repo',
|
|
1670
|
+
members: ['member1'],
|
|
1671
|
+
spreadsheetUrl: 'https://docs.google.com/spreadsheets/test',
|
|
1672
|
+
},
|
|
1673
|
+
urlOfStoryView: 'https://github.com/test-org/test-project/issues',
|
|
1674
|
+
disabled: false,
|
|
1675
|
+
};
|
|
1676
|
+
|
|
1677
|
+
it('does not call reopenedDoneIssueRevertUseCase when startPreparation is absent', async () => {
|
|
1678
|
+
await useCase.run(baseInput);
|
|
1679
|
+
|
|
1680
|
+
expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
it('does not call reopenedDoneIssueRevertUseCase when autoRevertReopenedDoneEnabled is absent', async () => {
|
|
1684
|
+
await useCase.run({
|
|
1685
|
+
...baseInput,
|
|
1686
|
+
startPreparation: {
|
|
1687
|
+
defaultAgentName: 'agent1',
|
|
1688
|
+
configFilePath: '/path/to/config.yml',
|
|
1689
|
+
maximumPreparingIssuesCount: null,
|
|
1690
|
+
autoAdvanceQualityCheckEnabled: false,
|
|
1691
|
+
},
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
|
|
1695
|
+
});
|
|
1696
|
+
|
|
1697
|
+
it('does not call reopenedDoneIssueRevertUseCase when autoRevertReopenedDoneEnabled is false', async () => {
|
|
1698
|
+
await useCase.run({
|
|
1699
|
+
...baseInput,
|
|
1700
|
+
startPreparation: {
|
|
1701
|
+
defaultAgentName: 'agent1',
|
|
1702
|
+
configFilePath: '/path/to/config.yml',
|
|
1703
|
+
maximumPreparingIssuesCount: null,
|
|
1704
|
+
autoAdvanceQualityCheckEnabled: false,
|
|
1705
|
+
autoRevertReopenedDoneEnabled: false,
|
|
1706
|
+
},
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
expect(mockReopenedDoneIssueRevertUseCase.run).not.toHaveBeenCalled();
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
it('calls reopenedDoneIssueRevertUseCase with project and issues when autoRevertReopenedDoneEnabled is true', async () => {
|
|
1713
|
+
const mockProject = mock<Project>();
|
|
1714
|
+
const mockIssues: Issue[] = [];
|
|
1715
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
1716
|
+
issues: mockIssues,
|
|
1717
|
+
project: mockProject,
|
|
1718
|
+
cacheUsed: false,
|
|
1719
|
+
});
|
|
1720
|
+
|
|
1721
|
+
await useCase.run({
|
|
1722
|
+
...baseInput,
|
|
1723
|
+
startPreparation: {
|
|
1724
|
+
defaultAgentName: 'agent1',
|
|
1725
|
+
configFilePath: '/path/to/config.yml',
|
|
1726
|
+
maximumPreparingIssuesCount: null,
|
|
1727
|
+
autoAdvanceQualityCheckEnabled: false,
|
|
1728
|
+
autoRevertReopenedDoneEnabled: true,
|
|
1729
|
+
},
|
|
1730
|
+
});
|
|
1731
|
+
|
|
1732
|
+
expect(mockReopenedDoneIssueRevertUseCase.run).toHaveBeenCalledWith({
|
|
1733
|
+
project: mockProject,
|
|
1734
|
+
issues: mockIssues,
|
|
1735
|
+
});
|
|
1736
|
+
});
|
|
1737
|
+
|
|
1738
|
+
it('continues to startPreparationUseCase when reopenedDoneIssueRevertUseCase.run rejects', async () => {
|
|
1739
|
+
mockReopenedDoneIssueRevertUseCase.run.mockRejectedValue(
|
|
1740
|
+
new AggregateError(
|
|
1741
|
+
[new Error('GitHub API error')],
|
|
1742
|
+
'Failed to revert 1 reopened Done issue(s)',
|
|
1743
|
+
),
|
|
1744
|
+
);
|
|
1745
|
+
|
|
1746
|
+
await useCase.run({
|
|
1747
|
+
...baseInput,
|
|
1748
|
+
startPreparation: {
|
|
1749
|
+
defaultAgentName: 'agent1',
|
|
1750
|
+
configFilePath: '/path/to/config.yml',
|
|
1751
|
+
maximumPreparingIssuesCount: null,
|
|
1752
|
+
autoAdvanceQualityCheckEnabled: false,
|
|
1753
|
+
autoRevertReopenedDoneEnabled: true,
|
|
1754
|
+
},
|
|
1755
|
+
});
|
|
1756
|
+
|
|
1757
|
+
expect(mockStartPreparationUseCase.run).toHaveBeenCalled();
|
|
1758
|
+
});
|
|
1759
|
+
});
|
|
1619
1760
|
});
|
|
1620
1761
|
|
|
1621
1762
|
describe('storyIssues', () => {
|
|
@@ -1637,6 +1778,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
1637
1778
|
mock<CreateNewStoryByLabelUseCase>(),
|
|
1638
1779
|
mock<AssignNoAssigneeIssueToManagerUseCase>(),
|
|
1639
1780
|
mock<UpdateIssueStatusByLabelUseCase>(),
|
|
1781
|
+
mock<IssueNoStatusUpdateUseCase>(),
|
|
1640
1782
|
mock<StartPreparationUseCase>(),
|
|
1641
1783
|
mock<RevertOrphanedPreparationUseCase>(),
|
|
1642
1784
|
mock<RevertNotReadyReviewQueueIssueUseCase>(),
|
|
@@ -1645,6 +1787,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
1645
1787
|
null,
|
|
1646
1788
|
null,
|
|
1647
1789
|
mock<QualityCheckAdvanceUseCase>(),
|
|
1790
|
+
mock<ReopenedDoneIssueRevertUseCase>(),
|
|
1648
1791
|
mock<DateRepository>(),
|
|
1649
1792
|
mock<SpreadsheetRepository>(),
|
|
1650
1793
|
mock<ProjectRepository>(),
|
|
@@ -1701,6 +1844,7 @@ describe('HandleScheduledEventUseCase', () => {
|
|
|
1701
1844
|
author: '',
|
|
1702
1845
|
closingIssueReferenceUrls: [],
|
|
1703
1846
|
agent: null,
|
|
1847
|
+
stateReason: null,
|
|
1704
1848
|
};
|
|
1705
1849
|
|
|
1706
1850
|
it('should return storyIssue as null when the only matching issue is closed', async () => {
|
|
@@ -21,6 +21,7 @@ import { SetNoStoryIssueToStoryUseCase } from './SetNoStoryIssueToStoryUseCase';
|
|
|
21
21
|
import { CreateNewStoryByLabelUseCase } from './CreateNewStoryByLabelUseCase';
|
|
22
22
|
import { AssignNoAssigneeIssueToManagerUseCase } from './AssignNoAssigneeIssueToManagerUseCase';
|
|
23
23
|
import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCase';
|
|
24
|
+
import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
|
|
24
25
|
import {
|
|
25
26
|
RotationOrderEntry,
|
|
26
27
|
StartPreparationUseCase,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
DailySecurityScanUseCase,
|
|
41
42
|
} from './DailySecurityScanUseCase';
|
|
42
43
|
import { QualityCheckAdvanceUseCase } from './QualityCheckAdvanceUseCase';
|
|
44
|
+
import { ReopenedDoneIssueRevertUseCase } from './ReopenedDoneIssueRevertUseCase';
|
|
43
45
|
|
|
44
46
|
export class ProjectNotFoundError extends Error {
|
|
45
47
|
constructor(message: string) {
|
|
@@ -133,6 +135,7 @@ export class HandleScheduledEventUseCase {
|
|
|
133
135
|
readonly createNewStoryByLabelUseCase: CreateNewStoryByLabelUseCase,
|
|
134
136
|
readonly assignNoAssigneeIssueToManagerUseCase: AssignNoAssigneeIssueToManagerUseCase,
|
|
135
137
|
readonly updateIssueStatusByLabelUseCase: UpdateIssueStatusByLabelUseCase,
|
|
138
|
+
readonly issueNoStatusUpdateUseCase: IssueNoStatusUpdateUseCase,
|
|
136
139
|
readonly startPreparationUseCase: StartPreparationUseCase,
|
|
137
140
|
readonly revertOrphanedPreparationUseCase: RevertOrphanedPreparationUseCase,
|
|
138
141
|
readonly revertNotReadyReviewQueueIssueUseCase: RevertNotReadyReviewQueueIssueUseCase,
|
|
@@ -141,6 +144,7 @@ export class HandleScheduledEventUseCase {
|
|
|
141
144
|
readonly updateRateLimitCacheUseCase: UpdateRateLimitCacheUseCase | null,
|
|
142
145
|
readonly dailySecurityScanUseCase: DailySecurityScanUseCase | null,
|
|
143
146
|
readonly qualityCheckAdvanceUseCase: QualityCheckAdvanceUseCase,
|
|
147
|
+
readonly reopenedDoneIssueRevertUseCase: ReopenedDoneIssueRevertUseCase,
|
|
144
148
|
readonly dateRepository: DateRepository,
|
|
145
149
|
readonly spreadsheetRepository: SpreadsheetRepository,
|
|
146
150
|
readonly projectRepository: ProjectRepository,
|
|
@@ -180,6 +184,7 @@ export class HandleScheduledEventUseCase {
|
|
|
180
184
|
awLogStaleThresholdMinutes?: number;
|
|
181
185
|
awaitingQualityCheckStatus?: string | null;
|
|
182
186
|
autoAdvanceQualityCheckEnabled?: boolean;
|
|
187
|
+
autoRevertReopenedDoneEnabled?: boolean;
|
|
183
188
|
labelsAsLlmAgentName?: string[] | null;
|
|
184
189
|
} | null;
|
|
185
190
|
thresholdForAutoReject?: number;
|
|
@@ -473,6 +478,16 @@ ${JSON.stringify(e)}
|
|
|
473
478
|
allowedIssueAuthors,
|
|
474
479
|
});
|
|
475
480
|
}
|
|
481
|
+
if (input.startPreparation.autoRevertReopenedDoneEnabled) {
|
|
482
|
+
try {
|
|
483
|
+
await this.reopenedDoneIssueRevertUseCase.run({ project, issues });
|
|
484
|
+
} catch (revertError) {
|
|
485
|
+
console.error(
|
|
486
|
+
`[HandleScheduledEvent] Failed to revert reopened Done issues for project ${project.url}: ${revertError instanceof Error ? revertError.message : String(revertError)}`,
|
|
487
|
+
revertError,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
476
491
|
if (input.startPreparation.autoAdvanceQualityCheckEnabled) {
|
|
477
492
|
try {
|
|
478
493
|
await this.qualityCheckAdvanceUseCase.run({
|
|
@@ -489,6 +504,7 @@ ${JSON.stringify(e)}
|
|
|
489
504
|
);
|
|
490
505
|
}
|
|
491
506
|
}
|
|
507
|
+
await this.issueNoStatusUpdateUseCase.run({ project, issues });
|
|
492
508
|
const preparationResult = await this.startPreparationUseCase.run({
|
|
493
509
|
projectUrl: input.projectUrl,
|
|
494
510
|
defaultAgentName: input.startPreparation.defaultAgentName,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { mock } from 'jest-mock-extended';
|
|
2
|
+
import { IssueNoStatusUpdateUseCase } from './IssueNoStatusUpdateUseCase';
|
|
3
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
|
+
import { Issue } from '../entities/Issue';
|
|
5
|
+
import { FieldOption, Project } from '../entities/Project';
|
|
6
|
+
|
|
7
|
+
describe('IssueNoStatusUpdateUseCase', () => {
|
|
8
|
+
const mockIssueRepository = mock<IssueRepository>();
|
|
9
|
+
|
|
10
|
+
const awaitingWorkspaceStatus = mock<FieldOption>();
|
|
11
|
+
awaitingWorkspaceStatus.id = 'status-awaiting';
|
|
12
|
+
awaitingWorkspaceStatus.name = 'Awaiting Workspace';
|
|
13
|
+
|
|
14
|
+
const inProgressStatus = mock<FieldOption>();
|
|
15
|
+
inProgressStatus.id = 'status-in-progress';
|
|
16
|
+
inProgressStatus.name = 'In Progress';
|
|
17
|
+
|
|
18
|
+
const basicProject: Project = {
|
|
19
|
+
...mock<Project>(),
|
|
20
|
+
status: {
|
|
21
|
+
name: 'Status',
|
|
22
|
+
fieldId: 'statusFieldId',
|
|
23
|
+
statuses: [awaitingWorkspaceStatus, inProgressStatus],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const projectWithoutAwaitingWorkspace: Project = {
|
|
28
|
+
...mock<Project>(),
|
|
29
|
+
status: {
|
|
30
|
+
name: 'Status',
|
|
31
|
+
fieldId: 'statusFieldId',
|
|
32
|
+
statuses: [inProgressStatus],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const openNullStatusIssue: Issue = {
|
|
37
|
+
...mock<Issue>(),
|
|
38
|
+
isClosed: false,
|
|
39
|
+
status: null,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const openInProgressIssue: Issue = {
|
|
43
|
+
...mock<Issue>(),
|
|
44
|
+
isClosed: false,
|
|
45
|
+
status: 'In Progress',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const closedNullStatusIssue: Issue = {
|
|
49
|
+
...mock<Issue>(),
|
|
50
|
+
isClosed: true,
|
|
51
|
+
status: null,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let useCase: IssueNoStatusUpdateUseCase;
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.clearAllMocks();
|
|
58
|
+
useCase = new IssueNoStatusUpdateUseCase(mockIssueRepository);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('run', () => {
|
|
62
|
+
const testCases: {
|
|
63
|
+
name: string;
|
|
64
|
+
project: Project;
|
|
65
|
+
issues: Issue[];
|
|
66
|
+
expectedUpdateStatusCalls: [Project, Issue, string][];
|
|
67
|
+
}[] = [
|
|
68
|
+
{
|
|
69
|
+
name: 'should call updateStatus for open issues with null status',
|
|
70
|
+
project: basicProject,
|
|
71
|
+
issues: [openNullStatusIssue],
|
|
72
|
+
expectedUpdateStatusCalls: [
|
|
73
|
+
[basicProject, openNullStatusIssue, 'status-awaiting'],
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'should skip issues with an existing status',
|
|
78
|
+
project: basicProject,
|
|
79
|
+
issues: [openInProgressIssue],
|
|
80
|
+
expectedUpdateStatusCalls: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'should skip closed issues with null status',
|
|
84
|
+
project: basicProject,
|
|
85
|
+
issues: [closedNullStatusIssue],
|
|
86
|
+
expectedUpdateStatusCalls: [],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: 'should not call updateStatus when Awaiting Workspace status does not exist in project',
|
|
90
|
+
project: projectWithoutAwaitingWorkspace,
|
|
91
|
+
issues: [openNullStatusIssue],
|
|
92
|
+
expectedUpdateStatusCalls: [],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'should handle empty issues array',
|
|
96
|
+
project: basicProject,
|
|
97
|
+
issues: [],
|
|
98
|
+
expectedUpdateStatusCalls: [],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'should call updateStatus only for open null-status issues among mixed issues',
|
|
102
|
+
project: basicProject,
|
|
103
|
+
issues: [
|
|
104
|
+
openNullStatusIssue,
|
|
105
|
+
openInProgressIssue,
|
|
106
|
+
closedNullStatusIssue,
|
|
107
|
+
],
|
|
108
|
+
expectedUpdateStatusCalls: [
|
|
109
|
+
[basicProject, openNullStatusIssue, 'status-awaiting'],
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
testCases.forEach(
|
|
115
|
+
({ name, project, issues, expectedUpdateStatusCalls }) => {
|
|
116
|
+
it(name, async () => {
|
|
117
|
+
await useCase.run({ project, issues });
|
|
118
|
+
|
|
119
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toEqual(
|
|
120
|
+
expectedUpdateStatusCalls,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
it('should skip archived items and continue updating remaining issues', async () => {
|
|
127
|
+
const archivedIssue: Issue = {
|
|
128
|
+
...mock<Issue>(),
|
|
129
|
+
isClosed: false,
|
|
130
|
+
status: null,
|
|
131
|
+
};
|
|
132
|
+
const normalIssue: Issue = {
|
|
133
|
+
...mock<Issue>(),
|
|
134
|
+
isClosed: false,
|
|
135
|
+
status: null,
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
mockIssueRepository.updateStatus
|
|
139
|
+
.mockRejectedValueOnce(
|
|
140
|
+
new Error('The item is archived and cannot be updated'),
|
|
141
|
+
)
|
|
142
|
+
.mockResolvedValueOnce(undefined);
|
|
143
|
+
|
|
144
|
+
await useCase.run({
|
|
145
|
+
project: basicProject,
|
|
146
|
+
issues: [archivedIssue, normalIssue],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toEqual([
|
|
150
|
+
[basicProject, archivedIssue, 'status-awaiting'],
|
|
151
|
+
[basicProject, normalIssue, 'status-awaiting'],
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should re-throw non-archived errors from updateStatus', async () => {
|
|
156
|
+
mockIssueRepository.updateStatus.mockRejectedValueOnce(
|
|
157
|
+
new Error('GraphQL rate limit exceeded'),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
useCase.run({ project: basicProject, issues: [openNullStatusIssue] }),
|
|
162
|
+
).rejects.toThrow('GraphQL rate limit exceeded');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Issue } from '../entities/Issue';
|
|
2
|
+
import { Project } from '../entities/Project';
|
|
3
|
+
import { AWAITING_WORKSPACE_STATUS_NAME } from '../entities/WorkflowStatus';
|
|
4
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
5
|
+
|
|
6
|
+
const isArchivedProjectItemError = (error: unknown): boolean => {
|
|
7
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8
|
+
return message.toLowerCase().includes('archived');
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class IssueNoStatusUpdateUseCase {
|
|
12
|
+
constructor(
|
|
13
|
+
readonly issueRepository: Pick<IssueRepository, 'updateStatus'>,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
run = async (input: { project: Project; issues: Issue[] }): Promise<void> => {
|
|
17
|
+
const awaitingWorkspaceStatus = input.project.status.statuses.find(
|
|
18
|
+
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
19
|
+
);
|
|
20
|
+
if (!awaitingWorkspaceStatus) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const issue of input.issues) {
|
|
24
|
+
if (issue.isClosed || issue.status !== null) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await this.issueRepository.updateStatus(
|
|
29
|
+
input.project,
|
|
30
|
+
issue,
|
|
31
|
+
awaitingWorkspaceStatus.id,
|
|
32
|
+
);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (isArchivedProjectItemError(error)) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`IssueNoStatusUpdateUseCase: project item is archived and cannot be updated, skipping. issueUrl: ${issue.url}`,
|
|
37
|
+
);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|