github-issue-tower-defence-management 1.177.0 → 1.178.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 +16 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-Bh2slMtk.css → index-DeZcPNCr.css} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/{index-D6aAQrf7.js → index-KCmqh781.js} +1 -1
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +23 -0
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +23 -7
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js +1 -7
- package/bin/domain/usecases/console/GenerateConsoleListsUseCase.js.map +1 -1
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +31 -0
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.test.tsx +3 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/detail/ConsolePullRequestDetail.tsx +5 -0
- package/src/adapter/entry-points/console/ui/src/index.css +5 -0
- package/src/adapter/entry-points/console/ui-dist/assets/{index-Bh2slMtk.css → index-DeZcPNCr.css} +1 -1
- package/src/adapter/entry-points/console/ui-dist/assets/{index-D6aAQrf7.js → index-KCmqh781.js} +1 -1
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +79 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +37 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +128 -0
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +43 -15
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +21 -26
- package/src/domain/usecases/console/GenerateConsoleListsUseCase.ts +5 -13
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +97 -0
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +49 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts +0 -1
- package/types/domain/usecases/console/GenerateConsoleListsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +21 -0
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts.map +1 -0
|
@@ -947,6 +947,85 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
947
947
|
expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
|
|
948
948
|
});
|
|
949
949
|
|
|
950
|
+
it('should comment the repeated dispatch when the declared nextStepAgent is already the agent field value', async () => {
|
|
951
|
+
const issue = createMockIssue({
|
|
952
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
953
|
+
status: 'Preparation',
|
|
954
|
+
agent: 'accounting',
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
958
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
959
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
960
|
+
createMockComment({
|
|
961
|
+
content:
|
|
962
|
+
'From: :robot: triager\n```json\n{"nextStepAgent": "accounting", "nextStep": null}\n```',
|
|
963
|
+
}),
|
|
964
|
+
]);
|
|
965
|
+
|
|
966
|
+
await useCase.run({
|
|
967
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
968
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
969
|
+
thresholdForAutoReject: 3,
|
|
970
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
971
|
+
allowedIssueAuthors: null,
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
975
|
+
mockProject,
|
|
976
|
+
expect.anything(),
|
|
977
|
+
'awaiting-workspace-id',
|
|
978
|
+
);
|
|
979
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
980
|
+
expect.anything(),
|
|
981
|
+
expect.stringContaining('Next step agent dispatch repeated: accounting'),
|
|
982
|
+
);
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
it('should escalate to Failed Preparation when the declared nextStepAgent was already dispatched up to the threshold without a report', async () => {
|
|
986
|
+
const issue = createMockIssue({
|
|
987
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
988
|
+
status: 'Preparation',
|
|
989
|
+
agent: 'accounting',
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
993
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
994
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
995
|
+
createMockComment({
|
|
996
|
+
content:
|
|
997
|
+
'From: :robot: triager\n```json\n{"nextStepAgent": "accounting", "nextStep": null}\n```',
|
|
998
|
+
}),
|
|
999
|
+
createMockComment({
|
|
1000
|
+
content: 'Next step agent dispatch repeated: accounting',
|
|
1001
|
+
}),
|
|
1002
|
+
createMockComment({
|
|
1003
|
+
content: 'Next step agent dispatch repeated: accounting',
|
|
1004
|
+
}),
|
|
1005
|
+
]);
|
|
1006
|
+
|
|
1007
|
+
await useCase.run({
|
|
1008
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1009
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1010
|
+
thresholdForAutoReject: 3,
|
|
1011
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1012
|
+
allowedIssueAuthors: null,
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
1016
|
+
mockProject,
|
|
1017
|
+
expect.anything(),
|
|
1018
|
+
'failed-preparation-id',
|
|
1019
|
+
);
|
|
1020
|
+
expect(mockIssueRepository.setIssueAgentField).not.toHaveBeenCalled();
|
|
1021
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1022
|
+
expect.anything(),
|
|
1023
|
+
expect.stringContaining(
|
|
1024
|
+
'Failed to receive a report from the dispatched agent for 3 times',
|
|
1025
|
+
),
|
|
1026
|
+
);
|
|
1027
|
+
});
|
|
1028
|
+
|
|
950
1029
|
it('should not modify labels when nextStepAgent is specified', async () => {
|
|
951
1030
|
const issue = createMockIssue({
|
|
952
1031
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -31,6 +31,7 @@ import { extractNextStepAgent } from './extractNextStepAgent';
|
|
|
31
31
|
import { findLastAgentReport } from './findLastAgentReport';
|
|
32
32
|
import { issueReactivationTriggerIsPending } from './issueReactivationTriggerIsPending';
|
|
33
33
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
34
|
+
import { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
|
|
34
35
|
|
|
35
36
|
export class IssueNotFoundError extends Error {
|
|
36
37
|
constructor(issueUrl: string) {
|
|
@@ -229,7 +230,37 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
229
230
|
const nextStepAgent = lastAgentReport
|
|
230
231
|
? extractNextStepAgent(lastAgentReport.content)
|
|
231
232
|
: null;
|
|
233
|
+
const commentsAfterLastAgentReport = lastAgentReport
|
|
234
|
+
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
235
|
+
: [];
|
|
232
236
|
if (nextStepAgent !== null) {
|
|
237
|
+
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
238
|
+
agentFieldValue: issue.agent,
|
|
239
|
+
nextStepAgent,
|
|
240
|
+
commentsAfterLastAgentReport,
|
|
241
|
+
isTrustedAuthor,
|
|
242
|
+
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
243
|
+
});
|
|
244
|
+
if (repetition.type === 'escalateToFailedPreparation') {
|
|
245
|
+
issue.status = FAILED_PREPARATION_STATUS_NAME;
|
|
246
|
+
await this.issueRepository.update(issue, project);
|
|
247
|
+
await this.issueRepository.updateStatus(
|
|
248
|
+
project,
|
|
249
|
+
issue,
|
|
250
|
+
failedPreparationStatusOption.id,
|
|
251
|
+
);
|
|
252
|
+
await this.patchConsoleTab(issue);
|
|
253
|
+
await this.issueCommentRepository.createComment(
|
|
254
|
+
issue,
|
|
255
|
+
repetition.comment,
|
|
256
|
+
);
|
|
257
|
+
await this.sendWorkflowBlockerNotification(
|
|
258
|
+
params.issueUrl,
|
|
259
|
+
params.workflowBlockerResolvedWebhookUrl,
|
|
260
|
+
project,
|
|
261
|
+
);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
233
264
|
const agentOptionId = await this.ensureAgentOptionAndGetId(
|
|
234
265
|
project,
|
|
235
266
|
nextStepAgent,
|
|
@@ -249,6 +280,12 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
249
280
|
awaitingWorkspaceStatusOption.id,
|
|
250
281
|
);
|
|
251
282
|
await this.patchConsoleTab(issue);
|
|
283
|
+
if (repetition.type === 'dispatchAgain') {
|
|
284
|
+
await this.issueCommentRepository.createComment(
|
|
285
|
+
issue,
|
|
286
|
+
repetition.comment,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
252
289
|
return;
|
|
253
290
|
}
|
|
254
291
|
|
|
@@ -260,6 +260,134 @@ describe('RevertOrphanedPreparationUseCase', () => {
|
|
|
260
260
|
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
|
|
261
261
|
});
|
|
262
262
|
|
|
263
|
+
it('should transition to Failed Preparation when the declared next step agent is already assigned and the repetition threshold is met', async () => {
|
|
264
|
+
mockProject.agent = {
|
|
265
|
+
name: 'agent',
|
|
266
|
+
fieldId: 'agent-field-id',
|
|
267
|
+
options: [
|
|
268
|
+
{
|
|
269
|
+
id: 'agent-option-developer',
|
|
270
|
+
name: 'developer',
|
|
271
|
+
color: 'GRAY',
|
|
272
|
+
description: '',
|
|
273
|
+
},
|
|
274
|
+
],
|
|
275
|
+
};
|
|
276
|
+
const stuckIssue = createMockIssue({
|
|
277
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
278
|
+
status: 'Preparation',
|
|
279
|
+
agent: 'developer',
|
|
280
|
+
});
|
|
281
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
282
|
+
project: mockProject,
|
|
283
|
+
issues: [stuckIssue],
|
|
284
|
+
cacheUsed: false,
|
|
285
|
+
});
|
|
286
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
287
|
+
stdout: '',
|
|
288
|
+
stderr: '',
|
|
289
|
+
exitCode: 1,
|
|
290
|
+
});
|
|
291
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
292
|
+
{
|
|
293
|
+
author: 'bot',
|
|
294
|
+
content:
|
|
295
|
+
'From: :robot: triager\n\n```json\n{"nextStep":null,"nextStepAgent":"developer"}\n```',
|
|
296
|
+
createdAt: new Date('2024-01-02T00:00:00Z'),
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
author: 'bot',
|
|
300
|
+
content: 'Next step agent dispatch repeated: developer',
|
|
301
|
+
createdAt: new Date('2024-01-02T01:00:00Z'),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
author: 'bot',
|
|
305
|
+
content: 'Next step agent dispatch repeated: developer',
|
|
306
|
+
createdAt: new Date('2024-01-02T02:00:00Z'),
|
|
307
|
+
},
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
await useCase.run({
|
|
311
|
+
projectUrl: 'https://github.com/user/repo',
|
|
312
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
313
|
+
thresholdForAutoReject: 3,
|
|
314
|
+
allowedIssueAuthors: ['bot'],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
|
|
318
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
319
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('5');
|
|
320
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
321
|
+
stuckIssue,
|
|
322
|
+
expect.stringContaining(
|
|
323
|
+
'Failed to receive a report from the dispatched agent for 3 times',
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it('should record the repeated dispatch even when the project has no Failed Preparation status', async () => {
|
|
329
|
+
mockProject.status.statuses = mockProject.status.statuses.filter(
|
|
330
|
+
(status) => status.name !== 'Failed Preparation',
|
|
331
|
+
);
|
|
332
|
+
mockProject.agent = {
|
|
333
|
+
name: 'agent',
|
|
334
|
+
fieldId: 'agent-field-id',
|
|
335
|
+
options: [
|
|
336
|
+
{
|
|
337
|
+
id: 'agent-option-developer',
|
|
338
|
+
name: 'developer',
|
|
339
|
+
color: 'GRAY',
|
|
340
|
+
description: '',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
const stuckIssue = createMockIssue({
|
|
345
|
+
url: 'https://github.com/user/repo/issues/10',
|
|
346
|
+
status: 'Preparation',
|
|
347
|
+
agent: 'developer',
|
|
348
|
+
});
|
|
349
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
350
|
+
project: mockProject,
|
|
351
|
+
issues: [stuckIssue],
|
|
352
|
+
cacheUsed: false,
|
|
353
|
+
});
|
|
354
|
+
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
355
|
+
stdout: '',
|
|
356
|
+
stderr: '',
|
|
357
|
+
exitCode: 1,
|
|
358
|
+
});
|
|
359
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
360
|
+
{
|
|
361
|
+
author: 'bot',
|
|
362
|
+
content:
|
|
363
|
+
'From: :robot: triager\n\n```json\n{"nextStep":null,"nextStepAgent":"developer"}\n```',
|
|
364
|
+
createdAt: new Date('2024-01-02T00:00:00Z'),
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
author: 'bot',
|
|
368
|
+
content: 'Next step agent dispatch repeated: developer',
|
|
369
|
+
createdAt: new Date('2024-01-02T01:00:00Z'),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
author: 'bot',
|
|
373
|
+
content: 'Next step agent dispatch repeated: developer',
|
|
374
|
+
createdAt: new Date('2024-01-02T02:00:00Z'),
|
|
375
|
+
},
|
|
376
|
+
]);
|
|
377
|
+
|
|
378
|
+
await useCase.run({
|
|
379
|
+
projectUrl: 'https://github.com/user/repo',
|
|
380
|
+
preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
|
|
381
|
+
thresholdForAutoReject: 3,
|
|
382
|
+
allowedIssueAuthors: ['bot'],
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
386
|
+
stuckIssue,
|
|
387
|
+
expect.stringContaining('Next step agent dispatch repeated: developer'),
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
|
|
263
391
|
it('should ignore a next step agent designated by an author outside the allowed issue authors', async () => {
|
|
264
392
|
mockProject.agent = {
|
|
265
393
|
name: 'agent',
|
|
@@ -26,6 +26,7 @@ import { extractNextStepAgent } from './extractNextStepAgent';
|
|
|
26
26
|
import { findLastAgentReport } from './findLastAgentReport';
|
|
27
27
|
import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
|
|
28
28
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
29
|
+
import { resolveNextStepAgentDispatchRepetition } from './resolveNextStepAgentDispatchRepetition';
|
|
29
30
|
|
|
30
31
|
const ORPHANED_PREPARATION_REJECTION_DETAIL = 'ORPHANED_PREPARATION';
|
|
31
32
|
|
|
@@ -121,11 +122,44 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
121
122
|
if (!isStillInPreparation) {
|
|
122
123
|
continue;
|
|
123
124
|
}
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
const lastAgentReport = findLastAgentReport(comments, (author) =>
|
|
126
|
+
isAuthorAuthorizedForAutoStatusCheck(
|
|
127
|
+
author,
|
|
128
|
+
params.allowedIssueAuthors,
|
|
129
|
+
),
|
|
127
130
|
);
|
|
131
|
+
const nextStepAgent = lastAgentReport
|
|
132
|
+
? extractNextStepAgent(lastAgentReport.content)
|
|
133
|
+
: null;
|
|
128
134
|
if (nextStepAgent !== null) {
|
|
135
|
+
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
136
|
+
agentFieldValue: issue.agent,
|
|
137
|
+
nextStepAgent,
|
|
138
|
+
commentsAfterLastAgentReport: lastAgentReport
|
|
139
|
+
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
140
|
+
: [],
|
|
141
|
+
isTrustedAuthor: (author) =>
|
|
142
|
+
isAuthorAuthorizedForAutoStatusCheck(
|
|
143
|
+
author,
|
|
144
|
+
params.allowedIssueAuthors,
|
|
145
|
+
),
|
|
146
|
+
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
147
|
+
});
|
|
148
|
+
if (
|
|
149
|
+
repetition.type === 'escalateToFailedPreparation' &&
|
|
150
|
+
failedPreparationStatusOption
|
|
151
|
+
) {
|
|
152
|
+
await this.issueRepository.updateStatus(
|
|
153
|
+
project,
|
|
154
|
+
issue,
|
|
155
|
+
failedPreparationStatusOption.id,
|
|
156
|
+
);
|
|
157
|
+
await this.issueCommentRepository.createComment(
|
|
158
|
+
issue,
|
|
159
|
+
repetition.comment,
|
|
160
|
+
);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
129
163
|
const agentOptionId = await ensureAgentOptionAndGetId(
|
|
130
164
|
this.projectRepository,
|
|
131
165
|
project,
|
|
@@ -143,6 +177,12 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
143
177
|
issue,
|
|
144
178
|
awaitingWorkspaceStatusOption.id,
|
|
145
179
|
);
|
|
180
|
+
if (repetition.type !== 'notRepeated') {
|
|
181
|
+
await this.issueCommentRepository.createComment(
|
|
182
|
+
issue,
|
|
183
|
+
repetition.comment,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
146
186
|
continue;
|
|
147
187
|
}
|
|
148
188
|
if (outcome === 'returnToLabelSelectedAgent') {
|
|
@@ -346,18 +386,6 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
346
386
|
return [pr];
|
|
347
387
|
};
|
|
348
388
|
|
|
349
|
-
private resolveNextStepAgent = (
|
|
350
|
-
comments: Comment[],
|
|
351
|
-
allowedIssueAuthors: string[] | null | undefined,
|
|
352
|
-
): string | null => {
|
|
353
|
-
const lastAgentReport = findLastAgentReport(comments, (author) =>
|
|
354
|
-
isAuthorAuthorizedForAutoStatusCheck(author, allowedIssueAuthors),
|
|
355
|
-
);
|
|
356
|
-
return lastAgentReport
|
|
357
|
-
? extractNextStepAgent(lastAgentReport.content)
|
|
358
|
-
: null;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
389
|
private reportBodyHasNextStep = (body: string): boolean => {
|
|
362
390
|
const reportMatch = normalizeReportBody(body).match(
|
|
363
391
|
/```json\n([\s\S]*?)\n```/,
|
|
@@ -217,31 +217,27 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
217
217
|
expect(result['todo-by-agent'].items).toHaveLength(0);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
it('selects
|
|
220
|
+
it('selects all actionable items for triage regardless of story', () => {
|
|
221
221
|
const result = run([
|
|
222
222
|
makeIssue({ story: 'regular / NO STORY; SET STORY FIELD' }),
|
|
223
223
|
makeIssue({ story: 'no story please' }),
|
|
224
224
|
makeIssue({ story: 'Story Alpha' }),
|
|
225
225
|
makeIssue({ story: null }),
|
|
226
226
|
]);
|
|
227
|
-
expect(result.triage.items).toHaveLength(
|
|
227
|
+
expect(result.triage.items).toHaveLength(4);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
it('
|
|
230
|
+
it('excludes In Tmux by agent items from triage regardless of story', () => {
|
|
231
231
|
const result = run([
|
|
232
232
|
makeIssue({ story: 'no story', status: 'In Tmux by agent' }),
|
|
233
233
|
makeIssue({ story: 'no story', status: 'in tmux by agent' }),
|
|
234
234
|
makeIssue({ story: 'no story', status: 'Unread' }),
|
|
235
235
|
]);
|
|
236
236
|
const statuses = result.triage.items.map((item) => item.status);
|
|
237
|
-
expect(statuses).toEqual([
|
|
238
|
-
'In Tmux by agent',
|
|
239
|
-
'in tmux by agent',
|
|
240
|
-
'Unread',
|
|
241
|
-
]);
|
|
237
|
+
expect(statuses).toEqual(['Unread']);
|
|
242
238
|
});
|
|
243
239
|
|
|
244
|
-
it('
|
|
240
|
+
it('excludes items with reactivation triggers from triage', () => {
|
|
245
241
|
const result = run([
|
|
246
242
|
makeIssue({
|
|
247
243
|
story: 'no story',
|
|
@@ -253,42 +249,42 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
253
249
|
}),
|
|
254
250
|
makeIssue({ story: 'no story', nextActionHour: 9 }),
|
|
255
251
|
]);
|
|
256
|
-
expect(result.triage.items).toHaveLength(
|
|
252
|
+
expect(result.triage.items).toHaveLength(0);
|
|
257
253
|
});
|
|
258
254
|
|
|
259
|
-
it('
|
|
255
|
+
it('includes pull requests in triage when assigned, excludes unassigned and closed', () => {
|
|
260
256
|
const result = run([
|
|
261
257
|
makeIssue({ story: 'no story', isPr: true }),
|
|
262
258
|
makeIssue({ story: 'no story', assignees: ['someone-else'] }),
|
|
263
259
|
makeIssue({ story: 'no story', isClosed: true }),
|
|
264
260
|
]);
|
|
265
|
-
expect(result.triage.items).toHaveLength(
|
|
261
|
+
expect(result.triage.items).toHaveLength(1);
|
|
266
262
|
});
|
|
267
263
|
|
|
268
|
-
it('
|
|
264
|
+
it('includes all actionable items in triage including those with a real story or null story', () => {
|
|
269
265
|
const result = run([
|
|
270
266
|
makeIssue({ story: null, status: 'Awaiting Workspace' }),
|
|
271
267
|
makeIssue({ story: null, status: null }),
|
|
272
268
|
makeIssue({ story: 'regular / NO STORY' }),
|
|
273
269
|
makeIssue({ story: 'Story Alpha' }),
|
|
274
270
|
]);
|
|
275
|
-
expect(result.triage.items).toHaveLength(
|
|
271
|
+
expect(result.triage.items).toHaveLength(4);
|
|
276
272
|
});
|
|
277
273
|
|
|
278
|
-
it('
|
|
274
|
+
it('includes pull requests in triage, excludes unassigned and closed items with null story', () => {
|
|
279
275
|
const result = run([
|
|
280
276
|
makeIssue({ story: null, isPr: true }),
|
|
281
277
|
makeIssue({ story: null, assignees: ['someone-else'] }),
|
|
282
278
|
makeIssue({ story: null, isClosed: true }),
|
|
283
279
|
]);
|
|
284
|
-
expect(result.triage.items).toHaveLength(
|
|
280
|
+
expect(result.triage.items).toHaveLength(1);
|
|
285
281
|
});
|
|
286
282
|
|
|
287
|
-
it('
|
|
283
|
+
it('shows all actionable items in triage whatever status they carry, excluding In Tmux by agent', () => {
|
|
288
284
|
const result = run([
|
|
289
285
|
makeIssue({ story: 'no story', status: 'Unread' }),
|
|
290
286
|
makeIssue({ story: 'no story', status: 'Awaiting Workspace' }),
|
|
291
|
-
makeIssue({ story: 'no story', status:
|
|
287
|
+
makeIssue({ story: 'no story', status: 'In Tmux by agent' }),
|
|
292
288
|
makeIssue({ story: 'no story', status: 'Preparation' }),
|
|
293
289
|
makeIssue({ story: 'no story', status: 'Failed Preparation' }),
|
|
294
290
|
makeIssue({ story: 'no story', status: 'Awaiting Quality Check' }),
|
|
@@ -301,7 +297,6 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
301
297
|
expect(result.triage.items.map((item) => item.status)).toEqual([
|
|
302
298
|
'Unread',
|
|
303
299
|
'Awaiting Workspace',
|
|
304
|
-
null,
|
|
305
300
|
'Preparation',
|
|
306
301
|
'Failed Preparation',
|
|
307
302
|
'Awaiting Quality Check',
|
|
@@ -313,11 +308,11 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
313
308
|
]);
|
|
314
309
|
});
|
|
315
310
|
|
|
316
|
-
it('
|
|
311
|
+
it('shows items with any story in triage at every status except In Tmux by agent', () => {
|
|
317
312
|
const result = run([
|
|
318
313
|
makeIssue({ story: 'Story Alpha', status: 'Unread' }),
|
|
319
314
|
makeIssue({ story: 'Story Alpha', status: 'Awaiting Workspace' }),
|
|
320
|
-
makeIssue({ story: 'Story Alpha', status:
|
|
315
|
+
makeIssue({ story: 'Story Alpha', status: 'In Tmux by agent' }),
|
|
321
316
|
makeIssue({ story: 'Story Alpha', status: 'Preparation' }),
|
|
322
317
|
makeIssue({ story: 'Story Alpha', status: 'Failed Preparation' }),
|
|
323
318
|
makeIssue({ story: 'Story Alpha', status: 'Awaiting Quality Check' }),
|
|
@@ -327,7 +322,7 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
327
322
|
makeIssue({ story: 'Story Alpha', status: 'Done' }),
|
|
328
323
|
makeIssue({ story: 'Story Alpha', status: 'Icebox' }),
|
|
329
324
|
]);
|
|
330
|
-
expect(result.triage.items).toHaveLength(
|
|
325
|
+
expect(result.triage.items).toHaveLength(10);
|
|
331
326
|
});
|
|
332
327
|
|
|
333
328
|
it('keeps a no-story item whose status is written in a different case', () => {
|
|
@@ -440,15 +435,15 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
440
435
|
expect(allTabItems(result)).toHaveLength(1);
|
|
441
436
|
});
|
|
442
437
|
|
|
443
|
-
it('
|
|
438
|
+
it('hides a no-story In Tmux by agent issue from triage and all other tabs', () => {
|
|
444
439
|
const result = run([
|
|
445
440
|
makeIssue({
|
|
446
441
|
story: 'no story',
|
|
447
442
|
status: 'In Tmux by agent',
|
|
448
443
|
}),
|
|
449
444
|
]);
|
|
450
|
-
expect(result.triage.items).toHaveLength(
|
|
451
|
-
expect(allTabItems(result)).toHaveLength(
|
|
445
|
+
expect(result.triage.items).toHaveLength(0);
|
|
446
|
+
expect(allTabItems(result)).toHaveLength(0);
|
|
452
447
|
});
|
|
453
448
|
|
|
454
449
|
it('hides In Tmux by agent case-insensitively from every tab other than the workflow blocker tab', () => {
|
|
@@ -477,7 +472,7 @@ describe('GenerateConsoleListsUseCase', () => {
|
|
|
477
472
|
expect(
|
|
478
473
|
result['workflow-blocker'].items.map((item) => item.status).sort(),
|
|
479
474
|
).toEqual(['In Tmux by agent', 'In Tmux by human']);
|
|
480
|
-
expect(result.triage.items.map((item) => item.number)).toEqual([3]);
|
|
475
|
+
expect(result.triage.items.map((item) => item.number)).toEqual([3, 2]);
|
|
481
476
|
expect(
|
|
482
477
|
result.prs.items
|
|
483
478
|
.concat(result.triage.items)
|
|
@@ -219,14 +219,12 @@ export class GenerateConsoleListsUseCase {
|
|
|
219
219
|
storyOrder,
|
|
220
220
|
storyColors: this.buildStoryColorsString(storyOptions),
|
|
221
221
|
items: this.sortByStoryOrder(
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
issue,
|
|
227
|
-
relatedOpenPullRequestUrlsByIssueUrl.get(issue.url) ?? [],
|
|
228
|
-
),
|
|
222
|
+
actionableIssues.map((issue) =>
|
|
223
|
+
this.projectItem(
|
|
224
|
+
issue,
|
|
225
|
+
relatedOpenPullRequestUrlsByIssueUrl.get(issue.url) ?? [],
|
|
229
226
|
),
|
|
227
|
+
),
|
|
230
228
|
storyOrder,
|
|
231
229
|
),
|
|
232
230
|
},
|
|
@@ -241,12 +239,6 @@ export class GenerateConsoleListsUseCase {
|
|
|
241
239
|
};
|
|
242
240
|
};
|
|
243
241
|
|
|
244
|
-
private needsStory = (issue: Issue, assigneeLogin: string): boolean =>
|
|
245
|
-
issue.isClosed === false &&
|
|
246
|
-
issue.isPr === false &&
|
|
247
|
-
issue.assignees.includes(assigneeLogin) &&
|
|
248
|
-
(issue.story === null || issue.story.toLowerCase().includes('no story'));
|
|
249
|
-
|
|
250
242
|
private isActionable = (issue: Issue, assigneeLogin: string): boolean =>
|
|
251
243
|
issue.isClosed === false &&
|
|
252
244
|
issue.assignees.includes(assigneeLogin) &&
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD,
|
|
3
|
+
resolveNextStepAgentDispatchRepetition,
|
|
4
|
+
} from './resolveNextStepAgentDispatchRepetition';
|
|
5
|
+
|
|
6
|
+
const trustAll = (): boolean => true;
|
|
7
|
+
|
|
8
|
+
describe('resolveNextStepAgentDispatchRepetition', () => {
|
|
9
|
+
it('returns notRepeated when the agent field holds no value', () => {
|
|
10
|
+
expect(
|
|
11
|
+
resolveNextStepAgentDispatchRepetition({
|
|
12
|
+
agentFieldValue: null,
|
|
13
|
+
nextStepAgent: 'accounting',
|
|
14
|
+
commentsAfterLastAgentReport: [],
|
|
15
|
+
isTrustedAuthor: trustAll,
|
|
16
|
+
thresholdForAutoReject: 3,
|
|
17
|
+
}),
|
|
18
|
+
).toEqual({ type: 'notRepeated' });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('returns notRepeated when the agent field holds another agent', () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveNextStepAgentDispatchRepetition({
|
|
24
|
+
agentFieldValue: 'triager',
|
|
25
|
+
nextStepAgent: 'accounting',
|
|
26
|
+
commentsAfterLastAgentReport: [],
|
|
27
|
+
isTrustedAuthor: trustAll,
|
|
28
|
+
thresholdForAutoReject: 3,
|
|
29
|
+
}),
|
|
30
|
+
).toEqual({ type: 'notRepeated' });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns dispatchAgain with the attempt count when the declared agent is already assigned', () => {
|
|
34
|
+
const result = resolveNextStepAgentDispatchRepetition({
|
|
35
|
+
agentFieldValue: 'accounting',
|
|
36
|
+
nextStepAgent: 'accounting',
|
|
37
|
+
commentsAfterLastAgentReport: [],
|
|
38
|
+
isTrustedAuthor: trustAll,
|
|
39
|
+
thresholdForAutoReject: 3,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(result.type).toBe('dispatchAgain');
|
|
43
|
+
expect(result.type === 'dispatchAgain' ? result.comment : '').toContain(
|
|
44
|
+
'(1/3)',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('matches the agent field and the declared agent ignoring case, spaces and hyphens', () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveNextStepAgentDispatchRepetition({
|
|
51
|
+
agentFieldValue: 'PR Reviewer',
|
|
52
|
+
nextStepAgent: 'pr-reviewer',
|
|
53
|
+
commentsAfterLastAgentReport: [],
|
|
54
|
+
isTrustedAuthor: trustAll,
|
|
55
|
+
thresholdForAutoReject: 3,
|
|
56
|
+
}).type,
|
|
57
|
+
).toBe('dispatchAgain');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('counts only repetition comments written by a trusted author', () => {
|
|
61
|
+
const result = resolveNextStepAgentDispatchRepetition({
|
|
62
|
+
agentFieldValue: 'accounting',
|
|
63
|
+
nextStepAgent: 'accounting',
|
|
64
|
+
commentsAfterLastAgentReport: [
|
|
65
|
+
{
|
|
66
|
+
author: 'stranger',
|
|
67
|
+
content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} accounting`,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
author: 'stranger',
|
|
71
|
+
content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} accounting`,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
isTrustedAuthor: (author: string): boolean => author === 'bot',
|
|
75
|
+
thresholdForAutoReject: 3,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(result.type).toBe('dispatchAgain');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('escalates once the repetition count reaches the auto reject threshold', () => {
|
|
82
|
+
const repeated = {
|
|
83
|
+
author: 'bot',
|
|
84
|
+
content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} accounting`,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const result = resolveNextStepAgentDispatchRepetition({
|
|
88
|
+
agentFieldValue: 'accounting',
|
|
89
|
+
nextStepAgent: 'accounting',
|
|
90
|
+
commentsAfterLastAgentReport: [repeated, repeated],
|
|
91
|
+
isTrustedAuthor: trustAll,
|
|
92
|
+
thresholdForAutoReject: 3,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(result.type).toBe('escalateToFailedPreparation');
|
|
96
|
+
});
|
|
97
|
+
});
|