github-issue-tower-defence-management 2.10.0 → 2.10.2
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/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/NotifyFinishedIssuePreparationUseCase.js +47 -5
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +24 -0
- package/src/domain/usecases/ConflictedIssueRevertUseCase.test.ts +188 -5
- package/src/domain/usecases/ConflictedIssueRevertUseCase.ts +12 -4
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +239 -15
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +89 -10
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +6 -0
- 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/NotifyFinishedIssuePreparationUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -2786,6 +2786,30 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
2786
2786
|
return `${status} ${reason}`;
|
|
2787
2787
|
};
|
|
2788
2788
|
|
|
2789
|
+
updateBranch = async (prUrl: string): Promise<boolean> => {
|
|
2790
|
+
const { owner, repo, issueNumber: prNumber } = this.parseIssueUrl(prUrl);
|
|
2791
|
+
const response = await this.fetchWithRateLimitRetry(() =>
|
|
2792
|
+
fetch(
|
|
2793
|
+
`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls/${prNumber}/update-branch`,
|
|
2794
|
+
{
|
|
2795
|
+
method: 'PUT',
|
|
2796
|
+
headers: {
|
|
2797
|
+
Authorization: `Bearer ${this.ghToken}`,
|
|
2798
|
+
Accept: 'application/vnd.github+json',
|
|
2799
|
+
},
|
|
2800
|
+
},
|
|
2801
|
+
),
|
|
2802
|
+
);
|
|
2803
|
+
if (response.ok) {
|
|
2804
|
+
return true;
|
|
2805
|
+
}
|
|
2806
|
+
if (response.status === 422) {
|
|
2807
|
+
return false;
|
|
2808
|
+
}
|
|
2809
|
+
const reason = await this.formatGitHubErrorWithStatus(response);
|
|
2810
|
+
throw new Error(`Failed to update branch for PR ${prUrl}: ${reason}`);
|
|
2811
|
+
};
|
|
2812
|
+
|
|
2789
2813
|
deletePullRequestBranch = async (
|
|
2790
2814
|
prUrl: string,
|
|
2791
2815
|
branchName: string,
|
|
@@ -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,40 @@ 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.updateStatus).toHaveBeenCalledWith(
|
|
198
|
+
mockProject,
|
|
199
|
+
issue,
|
|
200
|
+
'awaiting-workspace-id',
|
|
201
|
+
);
|
|
202
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
203
|
+
issue,
|
|
204
|
+
'conflict',
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
173
208
|
it('should skip PR items when scanning for target task issues', async () => {
|
|
174
209
|
const prIssue = createMockIssue({
|
|
175
210
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -195,6 +230,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
195
230
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
196
231
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
197
232
|
);
|
|
233
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
198
234
|
|
|
199
235
|
await useCase.run({ projectUrl });
|
|
200
236
|
|
|
@@ -255,7 +291,44 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
255
291
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
256
292
|
});
|
|
257
293
|
|
|
258
|
-
it('should update status and post conflict comment when linked PR is conflicted', async () => {
|
|
294
|
+
it('should update status and post conflict comment when linked PR is conflicted and update-branch fails', async () => {
|
|
295
|
+
const issue = createMockIssue({
|
|
296
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
297
|
+
status: 'Preparation',
|
|
298
|
+
});
|
|
299
|
+
const prItem = createMockPrItem({
|
|
300
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
301
|
+
closingIssueReferenceUrls: [issue.url],
|
|
302
|
+
});
|
|
303
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
304
|
+
project: mockProject,
|
|
305
|
+
issues: [issue, prItem],
|
|
306
|
+
cacheUsed: false,
|
|
307
|
+
});
|
|
308
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
309
|
+
url: prItem.url,
|
|
310
|
+
isConflicted: true,
|
|
311
|
+
mergeable: 'CONFLICTING',
|
|
312
|
+
});
|
|
313
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
314
|
+
new Map([[conflictedPr.url, conflictedPr]]),
|
|
315
|
+
);
|
|
316
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
317
|
+
|
|
318
|
+
await useCase.run({ projectUrl });
|
|
319
|
+
|
|
320
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
321
|
+
mockProject,
|
|
322
|
+
issue,
|
|
323
|
+
'awaiting-workspace-id',
|
|
324
|
+
);
|
|
325
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
326
|
+
issue,
|
|
327
|
+
'conflict',
|
|
328
|
+
);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should not update status or post comment when update-branch succeeds for the conflicted PR', async () => {
|
|
259
332
|
const issue = createMockIssue({
|
|
260
333
|
url: 'https://github.com/user/repo/issues/1',
|
|
261
334
|
status: 'Preparation',
|
|
@@ -277,6 +350,105 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
277
350
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
278
351
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
279
352
|
);
|
|
353
|
+
mockIssueRepository.updateBranch.mockResolvedValue(true);
|
|
354
|
+
|
|
355
|
+
await useCase.run({ projectUrl });
|
|
356
|
+
|
|
357
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
358
|
+
conflictedPr.url,
|
|
359
|
+
);
|
|
360
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
|
|
361
|
+
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should call update-branch only for conflicted PRs, not for all linked PRs', async () => {
|
|
365
|
+
const issue = createMockIssue({
|
|
366
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
367
|
+
status: 'Preparation',
|
|
368
|
+
});
|
|
369
|
+
const prItem1 = createMockPrItem({
|
|
370
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
371
|
+
number: 1,
|
|
372
|
+
closingIssueReferenceUrls: [issue.url],
|
|
373
|
+
});
|
|
374
|
+
const prItem2 = createMockPrItem({
|
|
375
|
+
url: 'https://github.com/user/repo/pull/2',
|
|
376
|
+
number: 2,
|
|
377
|
+
closingIssueReferenceUrls: [issue.url],
|
|
378
|
+
});
|
|
379
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
380
|
+
project: mockProject,
|
|
381
|
+
issues: [issue, prItem1, prItem2],
|
|
382
|
+
cacheUsed: false,
|
|
383
|
+
});
|
|
384
|
+
const conflictedPr = createMockRelatedPullRequest({
|
|
385
|
+
url: prItem1.url,
|
|
386
|
+
isConflicted: true,
|
|
387
|
+
mergeable: 'CONFLICTING',
|
|
388
|
+
});
|
|
389
|
+
const mergeablePr = createMockRelatedPullRequest({
|
|
390
|
+
url: prItem2.url,
|
|
391
|
+
isConflicted: false,
|
|
392
|
+
mergeable: 'MERGEABLE',
|
|
393
|
+
});
|
|
394
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
395
|
+
new Map([
|
|
396
|
+
[conflictedPr.url, conflictedPr],
|
|
397
|
+
[mergeablePr.url, mergeablePr],
|
|
398
|
+
]),
|
|
399
|
+
);
|
|
400
|
+
mockIssueRepository.updateBranch.mockResolvedValue(true);
|
|
401
|
+
|
|
402
|
+
await useCase.run({ projectUrl });
|
|
403
|
+
|
|
404
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledTimes(1);
|
|
405
|
+
expect(mockIssueRepository.updateBranch).toHaveBeenCalledWith(
|
|
406
|
+
conflictedPr.url,
|
|
407
|
+
);
|
|
408
|
+
expect(mockIssueRepository.updateBranch).not.toHaveBeenCalledWith(
|
|
409
|
+
mergeablePr.url,
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should fall back to status update when any conflicted PR update-branch fails', async () => {
|
|
414
|
+
const issue = createMockIssue({
|
|
415
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
416
|
+
status: 'Preparation',
|
|
417
|
+
});
|
|
418
|
+
const prItem1 = createMockPrItem({
|
|
419
|
+
url: 'https://github.com/user/repo/pull/1',
|
|
420
|
+
number: 1,
|
|
421
|
+
closingIssueReferenceUrls: [issue.url],
|
|
422
|
+
});
|
|
423
|
+
const prItem2 = createMockPrItem({
|
|
424
|
+
url: 'https://github.com/user/repo/pull/2',
|
|
425
|
+
number: 2,
|
|
426
|
+
closingIssueReferenceUrls: [issue.url],
|
|
427
|
+
});
|
|
428
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
429
|
+
project: mockProject,
|
|
430
|
+
issues: [issue, prItem1, prItem2],
|
|
431
|
+
cacheUsed: false,
|
|
432
|
+
});
|
|
433
|
+
const conflictedPr1 = createMockRelatedPullRequest({
|
|
434
|
+
url: prItem1.url,
|
|
435
|
+
isConflicted: true,
|
|
436
|
+
mergeable: 'CONFLICTING',
|
|
437
|
+
});
|
|
438
|
+
const conflictedPr2 = createMockRelatedPullRequest({
|
|
439
|
+
url: prItem2.url,
|
|
440
|
+
isConflicted: true,
|
|
441
|
+
mergeable: 'CONFLICTING',
|
|
442
|
+
});
|
|
443
|
+
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
444
|
+
new Map([
|
|
445
|
+
[conflictedPr1.url, conflictedPr1],
|
|
446
|
+
[conflictedPr2.url, conflictedPr2],
|
|
447
|
+
]),
|
|
448
|
+
);
|
|
449
|
+
mockIssueRepository.updateBranch
|
|
450
|
+
.mockResolvedValueOnce(true)
|
|
451
|
+
.mockResolvedValueOnce(false);
|
|
280
452
|
|
|
281
453
|
await useCase.run({ projectUrl });
|
|
282
454
|
|
|
@@ -313,6 +485,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
313
485
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
314
486
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
315
487
|
);
|
|
488
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
316
489
|
|
|
317
490
|
await useCase.run({ projectUrl });
|
|
318
491
|
|
|
@@ -430,6 +603,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
430
603
|
[mergeablePr.url, mergeablePr],
|
|
431
604
|
]),
|
|
432
605
|
);
|
|
606
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
433
607
|
|
|
434
608
|
await useCase.run({ projectUrl });
|
|
435
609
|
|
|
@@ -491,7 +665,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
491
665
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
492
666
|
});
|
|
493
667
|
|
|
494
|
-
it('should
|
|
668
|
+
it('should call update-branch before status update and comment when update-branch fails', async () => {
|
|
495
669
|
const callOrder: string[] = [];
|
|
496
670
|
const issue = createMockIssue({
|
|
497
671
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -514,6 +688,10 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
514
688
|
mockIssueRepository.getOpenPullRequests.mockResolvedValue(
|
|
515
689
|
new Map([[conflictedPr.url, conflictedPr]]),
|
|
516
690
|
);
|
|
691
|
+
mockIssueRepository.updateBranch.mockImplementation(() => {
|
|
692
|
+
callOrder.push('updateBranch');
|
|
693
|
+
return Promise.resolve(false);
|
|
694
|
+
});
|
|
517
695
|
mockIssueRepository.updateStatus.mockImplementation(() => {
|
|
518
696
|
callOrder.push('updateStatus');
|
|
519
697
|
return Promise.resolve();
|
|
@@ -525,7 +703,11 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
525
703
|
|
|
526
704
|
await useCase.run({ projectUrl });
|
|
527
705
|
|
|
528
|
-
expect(callOrder).toEqual([
|
|
706
|
+
expect(callOrder).toEqual([
|
|
707
|
+
'updateBranch',
|
|
708
|
+
'updateStatus',
|
|
709
|
+
'createComment',
|
|
710
|
+
]);
|
|
529
711
|
});
|
|
530
712
|
|
|
531
713
|
it('should fetch all PR URLs in a single batched call', async () => {
|
|
@@ -567,7 +749,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
567
749
|
);
|
|
568
750
|
});
|
|
569
751
|
|
|
570
|
-
it('should process multiple conflicted issues in one run', async () => {
|
|
752
|
+
it('should process multiple conflicted issues in one run when update-branch fails for both', async () => {
|
|
571
753
|
const issue1 = createMockIssue({
|
|
572
754
|
url: 'https://github.com/user/repo/issues/1',
|
|
573
755
|
number: 1,
|
|
@@ -609,6 +791,7 @@ describe('ConflictedIssueRevertUseCase', () => {
|
|
|
609
791
|
[conflictedPr2.url, conflictedPr2],
|
|
610
792
|
]),
|
|
611
793
|
);
|
|
794
|
+
mockIssueRepository.updateBranch.mockResolvedValue(false);
|
|
612
795
|
|
|
613
796
|
await useCase.run({ projectUrl });
|
|
614
797
|
|
|
@@ -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
|
|
|
@@ -117,6 +117,9 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
117
117
|
searchIssue: jest.Mock;
|
|
118
118
|
createNewIssue: jest.Mock;
|
|
119
119
|
updateNextActionDate: jest.Mock;
|
|
120
|
+
updateStory: jest.Mock;
|
|
121
|
+
addIssueToProject: jest.Mock;
|
|
122
|
+
getIssueByUrl: jest.Mock;
|
|
120
123
|
};
|
|
121
124
|
let mockIssueCommentRepository: {
|
|
122
125
|
getCommentsFromIssue: jest.Mock;
|
|
@@ -174,6 +177,9 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
174
177
|
searchIssue: jest.fn().mockResolvedValue([]),
|
|
175
178
|
createNewIssue: jest.fn().mockResolvedValue(42),
|
|
176
179
|
updateNextActionDate: jest.fn().mockResolvedValue(undefined),
|
|
180
|
+
updateStory: jest.fn().mockResolvedValue(undefined),
|
|
181
|
+
addIssueToProject: jest.fn().mockResolvedValue(undefined),
|
|
182
|
+
getIssueByUrl: jest.fn().mockResolvedValue(null),
|
|
177
183
|
};
|
|
178
184
|
|
|
179
185
|
mockIssueCommentRepository = {
|
|
@@ -4477,11 +4483,10 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
4477
4483
|
});
|
|
4478
4484
|
|
|
4479
4485
|
describe('nextStepAgent validation against agents list', () => {
|
|
4480
|
-
it('
|
|
4481
|
-
const
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
});
|
|
4486
|
+
it('creates a workflow blocker issue and returns original task to Awaiting Workspace when nextStepAgent is not in configured agents list', async () => {
|
|
4487
|
+
const issueUrl = 'https://github.com/user/repo/issues/1';
|
|
4488
|
+
const blockerIssueUrl = 'https://github.com/user/repo/issues/42';
|
|
4489
|
+
const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
|
|
4485
4490
|
|
|
4486
4491
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
4487
4492
|
mockIssueRepository.get.mockResolvedValue(issue);
|
|
@@ -4491,37 +4496,256 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
4491
4496
|
'From: :robot: test-agent (model)\n```json\n{"nextStepAgent": "unknown-agent"}\n```',
|
|
4492
4497
|
}),
|
|
4493
4498
|
]);
|
|
4494
|
-
mockIssueRepository.
|
|
4499
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
4500
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(42);
|
|
4495
4501
|
|
|
4496
4502
|
await useCase.run({
|
|
4497
4503
|
projectUrl: 'https://github.com/users/user/projects/1',
|
|
4498
|
-
issueUrl
|
|
4504
|
+
issueUrl,
|
|
4499
4505
|
thresholdForAutoReject: 3,
|
|
4500
4506
|
workflowBlockerResolvedWebhookUrl: null,
|
|
4501
4507
|
allowedIssueAuthors: ['test-user'],
|
|
4502
4508
|
agents: ['developer', 'triager'],
|
|
4503
4509
|
});
|
|
4504
4510
|
|
|
4511
|
+
expect(mockIssueRepository.createNewIssue).toHaveBeenCalledWith(
|
|
4512
|
+
'user',
|
|
4513
|
+
'repo',
|
|
4514
|
+
expect.stringContaining('unknown-agent'),
|
|
4515
|
+
expect.stringContaining(issueUrl),
|
|
4516
|
+
[],
|
|
4517
|
+
[],
|
|
4518
|
+
);
|
|
4519
|
+
expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
|
|
4520
|
+
issueUrl,
|
|
4521
|
+
mockProject,
|
|
4522
|
+
blockerIssueUrl,
|
|
4523
|
+
);
|
|
4505
4524
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
4525
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
4526
|
+
mockProject,
|
|
4527
|
+
);
|
|
4528
|
+
expect(mockIssueRepository.update).not.toHaveBeenCalledWith(
|
|
4506
4529
|
expect.objectContaining({ status: 'Failed Preparation' }),
|
|
4507
4530
|
mockProject,
|
|
4508
4531
|
);
|
|
4509
4532
|
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
4510
4533
|
mockProject,
|
|
4511
|
-
expect.objectContaining({ status: '
|
|
4512
|
-
'
|
|
4534
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
4535
|
+
'awaiting-workspace-id',
|
|
4513
4536
|
);
|
|
4514
4537
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
4515
|
-
expect.objectContaining({
|
|
4516
|
-
|
|
4538
|
+
expect.objectContaining({ url: issueUrl }),
|
|
4539
|
+
expect.stringContaining(blockerIssueUrl),
|
|
4540
|
+
);
|
|
4541
|
+
});
|
|
4542
|
+
|
|
4543
|
+
it('sets workflow blocker story on the created blocker issue when project has a matching story', async () => {
|
|
4544
|
+
const issueUrl = 'https://github.com/user/repo/issues/1';
|
|
4545
|
+
const blockerIssueUrl = 'https://github.com/user/repo/issues/42';
|
|
4546
|
+
const blockerStoryId = 'blocker-story-id';
|
|
4547
|
+
const projectWithStory = createMockProject({
|
|
4548
|
+
dependedIssueUrlSeparatedByComma: {
|
|
4549
|
+
name: 'Depended Issue URL',
|
|
4550
|
+
fieldId: 'depended-field-id',
|
|
4551
|
+
},
|
|
4552
|
+
story: {
|
|
4553
|
+
name: 'Story',
|
|
4554
|
+
fieldId: 'story-field-id',
|
|
4555
|
+
databaseId: 1,
|
|
4556
|
+
stories: [
|
|
4557
|
+
{
|
|
4558
|
+
id: blockerStoryId,
|
|
4559
|
+
name: 'workflow blocker',
|
|
4560
|
+
color: 'RED',
|
|
4561
|
+
description: '',
|
|
4562
|
+
},
|
|
4563
|
+
{
|
|
4564
|
+
id: 'regular-story-id',
|
|
4565
|
+
name: 'regular / workflow improvement',
|
|
4566
|
+
color: 'BLUE',
|
|
4567
|
+
description: '',
|
|
4568
|
+
},
|
|
4569
|
+
],
|
|
4570
|
+
workflowManagementStory: {
|
|
4571
|
+
id: 'wms-id',
|
|
4572
|
+
name: 'workflow management',
|
|
4573
|
+
},
|
|
4574
|
+
},
|
|
4575
|
+
});
|
|
4576
|
+
const blockerIssueObject = createMockIssue({
|
|
4577
|
+
url: blockerIssueUrl,
|
|
4578
|
+
itemId: 'blocker-item-id',
|
|
4579
|
+
});
|
|
4580
|
+
const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
|
|
4581
|
+
|
|
4582
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithStory);
|
|
4583
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
4584
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
4585
|
+
createMockComment({
|
|
4586
|
+
content:
|
|
4587
|
+
'From: :robot: test-agent (model)\n```json\n{"nextStepAgent": "unknown-agent"}\n```',
|
|
4517
4588
|
}),
|
|
4518
|
-
|
|
4589
|
+
]);
|
|
4590
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
4591
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(42);
|
|
4592
|
+
mockIssueRepository.getIssueByUrl.mockResolvedValue(blockerIssueObject);
|
|
4593
|
+
|
|
4594
|
+
await useCase.run({
|
|
4595
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
4596
|
+
issueUrl,
|
|
4597
|
+
thresholdForAutoReject: 3,
|
|
4598
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
4599
|
+
allowedIssueAuthors: ['test-user'],
|
|
4600
|
+
agents: ['developer', 'triager'],
|
|
4601
|
+
});
|
|
4602
|
+
|
|
4603
|
+
expect(mockIssueRepository.addIssueToProject).toHaveBeenCalledWith(
|
|
4604
|
+
projectWithStory,
|
|
4605
|
+
blockerIssueUrl,
|
|
4606
|
+
);
|
|
4607
|
+
expect(mockIssueRepository.updateStory).toHaveBeenCalledWith(
|
|
4608
|
+
expect.objectContaining({ story: projectWithStory.story }),
|
|
4609
|
+
blockerIssueObject,
|
|
4610
|
+
blockerStoryId,
|
|
4611
|
+
);
|
|
4612
|
+
});
|
|
4613
|
+
|
|
4614
|
+
it('reuses an existing open blocker issue instead of creating a new one', async () => {
|
|
4615
|
+
const issueUrl = 'https://github.com/user/repo/issues/1';
|
|
4616
|
+
const existingBlockerUrl = 'https://github.com/user/repo/issues/99';
|
|
4617
|
+
const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
|
|
4618
|
+
|
|
4619
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
4620
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
4621
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
4622
|
+
createMockComment({
|
|
4623
|
+
content:
|
|
4624
|
+
'From: :robot: test-agent (model)\n```json\n{"nextStepAgent": "unknown-agent"}\n```',
|
|
4625
|
+
}),
|
|
4626
|
+
]);
|
|
4627
|
+
mockIssueRepository.searchIssue.mockResolvedValue([
|
|
4628
|
+
{
|
|
4629
|
+
url: existingBlockerUrl,
|
|
4630
|
+
title: 'Unregistered agent in workflow configuration: unknown-agent',
|
|
4631
|
+
number: '99',
|
|
4632
|
+
},
|
|
4633
|
+
]);
|
|
4634
|
+
|
|
4635
|
+
await useCase.run({
|
|
4636
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
4637
|
+
issueUrl,
|
|
4638
|
+
thresholdForAutoReject: 3,
|
|
4639
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
4640
|
+
allowedIssueAuthors: ['test-user'],
|
|
4641
|
+
agents: ['developer', 'triager'],
|
|
4642
|
+
});
|
|
4643
|
+
|
|
4644
|
+
expect(mockIssueRepository.createNewIssue).not.toHaveBeenCalled();
|
|
4645
|
+
expect(mockIssueRepository.setDependedIssueUrl).toHaveBeenCalledWith(
|
|
4646
|
+
issueUrl,
|
|
4647
|
+
mockProject,
|
|
4648
|
+
existingBlockerUrl,
|
|
4519
4649
|
);
|
|
4520
4650
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
4521
|
-
expect.objectContaining({
|
|
4522
|
-
|
|
4651
|
+
expect.objectContaining({ url: issueUrl }),
|
|
4652
|
+
expect.stringContaining(existingBlockerUrl),
|
|
4653
|
+
);
|
|
4654
|
+
});
|
|
4655
|
+
|
|
4656
|
+
it('skips story assignment when project has no story field', async () => {
|
|
4657
|
+
const issueUrl = 'https://github.com/user/repo/issues/1';
|
|
4658
|
+
const projectWithoutStory = createMockProject({
|
|
4659
|
+
dependedIssueUrlSeparatedByComma: {
|
|
4660
|
+
name: 'Depended Issue URL',
|
|
4661
|
+
fieldId: 'depended-field-id',
|
|
4662
|
+
},
|
|
4663
|
+
story: null,
|
|
4664
|
+
});
|
|
4665
|
+
const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
|
|
4666
|
+
|
|
4667
|
+
mockProjectRepository.getByUrl.mockResolvedValue(projectWithoutStory);
|
|
4668
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
4669
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
4670
|
+
createMockComment({
|
|
4671
|
+
content:
|
|
4672
|
+
'From: :robot: test-agent (model)\n```json\n{"nextStepAgent": "unknown-agent"}\n```',
|
|
4673
|
+
}),
|
|
4674
|
+
]);
|
|
4675
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
4676
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(42);
|
|
4677
|
+
|
|
4678
|
+
await useCase.run({
|
|
4679
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
4680
|
+
issueUrl,
|
|
4681
|
+
thresholdForAutoReject: 3,
|
|
4682
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
4683
|
+
allowedIssueAuthors: ['test-user'],
|
|
4684
|
+
agents: ['developer', 'triager'],
|
|
4685
|
+
});
|
|
4686
|
+
|
|
4687
|
+
expect(mockIssueRepository.addIssueToProject).not.toHaveBeenCalled();
|
|
4688
|
+
expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
|
|
4689
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
4690
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
4691
|
+
projectWithoutStory,
|
|
4692
|
+
);
|
|
4693
|
+
});
|
|
4694
|
+
|
|
4695
|
+
it('skips story assignment when no workflow blocker story option exists in project', async () => {
|
|
4696
|
+
const issueUrl = 'https://github.com/user/repo/issues/1';
|
|
4697
|
+
const projectWithNoBlockerStory = createMockProject({
|
|
4698
|
+
dependedIssueUrlSeparatedByComma: {
|
|
4699
|
+
name: 'Depended Issue URL',
|
|
4700
|
+
fieldId: 'depended-field-id',
|
|
4701
|
+
},
|
|
4702
|
+
story: {
|
|
4703
|
+
name: 'Story',
|
|
4704
|
+
fieldId: 'story-field-id',
|
|
4705
|
+
databaseId: 1,
|
|
4706
|
+
stories: [
|
|
4707
|
+
{
|
|
4708
|
+
id: 'regular-story-id',
|
|
4709
|
+
name: 'regular / workflow improvement',
|
|
4710
|
+
color: 'BLUE',
|
|
4711
|
+
description: '',
|
|
4712
|
+
},
|
|
4713
|
+
],
|
|
4714
|
+
workflowManagementStory: {
|
|
4715
|
+
id: 'wms-id',
|
|
4716
|
+
name: 'workflow management',
|
|
4717
|
+
},
|
|
4718
|
+
},
|
|
4719
|
+
});
|
|
4720
|
+
const issue = createMockIssue({ url: issueUrl, status: 'Preparation' });
|
|
4721
|
+
|
|
4722
|
+
mockProjectRepository.getByUrl.mockResolvedValue(
|
|
4723
|
+
projectWithNoBlockerStory,
|
|
4724
|
+
);
|
|
4725
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
4726
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
4727
|
+
createMockComment({
|
|
4728
|
+
content:
|
|
4729
|
+
'From: :robot: test-agent (model)\n```json\n{"nextStepAgent": "unknown-agent"}\n```',
|
|
4523
4730
|
}),
|
|
4524
|
-
|
|
4731
|
+
]);
|
|
4732
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
4733
|
+
mockIssueRepository.createNewIssue.mockResolvedValue(42);
|
|
4734
|
+
|
|
4735
|
+
await useCase.run({
|
|
4736
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
4737
|
+
issueUrl,
|
|
4738
|
+
thresholdForAutoReject: 3,
|
|
4739
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
4740
|
+
allowedIssueAuthors: ['test-user'],
|
|
4741
|
+
agents: ['developer', 'triager'],
|
|
4742
|
+
});
|
|
4743
|
+
|
|
4744
|
+
expect(mockIssueRepository.addIssueToProject).not.toHaveBeenCalled();
|
|
4745
|
+
expect(mockIssueRepository.updateStory).not.toHaveBeenCalled();
|
|
4746
|
+
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
4747
|
+
expect.objectContaining({ status: 'Awaiting Workspace' }),
|
|
4748
|
+
projectWithNoBlockerStory,
|
|
4525
4749
|
);
|
|
4526
4750
|
});
|
|
4527
4751
|
|