github-issue-tower-defence-management 2.10.1 → 2.11.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 +14 -0
- package/README.md +9 -1
- package/bin/adapter/entry-points/cli/fleetConfig.js +7 -1
- package/bin/adapter/entry-points/cli/fleetConfig.js.map +1 -1
- package/bin/adapter/entry-points/cli/index.js +11 -3
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/CliGitHubGraphqlRateLimitRepository.js +24 -0
- package/bin/adapter/repositories/CliGitHubGraphqlRateLimitRepository.js.map +1 -0
- package/bin/adapter/repositories/KySlackRepository.js +2 -1
- package/bin/adapter/repositories/KySlackRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +18 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/ConflictedIssueRevertUseCase.js +6 -3
- package/bin/domain/usecases/ConflictedIssueRevertUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +22 -1
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/GitHubGraphqlRateLimitRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/GitHubGraphqlRateLimitRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/fleetConfig.test.ts +99 -0
- package/src/adapter/entry-points/cli/fleetConfig.ts +25 -0
- package/src/adapter/entry-points/cli/index.test.ts +4 -0
- package/src/adapter/entry-points/cli/index.ts +16 -2
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/CliGitHubGraphqlRateLimitRepository.test.ts +71 -0
- package/src/adapter/repositories/CliGitHubGraphqlRateLimitRepository.ts +21 -0
- package/src/adapter/repositories/KySlackRepository.test.ts +49 -1
- package/src/adapter/repositories/KySlackRepository.ts +3 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +24 -0
- package/src/domain/usecases/ConflictedIssueRevertUseCase.test.ts +262 -5
- package/src/domain/usecases/ConflictedIssueRevertUseCase.ts +12 -4
- package/src/domain/usecases/StartPreparationUseCase.test.ts +229 -0
- package/src/domain/usecases/StartPreparationUseCase.ts +35 -0
- package/src/domain/usecases/adapter-interfaces/GitHubGraphqlRateLimitRepository.ts +3 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
- package/types/adapter/entry-points/cli/fleetConfig.d.ts +4 -0
- package/types/adapter/entry-points/cli/fleetConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/CliGitHubGraphqlRateLimitRepository.d.ts +8 -0
- package/types/adapter/repositories/CliGitHubGraphqlRateLimitRepository.d.ts.map +1 -0
- package/types/adapter/repositories/KySlackRepository.d.ts +1 -1
- package/types/adapter/repositories/KySlackRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/ConflictedIssueRevertUseCase.d.ts +1 -1
- package/types/domain/usecases/ConflictedIssueRevertUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts +5 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/GitHubGraphqlRateLimitRepository.d.ts +4 -0
- package/types/domain/usecases/adapter-interfaces/GitHubGraphqlRateLimitRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -97,6 +97,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
97
97
|
getAllIssues: jest.Mock;
|
|
98
98
|
getOpenPullRequests: jest.Mock;
|
|
99
99
|
updateStatus: jest.Mock;
|
|
100
|
+
updateBranch: jest.Mock;
|
|
100
101
|
};
|
|
101
102
|
let mockIssueCommentRepository: {
|
|
102
103
|
createComment: jest.Mock;
|
|
@@ -122,6 +123,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
122
123
|
}),
|
|
123
124
|
getOpenPullRequests: jest.fn().mockResolvedValue(new Map()),
|
|
124
125
|
updateStatus: jest.fn().mockResolvedValue(undefined),
|
|
126
|
+
updateBranch: jest.fn().mockResolvedValue(false),
|
|
125
127
|
};
|
|
126
128
|
|
|
127
129
|
mockIssueCommentRepository = {
|
|
@@ -144,7 +146,6 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
144
146
|
});
|
|
145
147
|
|
|
146
148
|
const excludedStatuses = [
|
|
147
|
-
'Awaiting Workspace',
|
|
148
149
|
'Done',
|
|
149
150
|
'Icebox',
|
|
150
151
|
'Failed Preparation',
|
|
@@ -170,6 +171,73 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
170
171
|
});
|
|
171
172
|
});
|
|
172
173
|
|
|
174
|
+
it('should process Awaiting Workspace issues when their linked PR is conflicted and update-branch fails', async () => {
|
|
175
|
+
const issue = createMockIssue({ status: 'Awaiting Workspace' });
|
|
176
|
+
const prItem = createMockPrItem({
|
|
177
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
178
|
+
closingIssueReferenceUrls: [issue.url],
|
|
179
|
+
});
|
|
180
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
181
|
+
project: mockProject,
|
|
182
|
+
issues: [issue, prItem],
|
|
183
|
+
cacheUsed: false,
|
|
184
|
+
});
|
|
185
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
186
|
+
url: prItem.url,
|
|
187
|
+
isConflicted: true,
|
|
188
|
+
mergeable: 'CONFLICTING',
|
|
189
|
+
});
|
|
190
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
191
|
+
new Map([[conflictedPr.url, conflictedPr]]),
|
|
192
|
+
);
|
|
193
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
194
|
+
|
|
195
|
+
await useCase.run({ projectUrl });
|
|
196
|
+
|
|
197
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
198
|
+
conflictedPr.url,
|
|
199
|
+
);
|
|
200
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
201
|
+
mockProject,
|
|
202
|
+
issue,
|
|
203
|
+
'awaiting-workspace-id',
|
|
204
|
+
);
|
|
205
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
206
|
+
issue,
|
|
207
|
+
'conflict',
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should not update status or post comment when Awaiting Workspace PR update-branch succeeds', async () => {
|
|
212
|
+
const issue = createMockIssue({ status: 'Awaiting Workspace' });
|
|
213
|
+
const prItem = createMockPrItem({
|
|
214
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
215
|
+
closingIssueReferenceUrls: [issue.url],
|
|
216
|
+
});
|
|
217
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
218
|
+
project: mockProject,
|
|
219
|
+
issues: [issue, prItem],
|
|
220
|
+
cacheUsed: false,
|
|
221
|
+
});
|
|
222
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
223
|
+
url: prItem.url,
|
|
224
|
+
isConflicted: true,
|
|
225
|
+
mergeable: 'CONFLICTING',
|
|
226
|
+
});
|
|
227
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
228
|
+
new Map([[conflictedPr.url, conflictedPr]]),
|
|
229
|
+
);
|
|
230
|
+
mockIssueRepository.updateBranch.mockResolvedValue(true);
|
|
231
|
+
|
|
232
|
+
await useCase.run({ projectUrl });
|
|
233
|
+
|
|
234
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
235
|
+
conflictedPr.url,
|
|
236
|
+
);
|
|
237
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
238
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
239
|
+
});
|
|
240
|
+
|
|
173
241
|
it('should skip PR items when scanning for target task issues', async () => {
|
|
174
242
|
const prIssue = createMockIssue({
|
|
175
243
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -195,6 +263,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
195
263
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
196
264
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
197
265
|
);
|
|
266
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
198
267
|
|
|
199
268
|
await useCase.run({ projectUrl });
|
|
200
269
|
|
|
@@ -255,7 +324,85 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
255
324
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
256
325
|
});
|
|
257
326
|
|
|
258
|
-
it('should update status and post conflict comment when linked PR is conflicted', async () => {
|
|
327
|
+
it('should update status and post conflict comment when linked PR is conflicted and update-branch fails', async () => {
|
|
328
|
+
const issue = createMockIssue({
|
|
329
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
330
|
+
status: 'Preparation',
|
|
331
|
+
});
|
|
332
|
+
const prItem = createMockPrItem({
|
|
333
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
334
|
+
closingIssueReferenceUrls: [issue.url],
|
|
335
|
+
});
|
|
336
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
337
|
+
project: mockProject,
|
|
338
|
+
issues: [issue, prItem],
|
|
339
|
+
cacheUsed: false,
|
|
340
|
+
});
|
|
341
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
342
|
+
url: prItem.url,
|
|
343
|
+
isConflicted: true,
|
|
344
|
+
mergeable: 'CONFLICTING',
|
|
345
|
+
});
|
|
346
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
347
|
+
new Map([[conflictedPr.url, conflictedPr]]),
|
|
348
|
+
);
|
|
349
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
350
|
+
|
|
351
|
+
await useCase.run({ projectUrl });
|
|
352
|
+
|
|
353
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
354
|
+
conflictedPr.url,
|
|
355
|
+
);
|
|
356
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
357
|
+
mockProject,
|
|
358
|
+
issue,
|
|
359
|
+
'awaiting-workspace-id',
|
|
360
|
+
);
|
|
361
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
362
|
+
issue,
|
|
363
|
+
'conflict',
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should propagate errors thrown by updateBranch', async () => {
|
|
368
|
+
const issue = createMockIssue({
|
|
369
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
370
|
+
status: 'Preparation',
|
|
371
|
+
});
|
|
372
|
+
const prItem = createMockPrItem({
|
|
373
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
374
|
+
closingIssueReferenceUrls: [issue.url],
|
|
375
|
+
});
|
|
376
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
377
|
+
project: mockProject,
|
|
378
|
+
issues: [issue, prItem],
|
|
379
|
+
cacheUsed: false,
|
|
380
|
+
});
|
|
381
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
382
|
+
url: prItem.url,
|
|
383
|
+
isConflicted: true,
|
|
384
|
+
mergeable: 'CONFLICTING',
|
|
385
|
+
});
|
|
386
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
387
|
+
new Map([[conflictedPr.url, conflictedPr]]),
|
|
388
|
+
);
|
|
389
|
+
mockIssueRepository.updateBranch.mockRejectedValue(
|
|
390
|
+
new Error(
|
|
391
|
+
'Failed to update branch for PR https://github.com/user/repo/pull/1: 500 Internal Server Error',
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await expect(useCase.run({ projectUrl })).rejects.toThrow(
|
|
396
|
+
'Failed to update branch for PR',
|
|
397
|
+
);
|
|
398
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
399
|
+
conflictedPr.url,
|
|
400
|
+
);
|
|
401
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
402
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should not update status or post comment when update-branch succeeds for the conflicted PR', async () => {
|
|
259
406
|
const issue = createMockIssue({
|
|
260
407
|
url: 'https://github.com/user/repo/issues/1',
|
|
261
408
|
status: 'Preparation',
|
|
@@ -277,6 +424,105 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
277
424
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
278
425
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
279
426
|
);
|
|
427
|
+
mockIssueRepository.updateBranch.mockResolvedValue(true);
|
|
428
|
+
|
|
429
|
+
await useCase.run({ projectUrl });
|
|
430
|
+
|
|
431
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
432
|
+
conflictedPr.url,
|
|
433
|
+
);
|
|
434
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
435
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it('should call update-branch only for conflicted PRs, not for all linked PRs', async () => {
|
|
439
|
+
const issue = createMockIssue({
|
|
440
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
441
|
+
status: 'Preparation',
|
|
442
|
+
});
|
|
443
|
+
const prItem1 = createMockPrItem({
|
|
444
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
445
|
+
number: 1,
|
|
446
|
+
closingIssueReferenceUrls: [issue.url],
|
|
447
|
+
});
|
|
448
|
+
const prItem2 = createMockPrItem({
|
|
449
|
+
url: 'https://github.com/user/repo/pull/2',
|
|
450
|
+
number: 2,
|
|
451
|
+
closingIssueReferenceUrls: [issue.url],
|
|
452
|
+
});
|
|
453
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
454
|
+
project: mockProject,
|
|
455
|
+
issues: [issue, prItem1, prItem2],
|
|
456
|
+
cacheUsed: false,
|
|
457
|
+
});
|
|
458
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
459
|
+
url: prItem1.url,
|
|
460
|
+
isConflicted: true,
|
|
461
|
+
mergeable: 'CONFLICTING',
|
|
462
|
+
});
|
|
463
|
+
const mergeablePr = createMockRelatedPullRequest({
|
|
464
|
+
url: prItem2.url,
|
|
465
|
+
isConflicted: false,
|
|
466
|
+
mergeable: 'MERGEABLE',
|
|
467
|
+
});
|
|
468
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
469
|
+
new Map([
|
|
470
|
+
[conflictedPr.url, conflictedPr],
|
|
471
|
+
[mergeablePr.url, mergeablePr],
|
|
472
|
+
]),
|
|
473
|
+
);
|
|
474
|
+
mockIssueRepository.updateBranch.mockResolvedValue(true);
|
|
475
|
+
|
|
476
|
+
await useCase.run({ projectUrl });
|
|
477
|
+
|
|
478
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledTimes(1);
|
|
479
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
480
|
+
conflictedPr.url,
|
|
481
|
+
);
|
|
482
|
+
expect(mockIssueRepository.updateBranch).not.toHaveBeenCalledWith(
|
|
483
|
+
mergeablePr.url,
|
|
484
|
+
);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should fall back to status update when any conflicted PR update-branch fails', async () => {
|
|
488
|
+
const issue = createMockIssue({
|
|
489
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
490
|
+
status: 'Preparation',
|
|
491
|
+
});
|
|
492
|
+
const prItem1 = createMockPrItem({
|
|
493
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
494
|
+
number: 1,
|
|
495
|
+
closingIssueReferenceUrls: [issue.url],
|
|
496
|
+
});
|
|
497
|
+
const prItem2 = createMockPrItem({
|
|
498
|
+
url: 'https://github.com/user/repo/pull/2',
|
|
499
|
+
number: 2,
|
|
500
|
+
closingIssueReferenceUrls: [issue.url],
|
|
501
|
+
});
|
|
502
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
503
|
+
project: mockProject,
|
|
504
|
+
issues: [issue, prItem1, prItem2],
|
|
505
|
+
cacheUsed: false,
|
|
506
|
+
});
|
|
507
|
+
const conflictedPr1 = createMockRelatedPullRequest({
|
|
508
|
+
url: prItem1.url,
|
|
509
|
+
isConflicted: true,
|
|
510
|
+
mergeable: 'CONFLICTING',
|
|
511
|
+
});
|
|
512
|
+
const conflictedPr2 = createMockRelatedPullRequest({
|
|
513
|
+
url: prItem2.url,
|
|
514
|
+
isConflicted: true,
|
|
515
|
+
mergeable: 'CONFLICTING',
|
|
516
|
+
});
|
|
517
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
518
|
+
new Map([
|
|
519
|
+
[conflictedPr1.url, conflictedPr1],
|
|
520
|
+
[conflictedPr2.url, conflictedPr2],
|
|
521
|
+
]),
|
|
522
|
+
);
|
|
523
|
+
mockIssueRepository.updateBranch
|
|
524
|
+
.mockResolvedValueOnce(true)
|
|
525
|
+
.mockResolvedValueOnce(false);
|
|
280
526
|
|
|
281
527
|
await useCase.run({ projectUrl });
|
|
282
528
|
|
|
@@ -313,6 +559,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
313
559
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
314
560
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
315
561
|
);
|
|
562
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
316
563
|
|
|
317
564
|
await useCase.run({ projectUrl });
|
|
318
565
|
|
|
@@ -430,6 +677,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
430
677
|
[mergeablePr.url, mergeablePr],
|
|
431
678
|
]),
|
|
432
679
|
);
|
|
680
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
433
681
|
|
|
434
682
|
await useCase.run({ projectUrl });
|
|
435
683
|
|
|
@@ -491,7 +739,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
491
739
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
492
740
|
});
|
|
493
741
|
|
|
494
|
-
it('should
|
|
742
|
+
it('should call update-branch before status update and comment when update-branch fails', async () => {
|
|
495
743
|
const callOrder: string[] = [];
|
|
496
744
|
const issue = createMockIssue({
|
|
497
745
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -514,6 +762,10 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
514
762
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
515
763
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
516
764
|
);
|
|
765
|
+
mockIssueRepository.updateBranch.mockImplementation(() => {
|
|
766
|
+
callOrder.push('updateBranch');
|
|
767
|
+
return Promise.resolve(false);
|
|
768
|
+
});
|
|
517
769
|
mockIssueRepository.updateStatus.mockImplementation(() => {
|
|
518
770
|
callOrder.push('updateStatus');
|
|
519
771
|
return Promise.resolve();
|
|
@@ -525,7 +777,11 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
525
777
|
|
|
526
778
|
await useCase.run({ projectUrl });
|
|
527
779
|
|
|
528
|
-
expect(callOrder).toEqual([
|
|
780
|
+
expect(callOrder).toEqual([
|
|
781
|
+
'updateBranch',
|
|
782
|
+
'updateStatus',
|
|
783
|
+
'createComment',
|
|
784
|
+
]);
|
|
529
785
|
});
|
|
530
786
|
|
|
531
787
|
it('should fetch all PR URLs in a single batched call', async () => {
|
|
@@ -567,7 +823,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
567
823
|
);
|
|
568
824
|
});
|
|
569
825
|
|
|
570
|
-
it('should process multiple conflicted issues in one run', async () => {
|
|
826
|
+
it('should process multiple conflicted issues in one run when update-branch fails for both', async () => {
|
|
571
827
|
const issue1 = createMockIssue({
|
|
572
828
|
url: 'https://github.com/user/repo/issues/1',
|
|
573
829
|
number: 1,
|
|
@@ -609,6 +865,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
609
865
|
[conflictedPr2.url, conflictedPr2],
|
|
610
866
|
]),
|
|
611
867
|
);
|
|
868
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
612
869
|
|
|
613
870
|
await useCase.run({ projectUrl });
|
|
614
871
|
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from '../entities/WorkflowStatus';
|
|
12
12
|
|
|
13
13
|
const EXCLUDED_STATUSES = new Set([
|
|
14
|
-
AWAITING_WORKSPACE_STATUS_NAME,
|
|
15
14
|
DONE_STATUS_NAME,
|
|
16
15
|
ICEBOX_STATUS_NAME,
|
|
17
16
|
FAILED_PREPARATION_STATUS_NAME,
|
|
@@ -26,7 +25,7 @@ export class ConflictedIssueRevertUseCase {
|
|
|
26
25
|
>,
|
|
27
26
|
private readonly issueRepository: Pick<
|
|
28
27
|
IssueRepository,
|
|
29
|
-
'getAllIssues' | 'getOpenPullRequests' | 'updateStatus'
|
|
28
|
+
'getAllIssues' | 'getOpenPullRequests' | 'updateStatus' | 'updateBranch'
|
|
30
29
|
>,
|
|
31
30
|
private readonly issueCommentRepository: Pick<
|
|
32
31
|
IssueCommentRepository,
|
|
@@ -98,8 +97,17 @@ export class ConflictedIssueRevertUseCase {
|
|
|
98
97
|
continue;
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
const
|
|
102
|
-
if (
|
|
100
|
+
const conflictedPrs = relatedPrs.filter((pr) => pr.isConflicted);
|
|
101
|
+
if (conflictedPrs.length === 0) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const allBranchesUpdated = (
|
|
106
|
+
await Promise.all(
|
|
107
|
+
conflictedPrs.map((pr) => this.issueRepository.updateBranch(pr.url)),
|
|
108
|
+
)
|
|
109
|
+
).every(Boolean);
|
|
110
|
+
if (allBranchesUpdated) {
|
|
103
111
|
continue;
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -10,6 +10,7 @@ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
|
10
10
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
11
11
|
import { ClaudeTokenUsageRepository } from './adapter-interfaces/ClaudeTokenUsageRepository';
|
|
12
12
|
import { TakeOwnershipSpawnRepository } from './adapter-interfaces/TakeOwnershipSpawnRepository';
|
|
13
|
+
import { GitHubGraphqlRateLimitRepository } from './adapter-interfaces/GitHubGraphqlRateLimitRepository';
|
|
13
14
|
import { ClaudeTokenUsage } from '../entities/ClaudeTokenUsage';
|
|
14
15
|
import { Issue } from '../entities/Issue';
|
|
15
16
|
import { FieldOption, Project } from '../entities/Project';
|
|
@@ -109,6 +110,7 @@ describe('StartPreparationUseCase', () => {
|
|
|
109
110
|
let mockLocalCommandRunner: Mocked<LocalCommandRunner>;
|
|
110
111
|
let mockClaudeTokenUsageRepository: Mocked<ClaudeTokenUsageRepository>;
|
|
111
112
|
let mockTakeOwnershipSpawnRepository: Mocked<TakeOwnershipSpawnRepository>;
|
|
113
|
+
let mockGitHubGraphqlRateLimitRepository: Mocked<GitHubGraphqlRateLimitRepository>;
|
|
112
114
|
let mockProject: Project;
|
|
113
115
|
beforeEach(() => {
|
|
114
116
|
jest.resetAllMocks();
|
|
@@ -144,12 +146,16 @@ describe('StartPreparationUseCase', () => {
|
|
|
144
146
|
listSpawns: jest.fn().mockReturnValue([]),
|
|
145
147
|
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
146
148
|
};
|
|
149
|
+
mockGitHubGraphqlRateLimitRepository = {
|
|
150
|
+
getRemainingRequestCount: jest.fn().mockResolvedValue(null),
|
|
151
|
+
};
|
|
147
152
|
useCase = new StartPreparationUseCase(
|
|
148
153
|
mockProjectRepository,
|
|
149
154
|
mockIssueRepository,
|
|
150
155
|
mockLocalCommandRunner,
|
|
151
156
|
mockClaudeTokenUsageRepository,
|
|
152
157
|
mockTakeOwnershipSpawnRepository,
|
|
158
|
+
mockGitHubGraphqlRateLimitRepository,
|
|
153
159
|
);
|
|
154
160
|
});
|
|
155
161
|
it('should run aw command for awaiting workspace issues', async () => {
|
|
@@ -627,6 +633,225 @@ describe('StartPreparationUseCase', () => {
|
|
|
627
633
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
628
634
|
});
|
|
629
635
|
|
|
636
|
+
it('skips all spawning when active worker count meets the maxConcurrentWorkers cap', async () => {
|
|
637
|
+
const awaitingIssues: Issue[] = [
|
|
638
|
+
createMockIssue({
|
|
639
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
640
|
+
status: 'Awaiting Workspace',
|
|
641
|
+
author: 'testuser',
|
|
642
|
+
}),
|
|
643
|
+
createMockIssue({
|
|
644
|
+
number: 2,
|
|
645
|
+
url: 'https://github.com/user/repo/issues/11',
|
|
646
|
+
status: 'Awaiting Workspace',
|
|
647
|
+
author: 'testuser',
|
|
648
|
+
}),
|
|
649
|
+
];
|
|
650
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
651
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
652
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
653
|
+
);
|
|
654
|
+
mockTakeOwnershipSpawnRepository.listRunningIssueUrls.mockReturnValue([
|
|
655
|
+
'https://github.com/user/repo/issues/1',
|
|
656
|
+
'https://github.com/user/repo/issues/2',
|
|
657
|
+
]);
|
|
658
|
+
|
|
659
|
+
await useCase.run({
|
|
660
|
+
projectUrl: 'https://github.com/user/repo',
|
|
661
|
+
defaultAgentName: 'agent1',
|
|
662
|
+
defaultLlmModelName: 'claude-opus',
|
|
663
|
+
fallbackLlmModelName: null,
|
|
664
|
+
defaultLlmAgentName: null,
|
|
665
|
+
configFilePath: '/path/to/config.yml',
|
|
666
|
+
maximumPreparingIssuesCount: null,
|
|
667
|
+
utilizationPercentageThreshold: 90,
|
|
668
|
+
allowedIssueAuthors: ['testuser'],
|
|
669
|
+
manager: 'manager-user',
|
|
670
|
+
codexHomeCandidates: null,
|
|
671
|
+
labelsAsLlmAgentName: null,
|
|
672
|
+
maxConcurrentWorkers: 2,
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
676
|
+
expect(
|
|
677
|
+
mockLocalCommandRunner.runCommand.mock.calls.map((c) => c[0]),
|
|
678
|
+
).not.toContain('aw');
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('stops spawning mid-loop when workers started in the run bring the count to the cap', async () => {
|
|
682
|
+
const awaitingIssues: Issue[] = [
|
|
683
|
+
createMockIssue({
|
|
684
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
685
|
+
status: 'Awaiting Workspace',
|
|
686
|
+
author: 'testuser',
|
|
687
|
+
}),
|
|
688
|
+
createMockIssue({
|
|
689
|
+
number: 2,
|
|
690
|
+
url: 'https://github.com/user/repo/issues/11',
|
|
691
|
+
status: 'Awaiting Workspace',
|
|
692
|
+
author: 'testuser',
|
|
693
|
+
}),
|
|
694
|
+
];
|
|
695
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
696
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
697
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
698
|
+
);
|
|
699
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
700
|
+
mockTakeOwnershipSpawnRepository.listRunningIssueUrls.mockReturnValue([]);
|
|
701
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
702
|
+
stdout: '',
|
|
703
|
+
stderr: '',
|
|
704
|
+
exitCode: 0,
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
await useCase.run({
|
|
708
|
+
projectUrl: 'https://github.com/user/repo',
|
|
709
|
+
defaultAgentName: 'agent1',
|
|
710
|
+
defaultLlmModelName: 'claude-opus',
|
|
711
|
+
fallbackLlmModelName: null,
|
|
712
|
+
defaultLlmAgentName: null,
|
|
713
|
+
configFilePath: '/path/to/config.yml',
|
|
714
|
+
maximumPreparingIssuesCount: null,
|
|
715
|
+
utilizationPercentageThreshold: 90,
|
|
716
|
+
allowedIssueAuthors: ['testuser'],
|
|
717
|
+
manager: 'manager-user',
|
|
718
|
+
codexHomeCandidates: null,
|
|
719
|
+
labelsAsLlmAgentName: null,
|
|
720
|
+
maxConcurrentWorkers: 1,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const awCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
|
|
724
|
+
(call) => call[0] === 'aw',
|
|
725
|
+
);
|
|
726
|
+
expect(awCalls).toHaveLength(1);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it('skips the preparation cycle when GraphQL rate limit is below the floor', async () => {
|
|
730
|
+
const awaitingIssues: Issue[] = [
|
|
731
|
+
createMockIssue({
|
|
732
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
733
|
+
status: 'Awaiting Workspace',
|
|
734
|
+
author: 'testuser',
|
|
735
|
+
}),
|
|
736
|
+
];
|
|
737
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
738
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
739
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
740
|
+
);
|
|
741
|
+
mockGitHubGraphqlRateLimitRepository.getRemainingRequestCount.mockResolvedValue(
|
|
742
|
+
499,
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
await useCase.run({
|
|
746
|
+
projectUrl: 'https://github.com/user/repo',
|
|
747
|
+
defaultAgentName: 'agent1',
|
|
748
|
+
defaultLlmModelName: 'claude-opus',
|
|
749
|
+
fallbackLlmModelName: null,
|
|
750
|
+
defaultLlmAgentName: null,
|
|
751
|
+
configFilePath: '/path/to/config.yml',
|
|
752
|
+
maximumPreparingIssuesCount: null,
|
|
753
|
+
utilizationPercentageThreshold: 90,
|
|
754
|
+
allowedIssueAuthors: ['testuser'],
|
|
755
|
+
manager: 'manager-user',
|
|
756
|
+
codexHomeCandidates: null,
|
|
757
|
+
labelsAsLlmAgentName: null,
|
|
758
|
+
graphqlRateLimitFloor: 500,
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
762
|
+
expect(
|
|
763
|
+
mockLocalCommandRunner.runCommand.mock.calls.map((c) => c[0]),
|
|
764
|
+
).not.toContain('aw');
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('proceeds with spawning when GraphQL remaining equals the floor exactly', async () => {
|
|
768
|
+
const awaitingIssues: Issue[] = [
|
|
769
|
+
createMockIssue({
|
|
770
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
771
|
+
status: 'Awaiting Workspace',
|
|
772
|
+
author: 'testuser',
|
|
773
|
+
}),
|
|
774
|
+
];
|
|
775
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
776
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
777
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
778
|
+
);
|
|
779
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
780
|
+
mockGitHubGraphqlRateLimitRepository.getRemainingRequestCount.mockResolvedValue(
|
|
781
|
+
500,
|
|
782
|
+
);
|
|
783
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
784
|
+
stdout: '',
|
|
785
|
+
stderr: '',
|
|
786
|
+
exitCode: 0,
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
await useCase.run({
|
|
790
|
+
projectUrl: 'https://github.com/user/repo',
|
|
791
|
+
defaultAgentName: 'agent1',
|
|
792
|
+
defaultLlmModelName: 'claude-opus',
|
|
793
|
+
fallbackLlmModelName: null,
|
|
794
|
+
defaultLlmAgentName: null,
|
|
795
|
+
configFilePath: '/path/to/config.yml',
|
|
796
|
+
maximumPreparingIssuesCount: null,
|
|
797
|
+
utilizationPercentageThreshold: 90,
|
|
798
|
+
allowedIssueAuthors: ['testuser'],
|
|
799
|
+
manager: 'manager-user',
|
|
800
|
+
codexHomeCandidates: null,
|
|
801
|
+
labelsAsLlmAgentName: null,
|
|
802
|
+
graphqlRateLimitFloor: 500,
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
const awCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
|
|
806
|
+
(call) => call[0] === 'aw',
|
|
807
|
+
);
|
|
808
|
+
expect(awCalls).toHaveLength(1);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it('proceeds with spawning when GraphQL rate limit check returns null', async () => {
|
|
812
|
+
const awaitingIssues: Issue[] = [
|
|
813
|
+
createMockIssue({
|
|
814
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
815
|
+
status: 'Awaiting Workspace',
|
|
816
|
+
author: 'testuser',
|
|
817
|
+
}),
|
|
818
|
+
];
|
|
819
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
820
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
821
|
+
createMockStoryObjectMap(awaitingIssues),
|
|
822
|
+
);
|
|
823
|
+
mockIssueRepository.getAllOpened.mockResolvedValue([]);
|
|
824
|
+
mockGitHubGraphqlRateLimitRepository.getRemainingRequestCount.mockResolvedValue(
|
|
825
|
+
null,
|
|
826
|
+
);
|
|
827
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
828
|
+
stdout: '',
|
|
829
|
+
stderr: '',
|
|
830
|
+
exitCode: 0,
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
await useCase.run({
|
|
834
|
+
projectUrl: 'https://github.com/user/repo',
|
|
835
|
+
defaultAgentName: 'agent1',
|
|
836
|
+
defaultLlmModelName: 'claude-opus',
|
|
837
|
+
fallbackLlmModelName: null,
|
|
838
|
+
defaultLlmAgentName: null,
|
|
839
|
+
configFilePath: '/path/to/config.yml',
|
|
840
|
+
maximumPreparingIssuesCount: null,
|
|
841
|
+
utilizationPercentageThreshold: 90,
|
|
842
|
+
allowedIssueAuthors: ['testuser'],
|
|
843
|
+
manager: 'manager-user',
|
|
844
|
+
codexHomeCandidates: null,
|
|
845
|
+
labelsAsLlmAgentName: null,
|
|
846
|
+
graphqlRateLimitFloor: 500,
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
const awCalls = mockLocalCommandRunner.runCommand.mock.calls.filter(
|
|
850
|
+
(call) => call[0] === 'aw',
|
|
851
|
+
);
|
|
852
|
+
expect(awCalls).toHaveLength(1);
|
|
853
|
+
});
|
|
854
|
+
|
|
630
855
|
it('should pass --branch to aw command when issue has an existing linked PR', async () => {
|
|
631
856
|
const awaitingIssues: Issue[] = [
|
|
632
857
|
createMockIssue({
|
|
@@ -7055,6 +7280,7 @@ describe('StartPreparationUseCase.buildRotationOrder', () => {
|
|
|
7055
7280
|
listSpawns: jest.fn().mockReturnValue([]),
|
|
7056
7281
|
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
7057
7282
|
},
|
|
7283
|
+
{ getRemainingRequestCount: jest.fn().mockResolvedValue(null) },
|
|
7058
7284
|
);
|
|
7059
7285
|
|
|
7060
7286
|
it('lists selected tokens first in ascending 7-day reset deadline order then excluded tokens', () => {
|
|
@@ -7354,6 +7580,7 @@ describe('StartPreparationUseCase.getTokenConcurrentLimit', () => {
|
|
|
7354
7580
|
listSpawns: jest.fn().mockReturnValue([]),
|
|
7355
7581
|
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
7356
7582
|
},
|
|
7583
|
+
{ getRemainingRequestCount: jest.fn().mockResolvedValue(null) },
|
|
7357
7584
|
);
|
|
7358
7585
|
});
|
|
7359
7586
|
|
|
@@ -7465,6 +7692,7 @@ describe('StartPreparationUseCase.run normalConcurrentLimit', () => {
|
|
|
7465
7692
|
mockLocalCommandRunner,
|
|
7466
7693
|
mockClaudeTokenUsageRepository,
|
|
7467
7694
|
mockTakeOwnershipSpawnRepository,
|
|
7695
|
+
{ getRemainingRequestCount: jest.fn().mockResolvedValue(null) },
|
|
7468
7696
|
);
|
|
7469
7697
|
|
|
7470
7698
|
await useCase.run({
|
|
@@ -7542,6 +7770,7 @@ describe('StartPreparationUseCase.fetchSpawnCandidateBranchSources', () => {
|
|
|
7542
7770
|
listSpawns: jest.fn().mockReturnValue([]),
|
|
7543
7771
|
listRunningIssueUrls: jest.fn().mockReturnValue([]),
|
|
7544
7772
|
},
|
|
7773
|
+
{ getRemainingRequestCount: jest.fn().mockResolvedValue(null) },
|
|
7545
7774
|
);
|
|
7546
7775
|
|
|
7547
7776
|
it('looks up related open pull requests for issue urls concurrently up to the configured limit', async () => {
|