github-issue-tower-defence-management 1.52.1 → 1.54.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 +19 -0
- package/README.md +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +45 -31
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/entities/WorkflowStatus.js +5 -7
- package/bin/domain/entities/WorkflowStatus.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +8 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +80 -0
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +4 -67
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.js +42 -0
- package/bin/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.js.map +1 -0
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +19 -3
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +8 -0
- package/src/domain/entities/WorkflowStatus.ts +5 -6
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +58 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +11 -0
- package/src/domain/usecases/IssueRejectionEvaluator.ts +114 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +15 -89
- package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +376 -0
- package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.ts +88 -0
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +177 -9
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +34 -2
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/entities/WorkflowStatus.d.ts +4 -2
- package/types/domain/entities/WorkflowStatus.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/IssueRejectionEvaluator.d.ts +20 -0
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.d.ts +15 -0
- package/types/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.d.ts.map +1 -0
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts +2 -0
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
|
|
2
|
+
import { Issue } from '../entities/Issue';
|
|
3
|
+
import { Project } from '../entities/Project';
|
|
4
|
+
|
|
5
|
+
const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
|
6
|
+
id: 'project-1',
|
|
7
|
+
url: 'https://github.com/users/user/projects/1',
|
|
8
|
+
databaseId: 1,
|
|
9
|
+
name: 'Test Project',
|
|
10
|
+
status: {
|
|
11
|
+
name: 'Status',
|
|
12
|
+
fieldId: 'field-1',
|
|
13
|
+
statuses: [
|
|
14
|
+
{
|
|
15
|
+
id: 'awaiting-workspace-id',
|
|
16
|
+
name: 'Awaiting Workspace',
|
|
17
|
+
color: 'GRAY',
|
|
18
|
+
description: '',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'awaiting-quality-check-id',
|
|
22
|
+
name: 'Awaiting Quality Check',
|
|
23
|
+
color: 'BLUE',
|
|
24
|
+
description: '',
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
nextActionDate: null,
|
|
29
|
+
nextActionHour: null,
|
|
30
|
+
story: null,
|
|
31
|
+
remainingEstimationMinutes: null,
|
|
32
|
+
dependedIssueUrlSeparatedByComma: null,
|
|
33
|
+
completionDate50PercentConfidence: null,
|
|
34
|
+
...overrides,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
|
|
38
|
+
nameWithOwner: 'user/repo',
|
|
39
|
+
number: 1,
|
|
40
|
+
title: 'Test Issue',
|
|
41
|
+
state: 'OPEN',
|
|
42
|
+
status: 'Awaiting Quality Check',
|
|
43
|
+
story: null,
|
|
44
|
+
nextActionDate: null,
|
|
45
|
+
nextActionHour: null,
|
|
46
|
+
estimationMinutes: null,
|
|
47
|
+
dependedIssueUrls: [],
|
|
48
|
+
completionDate50PercentConfidence: null,
|
|
49
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
50
|
+
assignees: [],
|
|
51
|
+
labels: [],
|
|
52
|
+
org: 'user',
|
|
53
|
+
repo: 'repo',
|
|
54
|
+
body: '',
|
|
55
|
+
itemId: 'item-1',
|
|
56
|
+
isPr: false,
|
|
57
|
+
isInProgress: false,
|
|
58
|
+
isClosed: false,
|
|
59
|
+
createdAt: new Date(),
|
|
60
|
+
author: '',
|
|
61
|
+
...overrides,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const createReadyPr = (url = 'https://github.com/user/repo/pull/1') => ({
|
|
65
|
+
url,
|
|
66
|
+
isConflicted: false,
|
|
67
|
+
isPassedAllCiJob: true,
|
|
68
|
+
isCiStateSuccess: true,
|
|
69
|
+
isResolvedAllReviewComments: true,
|
|
70
|
+
isBranchOutOfDate: false,
|
|
71
|
+
missingRequiredCheckNames: [],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('RevertNotReadyAwaitingQualityCheckUseCase', () => {
|
|
75
|
+
let mockProjectRepository: {
|
|
76
|
+
findProjectIdByUrl: jest.Mock;
|
|
77
|
+
getProject: jest.Mock;
|
|
78
|
+
};
|
|
79
|
+
let mockIssueRepository: {
|
|
80
|
+
getAllIssues: jest.Mock;
|
|
81
|
+
updateStatus: jest.Mock;
|
|
82
|
+
findRelatedOpenPRs: jest.Mock;
|
|
83
|
+
getOpenPullRequest: jest.Mock;
|
|
84
|
+
};
|
|
85
|
+
let mockIssueCommentRepository: {
|
|
86
|
+
createComment: jest.Mock;
|
|
87
|
+
};
|
|
88
|
+
let mockProject: Project;
|
|
89
|
+
let useCase: RevertNotReadyAwaitingQualityCheckUseCase;
|
|
90
|
+
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
jest.resetAllMocks();
|
|
93
|
+
|
|
94
|
+
mockProject = createMockProject();
|
|
95
|
+
|
|
96
|
+
mockProjectRepository = {
|
|
97
|
+
findProjectIdByUrl: jest.fn().mockResolvedValue('project-1'),
|
|
98
|
+
getProject: jest.fn().mockResolvedValue(mockProject),
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
mockIssueRepository = {
|
|
102
|
+
getAllIssues: jest
|
|
103
|
+
.fn()
|
|
104
|
+
.mockResolvedValue({ issues: [], cacheUsed: false }),
|
|
105
|
+
updateStatus: jest.fn().mockResolvedValue(undefined),
|
|
106
|
+
findRelatedOpenPRs: jest.fn().mockResolvedValue([]),
|
|
107
|
+
getOpenPullRequest: jest.fn().mockResolvedValue(null),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
mockIssueCommentRepository = {
|
|
111
|
+
createComment: jest.fn().mockResolvedValue(undefined),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
useCase = new RevertNotReadyAwaitingQualityCheckUseCase(
|
|
115
|
+
mockProjectRepository,
|
|
116
|
+
mockIssueRepository,
|
|
117
|
+
mockIssueCommentRepository,
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should do nothing when there are no Awaiting Quality Check issues', async () => {
|
|
122
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
123
|
+
issues: [
|
|
124
|
+
createMockIssue({ status: 'Awaiting Workspace' }),
|
|
125
|
+
createMockIssue({ status: 'Preparation' }),
|
|
126
|
+
],
|
|
127
|
+
cacheUsed: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await useCase.run({
|
|
131
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
132
|
+
allowIssueCacheMinutes: 10,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
136
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should skip Awaiting Quality Check issue with llm-agent label', async () => {
|
|
140
|
+
const issue = createMockIssue({
|
|
141
|
+
status: 'Awaiting Quality Check',
|
|
142
|
+
labels: ['llm-agent'],
|
|
143
|
+
});
|
|
144
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
145
|
+
issues: [issue],
|
|
146
|
+
cacheUsed: false,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await useCase.run({
|
|
150
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
151
|
+
allowIssueCacheMinutes: 10,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
155
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('should revert issue when no linked PR is found', async () => {
|
|
159
|
+
const issue = createMockIssue({
|
|
160
|
+
status: 'Awaiting Quality Check',
|
|
161
|
+
});
|
|
162
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
163
|
+
issues: [issue],
|
|
164
|
+
cacheUsed: false,
|
|
165
|
+
});
|
|
166
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
167
|
+
|
|
168
|
+
await useCase.run({
|
|
169
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
170
|
+
allowIssueCacheMinutes: 10,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
174
|
+
mockProject,
|
|
175
|
+
issue,
|
|
176
|
+
'awaiting-workspace-id',
|
|
177
|
+
);
|
|
178
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
179
|
+
issue,
|
|
180
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
181
|
+
);
|
|
182
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
183
|
+
issue,
|
|
184
|
+
expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should not revert issue when PR is ready', async () => {
|
|
189
|
+
const issue = createMockIssue({
|
|
190
|
+
status: 'Awaiting Quality Check',
|
|
191
|
+
});
|
|
192
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
193
|
+
issues: [issue],
|
|
194
|
+
cacheUsed: false,
|
|
195
|
+
});
|
|
196
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([createReadyPr()]);
|
|
197
|
+
|
|
198
|
+
await useCase.run({
|
|
199
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
200
|
+
allowIssueCacheMinutes: 10,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
204
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should revert issue when PR is conflicted', async () => {
|
|
208
|
+
const issue = createMockIssue({
|
|
209
|
+
status: 'Awaiting Quality Check',
|
|
210
|
+
});
|
|
211
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
212
|
+
issues: [issue],
|
|
213
|
+
cacheUsed: false,
|
|
214
|
+
});
|
|
215
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
216
|
+
{ ...createReadyPr(), isConflicted: true },
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
await useCase.run({
|
|
220
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
221
|
+
allowIssueCacheMinutes: 10,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
225
|
+
mockProject,
|
|
226
|
+
issue,
|
|
227
|
+
'awaiting-workspace-id',
|
|
228
|
+
);
|
|
229
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
230
|
+
issue,
|
|
231
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
232
|
+
);
|
|
233
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
234
|
+
issue,
|
|
235
|
+
expect.stringContaining('PULL_REQUEST_CONFLICTED'),
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should revert issue when CI is failing', async () => {
|
|
240
|
+
const issue = createMockIssue({
|
|
241
|
+
status: 'Awaiting Quality Check',
|
|
242
|
+
});
|
|
243
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
244
|
+
issues: [issue],
|
|
245
|
+
cacheUsed: false,
|
|
246
|
+
});
|
|
247
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
248
|
+
{
|
|
249
|
+
...createReadyPr(),
|
|
250
|
+
isPassedAllCiJob: false,
|
|
251
|
+
isCiStateSuccess: false,
|
|
252
|
+
},
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
await useCase.run({
|
|
256
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
257
|
+
allowIssueCacheMinutes: 10,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
261
|
+
mockProject,
|
|
262
|
+
issue,
|
|
263
|
+
'awaiting-workspace-id',
|
|
264
|
+
);
|
|
265
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
266
|
+
issue,
|
|
267
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
268
|
+
);
|
|
269
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
270
|
+
issue,
|
|
271
|
+
expect.stringContaining('ANY_CI_JOB_FAILED_OR_IN_PROGRESS'),
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should revert issue when review comments are not resolved', async () => {
|
|
276
|
+
const issue = createMockIssue({
|
|
277
|
+
status: 'Awaiting Quality Check',
|
|
278
|
+
});
|
|
279
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
280
|
+
issues: [issue],
|
|
281
|
+
cacheUsed: false,
|
|
282
|
+
});
|
|
283
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
284
|
+
{ ...createReadyPr(), isResolvedAllReviewComments: false },
|
|
285
|
+
]);
|
|
286
|
+
|
|
287
|
+
await useCase.run({
|
|
288
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
289
|
+
allowIssueCacheMinutes: 10,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
293
|
+
mockProject,
|
|
294
|
+
issue,
|
|
295
|
+
'awaiting-workspace-id',
|
|
296
|
+
);
|
|
297
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
298
|
+
issue,
|
|
299
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
300
|
+
);
|
|
301
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
302
|
+
issue,
|
|
303
|
+
expect.stringContaining('ANY_REVIEW_COMMENT_NOT_RESOLVED'),
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should revert issue when multiple linked open PRs are found', async () => {
|
|
308
|
+
const issue = createMockIssue({
|
|
309
|
+
status: 'Awaiting Quality Check',
|
|
310
|
+
});
|
|
311
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
312
|
+
issues: [issue],
|
|
313
|
+
cacheUsed: false,
|
|
314
|
+
});
|
|
315
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
316
|
+
createReadyPr('https://github.com/user/repo/pull/1'),
|
|
317
|
+
createReadyPr('https://github.com/user/repo/pull/2'),
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
await useCase.run({
|
|
321
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
322
|
+
allowIssueCacheMinutes: 10,
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
326
|
+
mockProject,
|
|
327
|
+
issue,
|
|
328
|
+
'awaiting-workspace-id',
|
|
329
|
+
);
|
|
330
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
331
|
+
issue,
|
|
332
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
333
|
+
);
|
|
334
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
335
|
+
issue,
|
|
336
|
+
expect.stringContaining('MULTIPLE_PULL_REQUESTS_FOUND'),
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should revert issue when CI is SUCCESS but required check never started', async () => {
|
|
341
|
+
const issue = createMockIssue({
|
|
342
|
+
status: 'Awaiting Quality Check',
|
|
343
|
+
});
|
|
344
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
345
|
+
issues: [issue],
|
|
346
|
+
cacheUsed: false,
|
|
347
|
+
});
|
|
348
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
349
|
+
{
|
|
350
|
+
...createReadyPr(),
|
|
351
|
+
isPassedAllCiJob: false,
|
|
352
|
+
isCiStateSuccess: true,
|
|
353
|
+
missingRequiredCheckNames: ['E2E Tests'],
|
|
354
|
+
},
|
|
355
|
+
]);
|
|
356
|
+
|
|
357
|
+
await useCase.run({
|
|
358
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
359
|
+
allowIssueCacheMinutes: 10,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
363
|
+
mockProject,
|
|
364
|
+
issue,
|
|
365
|
+
'awaiting-workspace-id',
|
|
366
|
+
);
|
|
367
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
368
|
+
issue,
|
|
369
|
+
expect.stringContaining('Auto Status Check: REJECTED'),
|
|
370
|
+
);
|
|
371
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
372
|
+
issue,
|
|
373
|
+
expect.stringContaining('REQUIRED_CI_JOB_NEVER_STARTED'),
|
|
374
|
+
);
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
2
|
+
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
|
+
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
4
|
+
import { IssueRejectionEvaluator } from './IssueRejectionEvaluator';
|
|
5
|
+
import {
|
|
6
|
+
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
7
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
8
|
+
} from '../entities/WorkflowStatus';
|
|
9
|
+
|
|
10
|
+
export class RevertNotReadyAwaitingQualityCheckUseCase {
|
|
11
|
+
private readonly issueRejectionEvaluator: IssueRejectionEvaluator;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly projectRepository: Pick<
|
|
15
|
+
ProjectRepository,
|
|
16
|
+
'findProjectIdByUrl' | 'getProject'
|
|
17
|
+
>,
|
|
18
|
+
private readonly issueRepository: Pick<
|
|
19
|
+
IssueRepository,
|
|
20
|
+
| 'getAllIssues'
|
|
21
|
+
| 'updateStatus'
|
|
22
|
+
| 'findRelatedOpenPRs'
|
|
23
|
+
| 'getOpenPullRequest'
|
|
24
|
+
>,
|
|
25
|
+
private readonly issueCommentRepository: Pick<
|
|
26
|
+
IssueCommentRepository,
|
|
27
|
+
'createComment'
|
|
28
|
+
>,
|
|
29
|
+
) {
|
|
30
|
+
this.issueRejectionEvaluator = new IssueRejectionEvaluator(issueRepository);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
run = async (params: {
|
|
34
|
+
projectUrl: string;
|
|
35
|
+
allowIssueCacheMinutes: number;
|
|
36
|
+
}): Promise<void> => {
|
|
37
|
+
const projectId = await this.projectRepository.findProjectIdByUrl(
|
|
38
|
+
params.projectUrl,
|
|
39
|
+
);
|
|
40
|
+
if (!projectId) {
|
|
41
|
+
throw new Error(`Project not found. projectUrl: ${params.projectUrl}`);
|
|
42
|
+
}
|
|
43
|
+
const project = await this.projectRepository.getProject(projectId);
|
|
44
|
+
if (!project) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
`Project not found. projectId: ${projectId} projectUrl: ${params.projectUrl}`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const awaitingWorkspaceStatusOption = project.status.statuses.find(
|
|
51
|
+
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
52
|
+
);
|
|
53
|
+
if (!awaitingWorkspaceStatusOption) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { issues } = await this.issueRepository.getAllIssues(
|
|
58
|
+
projectId,
|
|
59
|
+
params.allowIssueCacheMinutes,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const awaitingQualityCheckIssues = issues.filter(
|
|
63
|
+
(issue) => issue.status === AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
for (const issue of awaitingQualityCheckIssues) {
|
|
67
|
+
const hasLlmAgentLabel = issue.labels.some(
|
|
68
|
+
(l) => l === 'llm-agent' || l.startsWith('llm-agent:'),
|
|
69
|
+
);
|
|
70
|
+
if (hasLlmAgentLabel) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const { rejections } = await this.issueRejectionEvaluator.evaluate(issue);
|
|
75
|
+
if (rejections.length > 0) {
|
|
76
|
+
await this.issueRepository.updateStatus(
|
|
77
|
+
project,
|
|
78
|
+
issue,
|
|
79
|
+
awaitingWorkspaceStatusOption.id,
|
|
80
|
+
);
|
|
81
|
+
await this.issueCommentRepository.createComment(
|
|
82
|
+
issue,
|
|
83
|
+
`Auto Status Check: REJECTED\n${rejections.map((r) => `- ${r.detail}`).join('\n')}`,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
FAILED_PREPARATION_STATUS_NAME,
|
|
12
12
|
ICEBOX_STATUS_NAME,
|
|
13
13
|
IN_TMUX_STATUS_NAME,
|
|
14
|
+
LEGACY_IN_TMUX_STATUS_NAME,
|
|
15
|
+
LEGACY_TODO_STATUS_NAME,
|
|
14
16
|
PC_TODO_STATUS_NAME,
|
|
15
17
|
PREPARATION_STATUS_NAME,
|
|
16
18
|
REQUIRED_WORKFLOW_STATUSES,
|
|
@@ -44,7 +46,7 @@ const buildCanonicalStatuses = (): FieldOption[] =>
|
|
|
44
46
|
}));
|
|
45
47
|
|
|
46
48
|
describe('SetupTowerDefenceProjectUseCase', () => {
|
|
47
|
-
it('should define exactly the
|
|
49
|
+
it('should define exactly the 10 required statuses in the documented order with the documented colors and no descriptions', () => {
|
|
48
50
|
expect(REQUIRED_WORKFLOW_STATUSES).toEqual([
|
|
49
51
|
{ name: DEFAULT_STATUS_NAME, color: 'ORANGE' },
|
|
50
52
|
{ name: AWAITING_TASK_BREAKDOWN_STATUS_NAME, color: 'ORANGE' },
|
|
@@ -53,7 +55,6 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
53
55
|
{ name: FAILED_PREPARATION_STATUS_NAME, color: 'RED' },
|
|
54
56
|
{ name: AWAITING_QUALITY_CHECK_STATUS_NAME, color: 'GREEN' },
|
|
55
57
|
{ name: TODO_STATUS_NAME, color: 'PINK' },
|
|
56
|
-
{ name: PC_TODO_STATUS_NAME, color: 'PINK' },
|
|
57
58
|
{ name: IN_TMUX_STATUS_NAME, color: 'RED' },
|
|
58
59
|
{ name: DONE_STATUS_NAME, color: 'PURPLE' },
|
|
59
60
|
{ name: ICEBOX_STATUS_NAME, color: 'GRAY' },
|
|
@@ -187,12 +188,6 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
187
188
|
color: 'PINK',
|
|
188
189
|
description: '',
|
|
189
190
|
},
|
|
190
|
-
{
|
|
191
|
-
id: null,
|
|
192
|
-
name: PC_TODO_STATUS_NAME,
|
|
193
|
-
color: 'PINK',
|
|
194
|
-
description: '',
|
|
195
|
-
},
|
|
196
191
|
{
|
|
197
192
|
id: null,
|
|
198
193
|
name: IN_TMUX_STATUS_NAME,
|
|
@@ -249,7 +244,6 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
249
244
|
FAILED_PREPARATION_STATUS_NAME,
|
|
250
245
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
251
246
|
TODO_STATUS_NAME,
|
|
252
|
-
PC_TODO_STATUS_NAME,
|
|
253
247
|
IN_TMUX_STATUS_NAME,
|
|
254
248
|
DONE_STATUS_NAME,
|
|
255
249
|
ICEBOX_STATUS_NAME,
|
|
@@ -272,4 +266,178 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
272
266
|
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
273
267
|
expect(payload[2].color).toBe(REQUIRED_WORKFLOW_STATUSES[2].color);
|
|
274
268
|
});
|
|
269
|
+
|
|
270
|
+
it('should rename legacy "Todo" to "Todo by human" by reusing the existing option ID', async () => {
|
|
271
|
+
const mockProjectRepository =
|
|
272
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
273
|
+
const statuses: FieldOption[] = REQUIRED_WORKFLOW_STATUSES.map(
|
|
274
|
+
(required, index) => ({
|
|
275
|
+
id: `id-${index}`,
|
|
276
|
+
name:
|
|
277
|
+
required.name === TODO_STATUS_NAME
|
|
278
|
+
? LEGACY_TODO_STATUS_NAME
|
|
279
|
+
: required.name,
|
|
280
|
+
color: required.color,
|
|
281
|
+
description: '',
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
const project = buildProject(statuses);
|
|
285
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
286
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
287
|
+
|
|
288
|
+
const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
|
|
289
|
+
await useCase.run({ projectUrl: project.url });
|
|
290
|
+
|
|
291
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
292
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
293
|
+
const todoEntry = payload.find((s) => s.name === TODO_STATUS_NAME);
|
|
294
|
+
expect(todoEntry).toBeDefined();
|
|
295
|
+
expect(todoEntry?.id).toBe('id-6');
|
|
296
|
+
expect(payload.some((s) => s.name === LEGACY_TODO_STATUS_NAME)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should rename legacy "In Tmux" to "In Tmux by human" by reusing the existing option ID', async () => {
|
|
300
|
+
const mockProjectRepository =
|
|
301
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
302
|
+
const statuses: FieldOption[] = REQUIRED_WORKFLOW_STATUSES.map(
|
|
303
|
+
(required, index) => ({
|
|
304
|
+
id: `id-${index}`,
|
|
305
|
+
name:
|
|
306
|
+
required.name === IN_TMUX_STATUS_NAME
|
|
307
|
+
? LEGACY_IN_TMUX_STATUS_NAME
|
|
308
|
+
: required.name,
|
|
309
|
+
color: required.color,
|
|
310
|
+
description: '',
|
|
311
|
+
}),
|
|
312
|
+
);
|
|
313
|
+
const project = buildProject(statuses);
|
|
314
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
315
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
316
|
+
|
|
317
|
+
const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
|
|
318
|
+
await useCase.run({ projectUrl: project.url });
|
|
319
|
+
|
|
320
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
321
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
322
|
+
const inTmuxEntry = payload.find((s) => s.name === IN_TMUX_STATUS_NAME);
|
|
323
|
+
expect(inTmuxEntry).toBeDefined();
|
|
324
|
+
expect(inTmuxEntry?.id).toBe('id-7');
|
|
325
|
+
expect(payload.some((s) => s.name === LEGACY_IN_TMUX_STATUS_NAME)).toBe(
|
|
326
|
+
false,
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should remove "PC Todo" from the status list and not include it in others', async () => {
|
|
331
|
+
const mockProjectRepository =
|
|
332
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
333
|
+
const statuses: FieldOption[] = [
|
|
334
|
+
...buildCanonicalStatuses(),
|
|
335
|
+
{
|
|
336
|
+
id: 'pc-todo-id',
|
|
337
|
+
name: PC_TODO_STATUS_NAME,
|
|
338
|
+
color: 'PINK',
|
|
339
|
+
description: '',
|
|
340
|
+
},
|
|
341
|
+
];
|
|
342
|
+
const project = buildProject(statuses);
|
|
343
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
344
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
345
|
+
|
|
346
|
+
const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
|
|
347
|
+
await useCase.run({ projectUrl: project.url });
|
|
348
|
+
|
|
349
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
350
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
351
|
+
expect(payload.some((s) => s.name === PC_TODO_STATUS_NAME)).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('should migrate a project with legacy statuses: rename Todo and In Tmux by ID, remove PC Todo', async () => {
|
|
355
|
+
const mockProjectRepository =
|
|
356
|
+
mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
|
|
357
|
+
const statuses: FieldOption[] = [
|
|
358
|
+
{
|
|
359
|
+
id: 'id-0',
|
|
360
|
+
name: DEFAULT_STATUS_NAME,
|
|
361
|
+
color: 'ORANGE',
|
|
362
|
+
description: '',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 'id-1',
|
|
366
|
+
name: AWAITING_TASK_BREAKDOWN_STATUS_NAME,
|
|
367
|
+
color: 'ORANGE',
|
|
368
|
+
description: '',
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: 'id-2',
|
|
372
|
+
name: AWAITING_WORKSPACE_STATUS_NAME,
|
|
373
|
+
color: 'BLUE',
|
|
374
|
+
description: '',
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: 'id-3',
|
|
378
|
+
name: PREPARATION_STATUS_NAME,
|
|
379
|
+
color: 'YELLOW',
|
|
380
|
+
description: '',
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: 'id-4',
|
|
384
|
+
name: FAILED_PREPARATION_STATUS_NAME,
|
|
385
|
+
color: 'RED',
|
|
386
|
+
description: '',
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
id: 'id-5',
|
|
390
|
+
name: AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
391
|
+
color: 'GREEN',
|
|
392
|
+
description: '',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 'id-6',
|
|
396
|
+
name: LEGACY_TODO_STATUS_NAME,
|
|
397
|
+
color: 'PINK',
|
|
398
|
+
description: '',
|
|
399
|
+
},
|
|
400
|
+
{ id: 'id-7', name: PC_TODO_STATUS_NAME, color: 'PINK', description: '' },
|
|
401
|
+
{
|
|
402
|
+
id: 'id-8',
|
|
403
|
+
name: LEGACY_IN_TMUX_STATUS_NAME,
|
|
404
|
+
color: 'RED',
|
|
405
|
+
description: '',
|
|
406
|
+
},
|
|
407
|
+
{ id: 'id-9', name: DONE_STATUS_NAME, color: 'PURPLE', description: '' },
|
|
408
|
+
{ id: 'id-10', name: ICEBOX_STATUS_NAME, color: 'GRAY', description: '' },
|
|
409
|
+
];
|
|
410
|
+
const project = buildProject(statuses);
|
|
411
|
+
mockProjectRepository.getByUrl.mockResolvedValue(project);
|
|
412
|
+
mockProjectRepository.updateStatusList.mockResolvedValue([]);
|
|
413
|
+
|
|
414
|
+
const useCase = new SetupTowerDefenceProjectUseCase(mockProjectRepository);
|
|
415
|
+
await useCase.run({ projectUrl: project.url });
|
|
416
|
+
|
|
417
|
+
expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
|
|
418
|
+
const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
|
|
419
|
+
|
|
420
|
+
expect(payload.map((s) => s.name)).toEqual([
|
|
421
|
+
DEFAULT_STATUS_NAME,
|
|
422
|
+
AWAITING_TASK_BREAKDOWN_STATUS_NAME,
|
|
423
|
+
AWAITING_WORKSPACE_STATUS_NAME,
|
|
424
|
+
PREPARATION_STATUS_NAME,
|
|
425
|
+
FAILED_PREPARATION_STATUS_NAME,
|
|
426
|
+
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
427
|
+
TODO_STATUS_NAME,
|
|
428
|
+
IN_TMUX_STATUS_NAME,
|
|
429
|
+
DONE_STATUS_NAME,
|
|
430
|
+
ICEBOX_STATUS_NAME,
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
expect(payload.find((s) => s.name === TODO_STATUS_NAME)?.id).toBe('id-6');
|
|
434
|
+
expect(payload.find((s) => s.name === IN_TMUX_STATUS_NAME)?.id).toBe(
|
|
435
|
+
'id-8',
|
|
436
|
+
);
|
|
437
|
+
expect(payload.some((s) => s.name === PC_TODO_STATUS_NAME)).toBe(false);
|
|
438
|
+
expect(payload.some((s) => s.name === LEGACY_TODO_STATUS_NAME)).toBe(false);
|
|
439
|
+
expect(payload.some((s) => s.name === LEGACY_IN_TMUX_STATUS_NAME)).toBe(
|
|
440
|
+
false,
|
|
441
|
+
);
|
|
442
|
+
});
|
|
275
443
|
});
|