github-issue-tower-defence-management 1.44.4 → 1.44.6
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/.github/workflows/api-created_issue_pr.yml +8 -1
- package/.github/workflows/umino-project.yml +55 -18
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/bin/adapter/entry-points/cli/index.js +4 -1
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +2 -2
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +2 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +2 -2
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +65 -3
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +4 -6
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +6 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +5 -2
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +3 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +8 -4
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +316 -11
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +107 -11
- package/src/domain/usecases/StartPreparationUseCase.test.ts +76 -79
- package/src/domain/usecases/StartPreparationUseCase.ts +10 -8
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +8 -2
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +2 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
|
|
2
2
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
+
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
3
4
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
5
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
5
6
|
import { Issue } from '../entities/Issue';
|
|
@@ -46,6 +47,12 @@ const createMockProject = (): Project => ({
|
|
|
46
47
|
{ id: '1', name: 'Awaiting Workspace', color: 'GRAY', description: '' },
|
|
47
48
|
{ id: '2', name: 'Preparation', color: 'YELLOW', description: '' },
|
|
48
49
|
{ id: '3', name: 'Done', color: 'GREEN', description: '' },
|
|
50
|
+
{
|
|
51
|
+
id: '4',
|
|
52
|
+
name: 'Awaiting Quality Check',
|
|
53
|
+
color: 'BLUE',
|
|
54
|
+
description: '',
|
|
55
|
+
},
|
|
49
56
|
],
|
|
50
57
|
},
|
|
51
58
|
nextActionDate: null,
|
|
@@ -72,13 +79,33 @@ const createMockProject = (): Project => ({
|
|
|
72
79
|
completionDate50PercentConfidence: null,
|
|
73
80
|
});
|
|
74
81
|
|
|
82
|
+
const createPassingPr = () => ({
|
|
83
|
+
url: 'https://github.com/user/repo/pull/5',
|
|
84
|
+
branchName: 'i1',
|
|
85
|
+
isConflicted: false,
|
|
86
|
+
isPassedAllCiJob: true,
|
|
87
|
+
isCiStateSuccess: true,
|
|
88
|
+
isResolvedAllReviewComments: true,
|
|
89
|
+
isBranchOutOfDate: false,
|
|
90
|
+
missingRequiredCheckNames: [],
|
|
91
|
+
});
|
|
92
|
+
|
|
75
93
|
describe('RevertOrphanedPreparationUseCase', () => {
|
|
76
94
|
let useCase: RevertOrphanedPreparationUseCase;
|
|
77
95
|
let mockProjectRepository: Mocked<
|
|
78
96
|
Pick<ProjectRepository, 'findProjectIdByUrl' | 'getProject'>
|
|
79
97
|
>;
|
|
80
98
|
let mockIssueRepository: Mocked<
|
|
81
|
-
Pick<
|
|
99
|
+
Pick<
|
|
100
|
+
IssueRepository,
|
|
101
|
+
| 'getAllIssues'
|
|
102
|
+
| 'updateStatus'
|
|
103
|
+
| 'findRelatedOpenPRs'
|
|
104
|
+
| 'getOpenPullRequest'
|
|
105
|
+
>
|
|
106
|
+
>;
|
|
107
|
+
let mockIssueCommentRepository: Mocked<
|
|
108
|
+
Pick<IssueCommentRepository, 'getCommentsFromIssue'>
|
|
82
109
|
>;
|
|
83
110
|
let mockLocalCommandRunner: Mocked<LocalCommandRunner>;
|
|
84
111
|
let mockProject: Project;
|
|
@@ -95,7 +122,11 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
95
122
|
.fn()
|
|
96
123
|
.mockResolvedValue({ issues: [], cacheUsed: false }),
|
|
97
124
|
updateStatus: jest.fn().mockResolvedValue(undefined),
|
|
98
|
-
|
|
125
|
+
findRelatedOpenPRs: jest.fn().mockResolvedValue([]),
|
|
126
|
+
getOpenPullRequest: jest.fn().mockResolvedValue(null),
|
|
127
|
+
};
|
|
128
|
+
mockIssueCommentRepository = {
|
|
129
|
+
getCommentsFromIssue: jest.fn().mockResolvedValue([]),
|
|
99
130
|
};
|
|
100
131
|
mockLocalCommandRunner = {
|
|
101
132
|
runCommand: jest.fn(),
|
|
@@ -103,11 +134,12 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
103
134
|
useCase = new RevertOrphanedPreparationUseCase(
|
|
104
135
|
mockProjectRepository,
|
|
105
136
|
mockIssueRepository,
|
|
137
|
+
mockIssueCommentRepository,
|
|
106
138
|
mockLocalCommandRunner,
|
|
107
139
|
);
|
|
108
140
|
});
|
|
109
141
|
|
|
110
|
-
it('should revert stuck-Preparation issue to Awaiting Workspace when check command exits non-zero', async () => {
|
|
142
|
+
it('should revert stuck-Preparation issue to Awaiting Workspace when check command exits non-zero and no agent report present', async () => {
|
|
111
143
|
const stuckIssue = createMockIssue({
|
|
112
144
|
url: 'https://github.com/user/repo/issues/10',
|
|
113
145
|
status: 'Preparation',
|
|
@@ -121,11 +153,13 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
121
153
|
stderr: '',
|
|
122
154
|
exitCode: 1,
|
|
123
155
|
});
|
|
156
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
124
157
|
|
|
125
158
|
await useCase.run({
|
|
126
159
|
projectUrl: 'https://github.com/user/repo',
|
|
127
160
|
preparationStatus: 'Preparation',
|
|
128
161
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
162
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
129
163
|
allowIssueCacheMinutes: 60,
|
|
130
164
|
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
131
165
|
});
|
|
@@ -134,8 +168,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
134
168
|
expect(mockIssueRepository.updateStatus.mock.calls[0][0]).toBe(mockProject);
|
|
135
169
|
expect(mockIssueRepository.updateStatus.mock.calls[0][1]).toBe(stuckIssue);
|
|
136
170
|
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
137
|
-
expect(mockIssueRepository.createComment.mock.calls).toHaveLength(1);
|
|
138
|
-
expect(mockIssueRepository.createComment.mock.calls[0][0]).toBe(stuckIssue);
|
|
139
171
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
140
172
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0][0]).toBe('sh');
|
|
141
173
|
expect(mockLocalCommandRunner.runCommand.mock.calls[0][1]).toEqual([
|
|
@@ -146,6 +178,259 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
146
178
|
]);
|
|
147
179
|
});
|
|
148
180
|
|
|
181
|
+
it('should advance orphaned issue to Awaiting Quality Check when agent report and passing PR are present', async () => {
|
|
182
|
+
const stuckIssue = createMockIssue({
|
|
183
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
184
|
+
status: 'Preparation',
|
|
185
|
+
});
|
|
186
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
187
|
+
issues: [stuckIssue],
|
|
188
|
+
cacheUsed: false,
|
|
189
|
+
});
|
|
190
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
191
|
+
stdout: '',
|
|
192
|
+
stderr: '',
|
|
193
|
+
exitCode: 1,
|
|
194
|
+
});
|
|
195
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
196
|
+
{
|
|
197
|
+
author: 'bot',
|
|
198
|
+
content: 'From: agent report',
|
|
199
|
+
createdAt: new Date(),
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
203
|
+
createPassingPr(),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
await useCase.run({
|
|
207
|
+
projectUrl: 'https://github.com/user/repo',
|
|
208
|
+
preparationStatus: 'Preparation',
|
|
209
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
210
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
211
|
+
allowIssueCacheMinutes: 60,
|
|
212
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
216
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('4');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should revert orphaned issue to Awaiting Workspace when agent report present but PR CI is failing', async () => {
|
|
220
|
+
const stuckIssue = createMockIssue({
|
|
221
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
222
|
+
status: 'Preparation',
|
|
223
|
+
});
|
|
224
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
225
|
+
issues: [stuckIssue],
|
|
226
|
+
cacheUsed: false,
|
|
227
|
+
});
|
|
228
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
229
|
+
stdout: '',
|
|
230
|
+
stderr: '',
|
|
231
|
+
exitCode: 1,
|
|
232
|
+
});
|
|
233
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
234
|
+
{
|
|
235
|
+
author: 'bot',
|
|
236
|
+
content: 'From: agent report',
|
|
237
|
+
createdAt: new Date(),
|
|
238
|
+
},
|
|
239
|
+
]);
|
|
240
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
241
|
+
{
|
|
242
|
+
...createPassingPr(),
|
|
243
|
+
isPassedAllCiJob: false,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
|
|
247
|
+
await useCase.run({
|
|
248
|
+
projectUrl: 'https://github.com/user/repo',
|
|
249
|
+
preparationStatus: 'Preparation',
|
|
250
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
251
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
252
|
+
allowIssueCacheMinutes: 60,
|
|
253
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
257
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should revert orphaned issue to Awaiting Workspace when agent report present but no PR found', async () => {
|
|
261
|
+
const stuckIssue = createMockIssue({
|
|
262
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
263
|
+
status: 'Preparation',
|
|
264
|
+
});
|
|
265
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
266
|
+
issues: [stuckIssue],
|
|
267
|
+
cacheUsed: false,
|
|
268
|
+
});
|
|
269
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
270
|
+
stdout: '',
|
|
271
|
+
stderr: '',
|
|
272
|
+
exitCode: 1,
|
|
273
|
+
});
|
|
274
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
275
|
+
{
|
|
276
|
+
author: 'bot',
|
|
277
|
+
content: 'From: agent report',
|
|
278
|
+
createdAt: new Date(),
|
|
279
|
+
},
|
|
280
|
+
]);
|
|
281
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
282
|
+
|
|
283
|
+
await useCase.run({
|
|
284
|
+
projectUrl: 'https://github.com/user/repo',
|
|
285
|
+
preparationStatus: 'Preparation',
|
|
286
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
287
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
288
|
+
allowIssueCacheMinutes: 60,
|
|
289
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
293
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should revert orphaned issue to Awaiting Workspace when awaitingQualityCheckStatus is not provided', async () => {
|
|
297
|
+
const stuckIssue = createMockIssue({
|
|
298
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
299
|
+
status: 'Preparation',
|
|
300
|
+
});
|
|
301
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
302
|
+
issues: [stuckIssue],
|
|
303
|
+
cacheUsed: false,
|
|
304
|
+
});
|
|
305
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
306
|
+
stdout: '',
|
|
307
|
+
stderr: '',
|
|
308
|
+
exitCode: 1,
|
|
309
|
+
});
|
|
310
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
311
|
+
{
|
|
312
|
+
author: 'bot',
|
|
313
|
+
content: 'From: agent report',
|
|
314
|
+
createdAt: new Date(),
|
|
315
|
+
},
|
|
316
|
+
]);
|
|
317
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
318
|
+
createPassingPr(),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
await useCase.run({
|
|
322
|
+
projectUrl: 'https://github.com/user/repo',
|
|
323
|
+
preparationStatus: 'Preparation',
|
|
324
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
325
|
+
allowIssueCacheMinutes: 60,
|
|
326
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
330
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should advance orphaned issue with llm-agent label to Awaiting Quality Check without PR check', async () => {
|
|
334
|
+
const stuckIssue = createMockIssue({
|
|
335
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
336
|
+
status: 'Preparation',
|
|
337
|
+
labels: ['llm-agent'],
|
|
338
|
+
});
|
|
339
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
340
|
+
issues: [stuckIssue],
|
|
341
|
+
cacheUsed: false,
|
|
342
|
+
});
|
|
343
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
344
|
+
stdout: '',
|
|
345
|
+
stderr: '',
|
|
346
|
+
exitCode: 1,
|
|
347
|
+
});
|
|
348
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
349
|
+
{
|
|
350
|
+
author: 'bot',
|
|
351
|
+
content: 'From: agent report',
|
|
352
|
+
createdAt: new Date(),
|
|
353
|
+
},
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
await useCase.run({
|
|
357
|
+
projectUrl: 'https://github.com/user/repo',
|
|
358
|
+
preparationStatus: 'Preparation',
|
|
359
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
360
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
361
|
+
allowIssueCacheMinutes: 60,
|
|
362
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
expect(mockIssueRepository.findRelatedOpenPRs.mock.calls).toHaveLength(0);
|
|
366
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
367
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('4');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should revert orphaned issue to Awaiting Workspace when report has nextStep set', async () => {
|
|
371
|
+
const stuckIssue = createMockIssue({
|
|
372
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
373
|
+
status: 'Preparation',
|
|
374
|
+
});
|
|
375
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
376
|
+
issues: [stuckIssue],
|
|
377
|
+
cacheUsed: false,
|
|
378
|
+
});
|
|
379
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
380
|
+
stdout: '',
|
|
381
|
+
stderr: '',
|
|
382
|
+
exitCode: 1,
|
|
383
|
+
});
|
|
384
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
385
|
+
{
|
|
386
|
+
author: 'bot',
|
|
387
|
+
content:
|
|
388
|
+
'From: agent report\n```json\n{"nextStep": "do something"}\n```',
|
|
389
|
+
createdAt: new Date(),
|
|
390
|
+
},
|
|
391
|
+
]);
|
|
392
|
+
|
|
393
|
+
await useCase.run({
|
|
394
|
+
projectUrl: 'https://github.com/user/repo',
|
|
395
|
+
preparationStatus: 'Preparation',
|
|
396
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
397
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
398
|
+
allowIssueCacheMinutes: 60,
|
|
399
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
403
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('should never post a comment regardless of orphan outcome', async () => {
|
|
407
|
+
const stuckIssue = createMockIssue({
|
|
408
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
409
|
+
status: 'Preparation',
|
|
410
|
+
});
|
|
411
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
412
|
+
issues: [stuckIssue],
|
|
413
|
+
cacheUsed: false,
|
|
414
|
+
});
|
|
415
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
416
|
+
stdout: '',
|
|
417
|
+
stderr: '',
|
|
418
|
+
exitCode: 1,
|
|
419
|
+
});
|
|
420
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
421
|
+
|
|
422
|
+
await useCase.run({
|
|
423
|
+
projectUrl: 'https://github.com/user/repo',
|
|
424
|
+
preparationStatus: 'Preparation',
|
|
425
|
+
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
426
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
427
|
+
allowIssueCacheMinutes: 60,
|
|
428
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
432
|
+
});
|
|
433
|
+
|
|
149
434
|
it('should leave in-flight Preparation issue untouched when check command exits zero', async () => {
|
|
150
435
|
const inFlightIssue = createMockIssue({
|
|
151
436
|
url: 'https://github.com/user/repo/issues/20',
|
|
@@ -165,12 +450,15 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
165
450
|
projectUrl: 'https://github.com/user/repo',
|
|
166
451
|
preparationStatus: 'Preparation',
|
|
167
452
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
453
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
168
454
|
allowIssueCacheMinutes: 60,
|
|
169
455
|
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
170
456
|
});
|
|
171
457
|
|
|
172
458
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
173
|
-
expect(
|
|
459
|
+
expect(
|
|
460
|
+
mockIssueCommentRepository.getCommentsFromIssue.mock.calls,
|
|
461
|
+
).toHaveLength(0);
|
|
174
462
|
});
|
|
175
463
|
|
|
176
464
|
it('should only process issues in Preparation status and skip others', async () => {
|
|
@@ -191,11 +479,13 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
191
479
|
stderr: '',
|
|
192
480
|
exitCode: 1,
|
|
193
481
|
});
|
|
482
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
194
483
|
|
|
195
484
|
await useCase.run({
|
|
196
485
|
projectUrl: 'https://github.com/user/repo',
|
|
197
486
|
preparationStatus: 'Preparation',
|
|
198
487
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
488
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
199
489
|
allowIssueCacheMinutes: 60,
|
|
200
490
|
preparationProcessCheckCommand: 'check {URL}',
|
|
201
491
|
});
|
|
@@ -231,18 +521,19 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
231
521
|
stderr: '',
|
|
232
522
|
exitCode: 0,
|
|
233
523
|
});
|
|
524
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
234
525
|
|
|
235
526
|
await useCase.run({
|
|
236
527
|
projectUrl: 'https://github.com/user/repo',
|
|
237
528
|
preparationStatus: 'Preparation',
|
|
238
529
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
530
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
239
531
|
allowIssueCacheMinutes: 60,
|
|
240
532
|
preparationProcessCheckCommand: 'check {URL}',
|
|
241
533
|
});
|
|
242
534
|
|
|
243
535
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
244
536
|
expect(mockIssueRepository.updateStatus.mock.calls[0][1]).toBe(stuckIssue);
|
|
245
|
-
expect(mockIssueRepository.createComment.mock.calls).toHaveLength(1);
|
|
246
537
|
});
|
|
247
538
|
|
|
248
539
|
it('should throw when project is not found by URL', async () => {
|
|
@@ -253,6 +544,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
253
544
|
projectUrl: 'https://github.com/user/repo',
|
|
254
545
|
preparationStatus: 'Preparation',
|
|
255
546
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
547
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
256
548
|
allowIssueCacheMinutes: 0,
|
|
257
549
|
preparationProcessCheckCommand: 'check {URL}',
|
|
258
550
|
}),
|
|
@@ -268,6 +560,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
268
560
|
projectUrl: 'https://github.com/user/repo',
|
|
269
561
|
preparationStatus: 'Preparation',
|
|
270
562
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
563
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
271
564
|
allowIssueCacheMinutes: 0,
|
|
272
565
|
preparationProcessCheckCommand: 'check {URL}',
|
|
273
566
|
}),
|
|
@@ -293,12 +586,12 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
293
586
|
projectUrl: 'https://github.com/user/repo',
|
|
294
587
|
preparationStatus: 'Preparation',
|
|
295
588
|
awaitingWorkspaceStatus: 'NonExistentStatus',
|
|
589
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
296
590
|
allowIssueCacheMinutes: 0,
|
|
297
591
|
preparationProcessCheckCommand: 'check {URL}',
|
|
298
592
|
});
|
|
299
593
|
|
|
300
594
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
301
|
-
expect(mockIssueRepository.createComment.mock.calls).toHaveLength(0);
|
|
302
595
|
});
|
|
303
596
|
|
|
304
597
|
it('should do nothing when there are no Preparation issues', async () => {
|
|
@@ -314,6 +607,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
314
607
|
projectUrl: 'https://github.com/user/repo',
|
|
315
608
|
preparationStatus: 'Preparation',
|
|
316
609
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
610
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
317
611
|
allowIssueCacheMinutes: 60,
|
|
318
612
|
preparationProcessCheckCommand: 'check {URL}',
|
|
319
613
|
});
|
|
@@ -346,11 +640,13 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
346
640
|
exitCode: 0,
|
|
347
641
|
})
|
|
348
642
|
.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 });
|
|
643
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
349
644
|
|
|
350
645
|
await useCase.run({
|
|
351
646
|
projectUrl: 'https://github.com/user/repo',
|
|
352
647
|
preparationStatus: 'Preparation',
|
|
353
648
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
649
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
354
650
|
allowIssueCacheMinutes: 60,
|
|
355
651
|
preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
|
|
356
652
|
awLogDirectoryPath: '/home/user/logs-aw',
|
|
@@ -358,7 +654,6 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
358
654
|
});
|
|
359
655
|
|
|
360
656
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
361
|
-
expect(mockIssueRepository.createComment.mock.calls).toHaveLength(1);
|
|
362
657
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(3);
|
|
363
658
|
expect(mockLocalCommandRunner.runCommand.mock.calls[1]).toEqual([
|
|
364
659
|
'sh',
|
|
@@ -416,6 +711,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
416
711
|
projectUrl: 'https://github.com/user/repo',
|
|
417
712
|
preparationStatus: 'Preparation',
|
|
418
713
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
714
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
419
715
|
allowIssueCacheMinutes: 60,
|
|
420
716
|
preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
|
|
421
717
|
awLogDirectoryPath: '/home/user/logs-aw',
|
|
@@ -423,7 +719,9 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
423
719
|
});
|
|
424
720
|
|
|
425
721
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
426
|
-
expect(
|
|
722
|
+
expect(
|
|
723
|
+
mockIssueCommentRepository.getCommentsFromIssue.mock.calls,
|
|
724
|
+
).toHaveLength(0);
|
|
427
725
|
});
|
|
428
726
|
|
|
429
727
|
it('should leave issue untouched when pgrep exits zero and no aw log files exist yet', async () => {
|
|
@@ -450,6 +748,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
450
748
|
projectUrl: 'https://github.com/user/repo',
|
|
451
749
|
preparationStatus: 'Preparation',
|
|
452
750
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
751
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
453
752
|
allowIssueCacheMinutes: 60,
|
|
454
753
|
preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
|
|
455
754
|
awLogDirectoryPath: '/home/user/logs-aw',
|
|
@@ -457,7 +756,9 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
457
756
|
});
|
|
458
757
|
|
|
459
758
|
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
460
|
-
expect(
|
|
759
|
+
expect(
|
|
760
|
+
mockIssueCommentRepository.getCommentsFromIssue.mock.calls,
|
|
761
|
+
).toHaveLength(0);
|
|
461
762
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
462
763
|
});
|
|
463
764
|
|
|
@@ -480,6 +781,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
480
781
|
projectUrl: 'https://github.com/user/repo',
|
|
481
782
|
preparationStatus: 'Preparation',
|
|
482
783
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
784
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
483
785
|
allowIssueCacheMinutes: 60,
|
|
484
786
|
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
485
787
|
});
|
|
@@ -505,11 +807,13 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
505
807
|
stderr: '',
|
|
506
808
|
exitCode: 1,
|
|
507
809
|
});
|
|
810
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
|
|
508
811
|
|
|
509
812
|
await useCase.run({
|
|
510
813
|
projectUrl: 'https://github.com/user/repo',
|
|
511
814
|
preparationStatus: 'Preparation',
|
|
512
815
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
816
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
513
817
|
allowIssueCacheMinutes: 60,
|
|
514
818
|
preparationProcessCheckCommand: 'pgrep -fa "Please handover {URL}"',
|
|
515
819
|
awLogDirectoryPath: '/home/user/logs-aw',
|
|
@@ -539,6 +843,7 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
539
843
|
projectUrl: 'https://github.com/user/repo',
|
|
540
844
|
preparationStatus: 'Preparation',
|
|
541
845
|
awaitingWorkspaceStatus: 'Awaiting Workspace',
|
|
846
|
+
awaitingQualityCheckStatus: 'Awaiting Quality Check',
|
|
542
847
|
allowIssueCacheMinutes: 0,
|
|
543
848
|
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
544
849
|
});
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IssueRepository,
|
|
3
|
+
RelatedPullRequest,
|
|
4
|
+
} from './adapter-interfaces/IssueRepository';
|
|
5
|
+
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
2
6
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
3
7
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
4
8
|
import { Issue } from '../entities/Issue';
|
|
@@ -11,7 +15,14 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
11
15
|
>,
|
|
12
16
|
readonly issueRepository: Pick<
|
|
13
17
|
IssueRepository,
|
|
14
|
-
|
|
18
|
+
| 'getAllIssues'
|
|
19
|
+
| 'updateStatus'
|
|
20
|
+
| 'findRelatedOpenPRs'
|
|
21
|
+
| 'getOpenPullRequest'
|
|
22
|
+
>,
|
|
23
|
+
readonly issueCommentRepository: Pick<
|
|
24
|
+
IssueCommentRepository,
|
|
25
|
+
'getCommentsFromIssue'
|
|
15
26
|
>,
|
|
16
27
|
readonly localCommandRunner: LocalCommandRunner,
|
|
17
28
|
) {}
|
|
@@ -20,6 +31,7 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
20
31
|
projectUrl: string;
|
|
21
32
|
preparationStatus: string;
|
|
22
33
|
awaitingWorkspaceStatus: string;
|
|
34
|
+
awaitingQualityCheckStatus?: string;
|
|
23
35
|
allowIssueCacheMinutes: number;
|
|
24
36
|
preparationProcessCheckCommand: string;
|
|
25
37
|
awLogDirectoryPath?: string;
|
|
@@ -53,22 +65,106 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
53
65
|
return;
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
const awaitingQualityCheckStatusOption = params.awaitingQualityCheckStatus
|
|
69
|
+
? project.status.statuses.find(
|
|
70
|
+
(s) => s.name === params.awaitingQualityCheckStatus,
|
|
71
|
+
)
|
|
72
|
+
: null;
|
|
73
|
+
|
|
56
74
|
for (const issue of preparationIssues) {
|
|
57
75
|
const isOrphaned = await this.isOrphanedIssue(issue, params);
|
|
58
76
|
if (isOrphaned) {
|
|
59
|
-
await this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
const hasRejections = await this.evaluateHasRejections(issue);
|
|
78
|
+
if (!hasRejections && awaitingQualityCheckStatusOption) {
|
|
79
|
+
await this.issueRepository.updateStatus(
|
|
80
|
+
project,
|
|
81
|
+
issue,
|
|
82
|
+
awaitingQualityCheckStatusOption.id,
|
|
83
|
+
);
|
|
84
|
+
} else {
|
|
85
|
+
await this.issueRepository.updateStatus(
|
|
86
|
+
project,
|
|
87
|
+
issue,
|
|
88
|
+
awaitingWorkspaceStatusOption.id,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
68
91
|
}
|
|
69
92
|
}
|
|
70
93
|
};
|
|
71
94
|
|
|
95
|
+
private evaluateHasRejections = async (issue: Issue): Promise<boolean> => {
|
|
96
|
+
const comments =
|
|
97
|
+
await this.issueCommentRepository.getCommentsFromIssue(issue);
|
|
98
|
+
const lastComment = comments[comments.length - 1];
|
|
99
|
+
if (!lastComment || !lastComment.content.startsWith('From:')) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
if (this.reportBodyHasNextStep(lastComment.content)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const categoryLabels = issue.labels.filter((label) =>
|
|
107
|
+
label.startsWith('category:'),
|
|
108
|
+
);
|
|
109
|
+
const hasLlmAgentLabel = issue.labels.some(
|
|
110
|
+
(l) => l === 'llm-agent' || l.startsWith('llm-agent:'),
|
|
111
|
+
);
|
|
112
|
+
if (
|
|
113
|
+
hasLlmAgentLabel ||
|
|
114
|
+
(categoryLabels.length > 0 && !categoryLabels.includes('category:e2e'))
|
|
115
|
+
) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const prsToCheck = issue.isPr
|
|
120
|
+
? await this.resolveOpenPrsForPrItem(issue.url)
|
|
121
|
+
: await this.issueRepository.findRelatedOpenPRs(issue.url);
|
|
122
|
+
|
|
123
|
+
if (prsToCheck.length !== 1) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const pr = prsToCheck[0];
|
|
128
|
+
return (
|
|
129
|
+
pr.isConflicted || !pr.isPassedAllCiJob || !pr.isResolvedAllReviewComments
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
private resolveOpenPrsForPrItem = async (
|
|
134
|
+
prUrl: string,
|
|
135
|
+
): Promise<RelatedPullRequest[]> => {
|
|
136
|
+
const pr = await this.issueRepository.getOpenPullRequest(prUrl);
|
|
137
|
+
if (pr === null) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
return [pr];
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
private reportBodyHasNextStep = (body: string): boolean => {
|
|
144
|
+
const reportMatch = body.match(/```json\n([\s\S]*?)\n```/);
|
|
145
|
+
if (!reportMatch || reportMatch.length < 2) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
let reportJson: unknown;
|
|
149
|
+
try {
|
|
150
|
+
reportJson = JSON.parse(reportMatch[1]);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(
|
|
153
|
+
'Invalid JSON in report body while checking nextStep:',
|
|
154
|
+
error,
|
|
155
|
+
);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (typeof reportJson !== 'object' || reportJson === null) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!('nextStep' in reportJson)) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const nextStepValue = Reflect.get(reportJson, 'nextStep');
|
|
165
|
+
return nextStepValue !== null && nextStepValue !== undefined;
|
|
166
|
+
};
|
|
167
|
+
|
|
72
168
|
private isOrphanedIssue = async (
|
|
73
169
|
issue: Issue,
|
|
74
170
|
params: {
|