github-issue-tower-defence-management 2.9.0 → 2.10.1
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 +2 -2
- package/bin/adapter/entry-points/cli/index.js +2 -2
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +17 -5
- package/bin/adapter/entry-points/cli/projectConfig.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/domain/usecases/CheckIssueReviewReadinessUseCase.js +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +6 -4
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +60 -14
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +4 -4
- package/src/adapter/entry-points/cli/index.ts +2 -2
- package/src/adapter/entry-points/cli/projectConfig.test.ts +40 -12
- package/src/adapter/entry-points/cli/projectConfig.ts +22 -7
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -2
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -2
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -2
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +43 -7
- package/src/domain/usecases/IssueRejectionEvaluator.ts +9 -7
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +243 -19
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +103 -21
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +2 -2
- package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +6 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +1 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +2 -2
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +3 -2
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
|
@@ -214,7 +214,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
214
214
|
agent: 'my-developer',
|
|
215
215
|
},
|
|
216
216
|
[],
|
|
217
|
-
{
|
|
217
|
+
{ developerAgentNames: ['my-developer'] },
|
|
218
218
|
);
|
|
219
219
|
|
|
220
220
|
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
@@ -222,7 +222,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
222
222
|
expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
it('should not require PR evaluation when issue agent does not match custom developer agent
|
|
225
|
+
it('should not require PR evaluation when issue agent does not match custom developer agent names', async () => {
|
|
226
226
|
const result = await evaluator.evaluate(
|
|
227
227
|
{
|
|
228
228
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -231,7 +231,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
231
231
|
agent: 'other-agent',
|
|
232
232
|
},
|
|
233
233
|
[],
|
|
234
|
-
{
|
|
234
|
+
{ developerAgentNames: ['my-developer'] },
|
|
235
235
|
);
|
|
236
236
|
|
|
237
237
|
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
@@ -239,7 +239,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
239
239
|
expect(result.approvedPrUrl).toBeNull();
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
it('should default to developer when
|
|
242
|
+
it('should default to developer when developerAgentNames is null', async () => {
|
|
243
243
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
244
244
|
|
|
245
245
|
const result = await evaluator.evaluate(
|
|
@@ -250,7 +250,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
250
250
|
agent: 'developer',
|
|
251
251
|
},
|
|
252
252
|
[],
|
|
253
|
-
{
|
|
253
|
+
{ developerAgentNames: null },
|
|
254
254
|
);
|
|
255
255
|
|
|
256
256
|
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
@@ -258,6 +258,42 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
258
258
|
expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
|
|
259
259
|
});
|
|
260
260
|
|
|
261
|
+
it('should require PR evaluation when issue agent matches any of multiple developer agent names', async () => {
|
|
262
|
+
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
|
|
263
|
+
|
|
264
|
+
const result = await evaluator.evaluate(
|
|
265
|
+
{
|
|
266
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
267
|
+
labels: [],
|
|
268
|
+
isPr: false,
|
|
269
|
+
agent: 'second-developer',
|
|
270
|
+
},
|
|
271
|
+
[],
|
|
272
|
+
{ developerAgentNames: ['first-developer', 'second-developer'] },
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
276
|
+
expect(result.rejections).toHaveLength(1);
|
|
277
|
+
expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should not require PR evaluation when issue agent does not match any of multiple developer agent names', async () => {
|
|
281
|
+
const result = await evaluator.evaluate(
|
|
282
|
+
{
|
|
283
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
284
|
+
labels: [],
|
|
285
|
+
isPr: false,
|
|
286
|
+
agent: 'chore',
|
|
287
|
+
},
|
|
288
|
+
[],
|
|
289
|
+
{ developerAgentNames: ['first-developer', 'second-developer'] },
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
|
|
293
|
+
expect(result.rejections).toHaveLength(0);
|
|
294
|
+
expect(result.approvedPrUrl).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
261
297
|
it('should require PR evaluation and reject CI failure when issue agent is pr-reviewer', async () => {
|
|
262
298
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
263
299
|
createReadyPr('https://github.com/user/repo/pull/10', {
|
|
@@ -316,7 +352,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
316
352
|
expect(result.approvedPrUrl).toBe('https://github.com/user/repo/pull/10');
|
|
317
353
|
});
|
|
318
354
|
|
|
319
|
-
it('should still require PR evaluation for pr-reviewer when custom
|
|
355
|
+
it('should still require PR evaluation for pr-reviewer when custom developerAgentNames is set', async () => {
|
|
320
356
|
mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
|
|
321
357
|
createReadyPr('https://github.com/user/repo/pull/10', {
|
|
322
358
|
isPassedAllCiJob: false,
|
|
@@ -332,7 +368,7 @@ describe('IssueRejectionEvaluator', () => {
|
|
|
332
368
|
agent: 'pr-reviewer',
|
|
333
369
|
},
|
|
334
370
|
[],
|
|
335
|
-
{
|
|
371
|
+
{ developerAgentNames: ['my-developer'] },
|
|
336
372
|
);
|
|
337
373
|
|
|
338
374
|
expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
|
|
@@ -33,9 +33,9 @@ export type EvaluateOptions = {
|
|
|
33
33
|
// getOpenPullRequest call, which keeps the transient-failure handling that
|
|
34
34
|
// distinguishes an unknown state from an absent pull request.
|
|
35
35
|
resolvedOpenPrByUrl?: ReadonlyMap<string, RelatedPullRequest | null> | null;
|
|
36
|
-
// The agent
|
|
37
|
-
// When null or
|
|
38
|
-
|
|
36
|
+
// The agent names treated as developer agents for PR/CI/conflict checks.
|
|
37
|
+
// When null, undefined, or empty, defaults to ['developer'].
|
|
38
|
+
developerAgentNames?: string[] | null;
|
|
39
39
|
detectConflictEvenIfEvaluationSkipped?: boolean;
|
|
40
40
|
};
|
|
41
41
|
|
|
@@ -68,7 +68,7 @@ export class IssueRejectionEvaluator {
|
|
|
68
68
|
this.requiresPullRequestEvaluation(
|
|
69
69
|
issue,
|
|
70
70
|
labelsNotRequiringPullRequest,
|
|
71
|
-
options.
|
|
71
|
+
options.developerAgentNames,
|
|
72
72
|
)
|
|
73
73
|
) {
|
|
74
74
|
let prsToCheck: RelatedPullRequest[];
|
|
@@ -226,15 +226,17 @@ export class IssueRejectionEvaluator {
|
|
|
226
226
|
requiresPullRequestEvaluation = (
|
|
227
227
|
issue: { labels: string[]; body?: string | null; agent?: string | null },
|
|
228
228
|
labelsNotRequiringPullRequest: string[] = [],
|
|
229
|
-
|
|
229
|
+
developerAgentNames?: string[] | null,
|
|
230
230
|
): boolean => {
|
|
231
231
|
const categoryLabels = issue.labels.filter((label) =>
|
|
232
232
|
label.startsWith('category:'),
|
|
233
233
|
);
|
|
234
|
-
const
|
|
234
|
+
const effectiveDeveloperAgentNames = developerAgentNames?.length
|
|
235
|
+
? developerAgentNames
|
|
236
|
+
: ['developer'];
|
|
235
237
|
const isNonDeveloperAgent =
|
|
236
238
|
issue.agent != null &&
|
|
237
|
-
issue.agent
|
|
239
|
+
!effectiveDeveloperAgentNames.includes(issue.agent) &&
|
|
238
240
|
issue.agent !== 'pr-reviewer';
|
|
239
241
|
const hasLabelNotRequiringPullRequest = issue.labels.some((label) =>
|
|
240
242
|
labelsNotRequiringPullRequest.includes(label),
|
|
@@ -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 = {
|
|
@@ -1757,7 +1763,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1757
1763
|
);
|
|
1758
1764
|
});
|
|
1759
1765
|
|
|
1760
|
-
it('should reject a missing PR when the issue agent matches
|
|
1766
|
+
it('should reject a missing PR when the issue agent matches developerAgentNames', async () => {
|
|
1761
1767
|
const issue = createMockIssue({
|
|
1762
1768
|
url: 'https://github.com/user/repo/issues/1',
|
|
1763
1769
|
status: 'Preparation',
|
|
@@ -1777,7 +1783,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1777
1783
|
thresholdForAutoReject: 3,
|
|
1778
1784
|
workflowBlockerResolvedWebhookUrl: null,
|
|
1779
1785
|
allowedIssueAuthors: ['test-user'],
|
|
1780
|
-
|
|
1786
|
+
developerAgentNames: ['my-agent'],
|
|
1781
1787
|
});
|
|
1782
1788
|
|
|
1783
1789
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
@@ -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
|
|
|
@@ -4888,7 +5112,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
4888
5112
|
);
|
|
4889
5113
|
});
|
|
4890
5114
|
|
|
4891
|
-
it('should reassign to the configured
|
|
5115
|
+
it('should reassign to the first configured developerAgentNames when chore agent has a failing CI PR', async () => {
|
|
4892
5116
|
const issue = createMockIssue({
|
|
4893
5117
|
url: 'https://github.com/user/repo/issues/1',
|
|
4894
5118
|
status: 'Preparation',
|
|
@@ -4921,7 +5145,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
4921
5145
|
thresholdForAutoReject: 3,
|
|
4922
5146
|
workflowBlockerResolvedWebhookUrl: null,
|
|
4923
5147
|
allowedIssueAuthors: null,
|
|
4924
|
-
|
|
5148
|
+
developerAgentNames: ['custom-developer'],
|
|
4925
5149
|
});
|
|
4926
5150
|
|
|
4927
5151
|
expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledWith(
|
|
@@ -85,6 +85,9 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
85
85
|
| 'searchIssue'
|
|
86
86
|
| 'createNewIssue'
|
|
87
87
|
| 'updateNextActionDate'
|
|
88
|
+
| 'updateStory'
|
|
89
|
+
| 'addIssueToProject'
|
|
90
|
+
| 'getIssueByUrl'
|
|
88
91
|
>,
|
|
89
92
|
private readonly issueCommentRepository: Pick<
|
|
90
93
|
IssueCommentRepository,
|
|
@@ -116,7 +119,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
116
119
|
missingAgentName?: string | null;
|
|
117
120
|
sessionErrorLine?: string | null;
|
|
118
121
|
manager?: string | null;
|
|
119
|
-
|
|
122
|
+
developerAgentNames?: string[] | null;
|
|
120
123
|
deferPreparation?: boolean | null;
|
|
121
124
|
}): Promise<void> => {
|
|
122
125
|
const project = await this.projectRepository.getByUrl(params.projectUrl);
|
|
@@ -253,31 +256,26 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
253
256
|
params.agents.length > 0 &&
|
|
254
257
|
!params.agents.includes(nextStepAgent)
|
|
255
258
|
) {
|
|
256
|
-
|
|
257
|
-
await this.issueRepository.update(issue, project);
|
|
258
|
-
await this.issueRepository.updateStatus(
|
|
259
|
-
project,
|
|
260
|
-
issue,
|
|
261
|
-
failedPreparationStatusOption.id,
|
|
262
|
-
);
|
|
263
|
-
await this.patchConsoleTab(issue);
|
|
264
|
-
await this.issueCommentRepository.createComment(
|
|
259
|
+
await this.handleUnregisteredNextStepAgent(
|
|
265
260
|
issue,
|
|
266
|
-
|
|
261
|
+
project,
|
|
262
|
+
awaitingWorkspaceStatusOption,
|
|
263
|
+
nextStepAgent,
|
|
267
264
|
);
|
|
268
265
|
return;
|
|
269
266
|
}
|
|
270
267
|
|
|
271
268
|
const ciFailingPrUrl = await this.resolveLinkedPrWithCiFailure(
|
|
272
269
|
issue,
|
|
273
|
-
params.
|
|
270
|
+
params.developerAgentNames ?? null,
|
|
274
271
|
);
|
|
275
272
|
if (ciFailingPrUrl !== null) {
|
|
276
|
-
const
|
|
277
|
-
params.
|
|
273
|
+
const effectiveDeveloperAgentNames = params.developerAgentNames?.length
|
|
274
|
+
? params.developerAgentNames
|
|
275
|
+
: ['developer'];
|
|
278
276
|
const agentOptionId = await this.ensureAgentOptionAndGetId(
|
|
279
277
|
project,
|
|
280
|
-
|
|
278
|
+
effectiveDeveloperAgentNames[0],
|
|
281
279
|
);
|
|
282
280
|
if (agentOptionId !== null) {
|
|
283
281
|
await this.issueRepository.setIssueAgentField(
|
|
@@ -312,7 +310,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
312
310
|
isTrustedAuthor,
|
|
313
311
|
resolveLabelsNotRequiringPullRequest(params),
|
|
314
312
|
nextStepAgent,
|
|
315
|
-
params.
|
|
313
|
+
params.developerAgentNames,
|
|
316
314
|
);
|
|
317
315
|
|
|
318
316
|
const rejectionStatusMessage =
|
|
@@ -579,6 +577,88 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
579
577
|
);
|
|
580
578
|
};
|
|
581
579
|
|
|
580
|
+
private handleUnregisteredNextStepAgent = async (
|
|
581
|
+
issue: Issue,
|
|
582
|
+
project: Project,
|
|
583
|
+
awaitingWorkspaceStatusOption: { id: string },
|
|
584
|
+
nextStepAgent: string,
|
|
585
|
+
): Promise<void> => {
|
|
586
|
+
const blockerIssueTitle = `Unregistered agent in workflow configuration: ${nextStepAgent}`;
|
|
587
|
+
|
|
588
|
+
const searchResults = await this.issueRepository.searchIssue({
|
|
589
|
+
owner: issue.org,
|
|
590
|
+
repositoryName: issue.repo,
|
|
591
|
+
type: 'issue',
|
|
592
|
+
state: 'open',
|
|
593
|
+
title: blockerIssueTitle,
|
|
594
|
+
});
|
|
595
|
+
const exactMatch = searchResults.find((i) => i.title === blockerIssueTitle);
|
|
596
|
+
|
|
597
|
+
let blockerIssueUrl: string;
|
|
598
|
+
if (exactMatch) {
|
|
599
|
+
blockerIssueUrl = exactMatch.url;
|
|
600
|
+
} else {
|
|
601
|
+
const body = [
|
|
602
|
+
`The last agent report on ${issue.url} designated \`nextStepAgent\` as \`${nextStepAgent}\`, which is absent from the configured agents list.`,
|
|
603
|
+
'',
|
|
604
|
+
`- Missing agent name: \`${nextStepAgent}\``,
|
|
605
|
+
`- Declaring task: ${issue.url}`,
|
|
606
|
+
].join('\n');
|
|
607
|
+
const issueNumber = await this.issueRepository.createNewIssue(
|
|
608
|
+
issue.org,
|
|
609
|
+
issue.repo,
|
|
610
|
+
blockerIssueTitle,
|
|
611
|
+
body,
|
|
612
|
+
[],
|
|
613
|
+
[],
|
|
614
|
+
);
|
|
615
|
+
blockerIssueUrl = `https://github.com/${issue.org}/${issue.repo}/issues/${issueNumber}`;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (project.story) {
|
|
619
|
+
const workflowBlockerStory = project.story.stories.find((s) =>
|
|
620
|
+
s.name.toLowerCase().includes('workflow blocker'),
|
|
621
|
+
);
|
|
622
|
+
if (workflowBlockerStory) {
|
|
623
|
+
await this.issueRepository.addIssueToProject(project, blockerIssueUrl);
|
|
624
|
+
const blockerIssue =
|
|
625
|
+
await this.issueRepository.getIssueByUrl(blockerIssueUrl);
|
|
626
|
+
if (blockerIssue) {
|
|
627
|
+
await this.issueRepository.updateStory(
|
|
628
|
+
{ ...project, story: project.story },
|
|
629
|
+
blockerIssue,
|
|
630
|
+
workflowBlockerStory.id,
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (project.dependedIssueUrlSeparatedByComma) {
|
|
637
|
+
await this.issueRepository.setDependedIssueUrl(
|
|
638
|
+
issue.url,
|
|
639
|
+
project,
|
|
640
|
+
blockerIssueUrl,
|
|
641
|
+
);
|
|
642
|
+
} else {
|
|
643
|
+
console.warn(
|
|
644
|
+
`dependedIssueUrlSeparatedByComma not configured; cannot block ${issue.url} via ${blockerIssueUrl}`,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
issue.status = AWAITING_WORKSPACE_STATUS_NAME;
|
|
649
|
+
await this.issueRepository.update(issue, project);
|
|
650
|
+
await this.issueRepository.updateStatus(
|
|
651
|
+
project,
|
|
652
|
+
issue,
|
|
653
|
+
awaitingWorkspaceStatusOption.id,
|
|
654
|
+
);
|
|
655
|
+
await this.patchConsoleTab(issue);
|
|
656
|
+
await this.issueCommentRepository.createComment(
|
|
657
|
+
issue,
|
|
658
|
+
`nextStepAgent \`${nextStepAgent}\` is not in the configured agents list. Created workflow blocker task:\n${blockerIssueUrl}`,
|
|
659
|
+
);
|
|
660
|
+
};
|
|
661
|
+
|
|
582
662
|
private collectRejections = async (
|
|
583
663
|
issue: {
|
|
584
664
|
url: string;
|
|
@@ -590,7 +670,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
590
670
|
isTrustedAuthor: (author: string) => boolean,
|
|
591
671
|
labelsNotRequiringPullRequest: string[],
|
|
592
672
|
nextStepAgent: string | null,
|
|
593
|
-
|
|
673
|
+
developerAgentNames?: string[] | null,
|
|
594
674
|
): Promise<{
|
|
595
675
|
rejections: { type: RejectedReasonType; detail: string }[];
|
|
596
676
|
approvedPrUrl: string | null;
|
|
@@ -618,7 +698,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
618
698
|
await this.issueRejectionEvaluator.evaluate(
|
|
619
699
|
issue,
|
|
620
700
|
labelsNotRequiringPullRequest,
|
|
621
|
-
{
|
|
701
|
+
{ developerAgentNames },
|
|
622
702
|
);
|
|
623
703
|
const requiredPrRejections = isTriagerAgentName(nextStepAgent)
|
|
624
704
|
? prRejections.filter(
|
|
@@ -682,12 +762,14 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
682
762
|
|
|
683
763
|
private resolveLinkedPrWithCiFailure = async (
|
|
684
764
|
issue: { url: string; agent: string | null; isPr: boolean },
|
|
685
|
-
|
|
765
|
+
developerAgentNames: string[] | null,
|
|
686
766
|
): Promise<string | null> => {
|
|
687
|
-
const
|
|
767
|
+
const effectiveDeveloperAgentNames = developerAgentNames?.length
|
|
768
|
+
? developerAgentNames
|
|
769
|
+
: ['developer'];
|
|
688
770
|
if (
|
|
689
771
|
issue.agent === null ||
|
|
690
|
-
issue.agent
|
|
772
|
+
effectiveDeveloperAgentNames.includes(issue.agent) ||
|
|
691
773
|
issue.agent === 'pr-reviewer'
|
|
692
774
|
) {
|
|
693
775
|
return null;
|