github-issue-tower-defence-management 1.188.0 → 1.189.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -0
- package/bin/adapter/entry-points/cli/index.js +20 -12
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CLHvrafO.css +2 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CZ1NNzIm.js +86 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -4
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +3 -3
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js +13 -1
- package/bin/domain/usecases/TriagerApprovalDispatchUseCase.js.map +1 -1
- package/bin/domain/usecases/isHumanComment.js +21 -0
- package/bin/domain/usecases/isHumanComment.js.map +1 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js +5 -0
- package/bin/domain/usecases/nextStepAgentDispatchRepeatedMessage.js.map +1 -0
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js +62 -12
- package/bin/domain/usecases/resolveNextStepAgentDispatchRepetition.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +2 -0
- package/src/adapter/entry-points/cli/index.ts +39 -16
- package/src/adapter/entry-points/cli/projectConfig.ts +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleCopyStoryNameButton.stories.tsx +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleCopyStoryNameButton.test.tsx +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleCopyStoryNameButton.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +26 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +2 -0
- package/src/adapter/entry-points/console/ui/src/index.css +14 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-CLHvrafO.css +2 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-CZ1NNzIm.js +86 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +98 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +9 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +9 -4
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +88 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +17 -2
- package/src/domain/usecases/isHumanComment.test.ts +90 -0
- package/src/domain/usecases/isHumanComment.ts +23 -0
- package/src/domain/usecases/nextStepAgentDispatchRepeatedMessage.ts +2 -0
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.test.ts +312 -72
- package/src/domain/usecases/resolveNextStepAgentDispatchRepetition.ts +103 -19
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
- package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/TriagerApprovalDispatchUseCase.d.ts.map +1 -1
- package/types/domain/usecases/isHumanComment.d.ts +5 -0
- package/types/domain/usecases/isHumanComment.d.ts.map +1 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts +2 -0
- package/types/domain/usecases/nextStepAgentDispatchRepeatedMessage.d.ts.map +1 -0
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts +5 -2
- package/types/domain/usecases/resolveNextStepAgentDispatchRepetition.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +0 -86
- package/bin/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +0 -2
- package/src/adapter/entry-points/console/ui-dist/assets/index-CA6c7Pfz.js +0 -86
- package/src/adapter/entry-points/console/ui-dist/assets/index-D1xvwesF.css +0 -2
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>TDPM Console</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CZ1NNzIm.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CLHvrafO.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -301,6 +301,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
301
301
|
input.claudeCodeOauthTokenListJsonPath,
|
|
302
302
|
thresholdForAutoReject:
|
|
303
303
|
readmeConfig.thresholdForAutoReject ?? input.thresholdForAutoReject,
|
|
304
|
+
thresholdForDispatchLoop:
|
|
305
|
+
readmeConfig.thresholdForDispatchLoop ?? input.thresholdForDispatchLoop,
|
|
304
306
|
developerAgentName:
|
|
305
307
|
readmeConfig.developerAgentName ?? input.developerAgentName ?? null,
|
|
306
308
|
startPreparation: input.startPreparation
|
|
@@ -188,6 +188,7 @@ export class HandleScheduledEventUseCase {
|
|
|
188
188
|
labelsAsLlmAgentName?: string[] | null;
|
|
189
189
|
} | null;
|
|
190
190
|
thresholdForAutoReject?: number;
|
|
191
|
+
thresholdForDispatchLoop?: number;
|
|
191
192
|
createTaskFromStoryBodyCheckboxEnabled?: boolean;
|
|
192
193
|
queryToAddProjectEnabled?: boolean;
|
|
193
194
|
queryToAddProject?: string | null;
|
|
@@ -468,6 +469,7 @@ ${JSON.stringify(e)}
|
|
|
468
469
|
preparationProcessCheckCommand:
|
|
469
470
|
input.startPreparation.preparationProcessCheckCommand,
|
|
470
471
|
thresholdForAutoReject: input.thresholdForAutoReject ?? 3,
|
|
472
|
+
thresholdForDispatchLoop: input.thresholdForDispatchLoop,
|
|
471
473
|
awLogDirectoryPath: input.startPreparation.awLogDirectoryPath,
|
|
472
474
|
awLogStaleThresholdMinutes:
|
|
473
475
|
input.startPreparation.awLogStaleThresholdMinutes,
|
|
@@ -1079,6 +1079,104 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
1079
1079
|
);
|
|
1080
1080
|
});
|
|
1081
1081
|
|
|
1082
|
+
it('should escalate to Failed Preparation when two agents keep naming each other and each one reports every round', async () => {
|
|
1083
|
+
const issue = createMockIssue({
|
|
1084
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1085
|
+
status: 'Preparation',
|
|
1086
|
+
agent: 'systems-analyst',
|
|
1087
|
+
});
|
|
1088
|
+
const namesReviewer = createMockComment({
|
|
1089
|
+
content:
|
|
1090
|
+
'From: :robot: systems-analyst\n```json\n{"nextStepAgent": "system-design-reviewer"}\n```',
|
|
1091
|
+
});
|
|
1092
|
+
const namesAnalyst = createMockComment({
|
|
1093
|
+
content:
|
|
1094
|
+
'From: :robot: system-design-reviewer\n```json\n{"nextStepAgent": "systems-analyst"}\n```',
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1098
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1099
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1100
|
+
namesReviewer,
|
|
1101
|
+
namesAnalyst,
|
|
1102
|
+
namesReviewer,
|
|
1103
|
+
namesAnalyst,
|
|
1104
|
+
namesReviewer,
|
|
1105
|
+
]);
|
|
1106
|
+
|
|
1107
|
+
await useCase.run({
|
|
1108
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1109
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1110
|
+
thresholdForAutoReject: 3,
|
|
1111
|
+
thresholdForDispatchLoop: 3,
|
|
1112
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1113
|
+
allowedIssueAuthors: null,
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
1117
|
+
mockProject,
|
|
1118
|
+
expect.anything(),
|
|
1119
|
+
'failed-preparation-id',
|
|
1120
|
+
);
|
|
1121
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1122
|
+
expect.anything(),
|
|
1123
|
+
expect.stringContaining(
|
|
1124
|
+
'dispatched 3 times since the last human comment',
|
|
1125
|
+
),
|
|
1126
|
+
);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
it('should keep dispatching when a human comment separates the repeated dispatches', async () => {
|
|
1130
|
+
const issue = createMockIssue({
|
|
1131
|
+
url: 'https://github.com/user/repo/issues/1',
|
|
1132
|
+
status: 'Preparation',
|
|
1133
|
+
agent: 'systems-analyst',
|
|
1134
|
+
});
|
|
1135
|
+
const namesReviewer = createMockComment({
|
|
1136
|
+
content:
|
|
1137
|
+
'From: :robot: systems-analyst\n```json\n{"nextStepAgent": "system-design-reviewer"}\n```',
|
|
1138
|
+
});
|
|
1139
|
+
const namesAnalyst = createMockComment({
|
|
1140
|
+
content:
|
|
1141
|
+
'From: :robot: system-design-reviewer\n```json\n{"nextStepAgent": "systems-analyst"}\n```',
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
1145
|
+
mockIssueRepository.get.mockResolvedValue(issue);
|
|
1146
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
|
|
1147
|
+
namesReviewer,
|
|
1148
|
+
namesAnalyst,
|
|
1149
|
+
createMockComment({ content: 'Please take the second option.' }),
|
|
1150
|
+
namesReviewer,
|
|
1151
|
+
namesAnalyst,
|
|
1152
|
+
namesReviewer,
|
|
1153
|
+
]);
|
|
1154
|
+
|
|
1155
|
+
await useCase.run({
|
|
1156
|
+
projectUrl: 'https://github.com/users/user/projects/1',
|
|
1157
|
+
issueUrl: 'https://github.com/user/repo/issues/1',
|
|
1158
|
+
thresholdForAutoReject: 3,
|
|
1159
|
+
thresholdForDispatchLoop: 3,
|
|
1160
|
+
workflowBlockerResolvedWebhookUrl: null,
|
|
1161
|
+
allowedIssueAuthors: null,
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
expect(mockIssueRepository.updateStatus).not.toHaveBeenCalledWith(
|
|
1165
|
+
mockProject,
|
|
1166
|
+
expect.anything(),
|
|
1167
|
+
'failed-preparation-id',
|
|
1168
|
+
);
|
|
1169
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
1170
|
+
mockProject,
|
|
1171
|
+
expect.anything(),
|
|
1172
|
+
'awaiting-workspace-id',
|
|
1173
|
+
);
|
|
1174
|
+
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
1175
|
+
expect.anything(),
|
|
1176
|
+
expect.stringContaining('(2/3)'),
|
|
1177
|
+
);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1082
1180
|
it('should not modify labels when nextStepAgent is specified', async () => {
|
|
1083
1181
|
const issue = createMockIssue({
|
|
1084
1182
|
url: 'https://github.com/user/repo/issues/1',
|
|
@@ -35,7 +35,10 @@ import {
|
|
|
35
35
|
issueReactivationTriggerStartOfTomorrow,
|
|
36
36
|
} from './issueReactivationTriggerIsPending';
|
|
37
37
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
40
|
+
resolveNextStepAgentDispatchRepetition,
|
|
41
|
+
} from './resolveNextStepAgentDispatchRepetition';
|
|
39
42
|
|
|
40
43
|
export class IssueNotFoundError extends Error {
|
|
41
44
|
constructor(issueUrl: string) {
|
|
@@ -106,6 +109,7 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
106
109
|
projectUrl: string;
|
|
107
110
|
issueUrl: string;
|
|
108
111
|
thresholdForAutoReject: number;
|
|
112
|
+
thresholdForDispatchLoop?: number;
|
|
109
113
|
workflowBlockerResolvedWebhookUrl: string | null;
|
|
110
114
|
allowedIssueAuthors?: string[] | null;
|
|
111
115
|
labelsAsLlmAgentName?: string[] | null;
|
|
@@ -246,16 +250,16 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
246
250
|
const nextStepAgent = lastAgentReport
|
|
247
251
|
? extractNextStepAgent(lastAgentReport.content)
|
|
248
252
|
: null;
|
|
249
|
-
const commentsAfterLastAgentReport = lastAgentReport
|
|
250
|
-
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
251
|
-
: [];
|
|
252
253
|
if (nextStepAgent !== null) {
|
|
253
254
|
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
254
255
|
agentFieldValue: issue.agent,
|
|
255
256
|
nextStepAgent,
|
|
256
|
-
|
|
257
|
+
comments,
|
|
257
258
|
isTrustedAuthor,
|
|
258
259
|
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
260
|
+
thresholdForDispatchLoop:
|
|
261
|
+
params.thresholdForDispatchLoop ??
|
|
262
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
259
263
|
});
|
|
260
264
|
if (repetition.type === 'escalateToFailedPreparation') {
|
|
261
265
|
issue.status = FAILED_PREPARATION_STATUS_NAME;
|
|
@@ -27,7 +27,10 @@ import { findLastAgentReport } from './findLastAgentReport';
|
|
|
27
27
|
import { isAgentReportBody } from './isAgentReportBody';
|
|
28
28
|
import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
|
|
29
29
|
import { normalizeReportBody } from './normalizeReportBody';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
32
|
+
resolveNextStepAgentDispatchRepetition,
|
|
33
|
+
} from './resolveNextStepAgentDispatchRepetition';
|
|
31
34
|
|
|
32
35
|
const ORPHANED_PREPARATION_REJECTION_DETAIL = 'ORPHANED_PREPARATION';
|
|
33
36
|
|
|
@@ -64,6 +67,7 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
64
67
|
projectUrl: string;
|
|
65
68
|
preparationProcessCheckCommand: string;
|
|
66
69
|
thresholdForAutoReject: number;
|
|
70
|
+
thresholdForDispatchLoop?: number;
|
|
67
71
|
awLogDirectoryPath?: string;
|
|
68
72
|
awLogStaleThresholdMinutes?: number;
|
|
69
73
|
awaitingQualityCheckStatus?: string | null;
|
|
@@ -136,15 +140,16 @@ export class RevertOrphanedPreparationUseCase {
|
|
|
136
140
|
const repetition = resolveNextStepAgentDispatchRepetition({
|
|
137
141
|
agentFieldValue: issue.agent,
|
|
138
142
|
nextStepAgent,
|
|
139
|
-
|
|
140
|
-
? comments.slice(comments.indexOf(lastAgentReport) + 1)
|
|
141
|
-
: [],
|
|
143
|
+
comments,
|
|
142
144
|
isTrustedAuthor: (author) =>
|
|
143
145
|
isAuthorAuthorizedForAutoStatusCheck(
|
|
144
146
|
author,
|
|
145
147
|
params.allowedIssueAuthors,
|
|
146
148
|
),
|
|
147
149
|
thresholdForAutoReject: params.thresholdForAutoReject,
|
|
150
|
+
thresholdForDispatchLoop:
|
|
151
|
+
params.thresholdForDispatchLoop ??
|
|
152
|
+
DEFAULT_THRESHOLD_FOR_DISPATCH_LOOP,
|
|
148
153
|
});
|
|
149
154
|
if (
|
|
150
155
|
repetition.type === 'escalateToFailedPreparation' &&
|
|
@@ -838,6 +838,94 @@ describe('TriagerApprovalDispatchUseCase', () => {
|
|
|
838
838
|
);
|
|
839
839
|
});
|
|
840
840
|
|
|
841
|
+
it('should skip a candidate issue when getCommentsFromIssue throws and continue processing the remaining issues', async () => {
|
|
842
|
+
const failingIssue = createMockIssue({
|
|
843
|
+
number: 1,
|
|
844
|
+
url: 'https://github.com/owner/repo/issues/1',
|
|
845
|
+
itemId: 'item-1',
|
|
846
|
+
status: 'Awaiting Quality Check',
|
|
847
|
+
author: 'owner-user',
|
|
848
|
+
createdAt: new Date('2000-01-01T00:00:00Z'),
|
|
849
|
+
});
|
|
850
|
+
const successIssue = createMockIssue({
|
|
851
|
+
number: 2,
|
|
852
|
+
url: 'https://github.com/owner/repo/issues/2',
|
|
853
|
+
itemId: 'item-2',
|
|
854
|
+
status: 'Awaiting Quality Check',
|
|
855
|
+
author: 'owner-user',
|
|
856
|
+
createdAt: new Date('2000-01-02T00:00:00Z'),
|
|
857
|
+
});
|
|
858
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
859
|
+
project: mockProject,
|
|
860
|
+
issues: [failingIssue, successIssue],
|
|
861
|
+
cacheUsed: false,
|
|
862
|
+
});
|
|
863
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockImplementation(
|
|
864
|
+
async (issue) => {
|
|
865
|
+
if (issue.number === 1) {
|
|
866
|
+
throw new Error(
|
|
867
|
+
'Failed to fetch comments from GitHub REST API: 404 ',
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
return [
|
|
871
|
+
createComment(
|
|
872
|
+
'bot',
|
|
873
|
+
TRIAGER_PROPOSAL_COMMENT(
|
|
874
|
+
'developer',
|
|
875
|
+
'regular / workflow improvement',
|
|
876
|
+
false,
|
|
877
|
+
),
|
|
878
|
+
),
|
|
879
|
+
createComment('owner-user', 'ok'),
|
|
880
|
+
];
|
|
881
|
+
},
|
|
882
|
+
);
|
|
883
|
+
|
|
884
|
+
await useCase.run({
|
|
885
|
+
projectUrl: 'https://github.com/orgs/owner/projects/1',
|
|
886
|
+
allowedIssueAuthors: ['owner-user'],
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
expect(
|
|
890
|
+
mockIssueCommentRepository.getCommentsFromIssue,
|
|
891
|
+
).toHaveBeenCalledTimes(2);
|
|
892
|
+
expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledTimes(1);
|
|
893
|
+
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
894
|
+
mockProject,
|
|
895
|
+
successIssue,
|
|
896
|
+
'awaiting-workspace-id',
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
it('should propagate non-404 errors from getCommentsFromIssue', async () => {
|
|
901
|
+
const issue = createMockIssue({
|
|
902
|
+
number: 1,
|
|
903
|
+
url: 'https://github.com/owner/repo/issues/1',
|
|
904
|
+
itemId: 'item-1',
|
|
905
|
+
status: 'Awaiting Quality Check',
|
|
906
|
+
author: 'owner-user',
|
|
907
|
+
createdAt: new Date('2000-01-01T00:00:00Z'),
|
|
908
|
+
});
|
|
909
|
+
mockIssueRepository.getAllIssues.mockResolvedValue({
|
|
910
|
+
project: mockProject,
|
|
911
|
+
issues: [issue],
|
|
912
|
+
cacheUsed: false,
|
|
913
|
+
});
|
|
914
|
+
const authError = new Error(
|
|
915
|
+
'Failed to fetch comments from GitHub REST API: 401 Unauthorized',
|
|
916
|
+
);
|
|
917
|
+
mockIssueCommentRepository.getCommentsFromIssue.mockRejectedValue(
|
|
918
|
+
authError,
|
|
919
|
+
);
|
|
920
|
+
|
|
921
|
+
await expect(
|
|
922
|
+
useCase.run({
|
|
923
|
+
projectUrl: 'https://github.com/orgs/owner/projects/1',
|
|
924
|
+
allowedIssueAuthors: ['owner-user'],
|
|
925
|
+
}),
|
|
926
|
+
).rejects.toThrow(authError);
|
|
927
|
+
});
|
|
928
|
+
|
|
841
929
|
it('should use the last triager proposal when multiple proposals exist', async () => {
|
|
842
930
|
const issue = createMockIssue({
|
|
843
931
|
status: 'Awaiting Quality Check',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
2
2
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
3
|
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
|
+
import { Comment } from '../entities/Comment';
|
|
4
5
|
import {
|
|
5
6
|
isAgentReportBody,
|
|
6
7
|
isAgentReportBodyFromAgent,
|
|
@@ -68,6 +69,9 @@ const parseTriagerProposalBlock = (
|
|
|
68
69
|
return null;
|
|
69
70
|
};
|
|
70
71
|
|
|
72
|
+
const isNotFoundError = (error: unknown): boolean =>
|
|
73
|
+
(error instanceof Error ? error.message : String(error)).includes('404');
|
|
74
|
+
|
|
71
75
|
const isApprovalComment = (
|
|
72
76
|
content: string,
|
|
73
77
|
author: string,
|
|
@@ -160,8 +164,19 @@ export class TriagerApprovalDispatchUseCase {
|
|
|
160
164
|
const issue =
|
|
161
165
|
candidateIssues[(windowStart + slot) % candidateIssues.length];
|
|
162
166
|
|
|
163
|
-
|
|
164
|
-
|
|
167
|
+
let comments: Comment[];
|
|
168
|
+
try {
|
|
169
|
+
comments =
|
|
170
|
+
await this.issueCommentRepository.getCommentsFromIssue(issue);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!isNotFoundError(error)) {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
console.warn(
|
|
176
|
+
`[TriagerApprovalDispatch] Failed to fetch comments, skipping issue for this cycle. issueUrl: ${issue.url} error: ${error instanceof Error ? error.message : String(error)}`,
|
|
177
|
+
);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
165
180
|
|
|
166
181
|
let firstProposalIndex = -1;
|
|
167
182
|
let proposal: TriagerProposal | null = null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
|
|
2
|
+
import { isHumanComment } from './isHumanComment';
|
|
3
|
+
import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
|
|
4
|
+
import { RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD } from './returnedToAwaitingWorkspaceMessage';
|
|
5
|
+
|
|
6
|
+
const trustAll = (): boolean => true;
|
|
7
|
+
|
|
8
|
+
describe('isHumanComment', () => {
|
|
9
|
+
it('treats a plain comment from a trusted author as human input', () => {
|
|
10
|
+
expect(
|
|
11
|
+
isHumanComment(
|
|
12
|
+
{ author: 'bot', content: 'Please use the second option.' },
|
|
13
|
+
trustAll,
|
|
14
|
+
),
|
|
15
|
+
).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('treats an agent report as machine generated', () => {
|
|
19
|
+
expect(
|
|
20
|
+
isHumanComment(
|
|
21
|
+
{
|
|
22
|
+
author: 'bot',
|
|
23
|
+
content: 'From: :robot: agent (model)\n\nReport body.',
|
|
24
|
+
},
|
|
25
|
+
trustAll,
|
|
26
|
+
),
|
|
27
|
+
).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('treats an agent report whose leading fenced block precedes the prefix as machine generated', () => {
|
|
31
|
+
expect(
|
|
32
|
+
isHumanComment(
|
|
33
|
+
{
|
|
34
|
+
author: 'bot',
|
|
35
|
+
content:
|
|
36
|
+
'```json\n{ "nextStepAgent": "developer" }\n```\n\nFrom: :robot: agent (model)\n\nReport body.',
|
|
37
|
+
},
|
|
38
|
+
trustAll,
|
|
39
|
+
),
|
|
40
|
+
).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('treats the dispatch repetition comment as machine generated', () => {
|
|
44
|
+
expect(
|
|
45
|
+
isHumanComment(
|
|
46
|
+
{
|
|
47
|
+
author: 'bot',
|
|
48
|
+
content: `${NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD} developer\n\nDispatching it again.`,
|
|
49
|
+
},
|
|
50
|
+
trustAll,
|
|
51
|
+
),
|
|
52
|
+
).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('treats an auto status check comment as machine generated', () => {
|
|
56
|
+
expect(
|
|
57
|
+
isHumanComment(
|
|
58
|
+
{
|
|
59
|
+
author: 'bot',
|
|
60
|
+
content: `${AUTO_STATUS_CHECK_MESSAGE_HEAD} PULL_REQUEST_NOT_FOUND`,
|
|
61
|
+
},
|
|
62
|
+
trustAll,
|
|
63
|
+
),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('treats the returned to awaiting workspace comment as machine generated', () => {
|
|
68
|
+
expect(
|
|
69
|
+
isHumanComment(
|
|
70
|
+
{
|
|
71
|
+
author: 'bot',
|
|
72
|
+
content: `${RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD}\nThe last report declared that this task needs no pull request.`,
|
|
73
|
+
},
|
|
74
|
+
trustAll,
|
|
75
|
+
),
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('treats any comment from an untrusted author as human input', () => {
|
|
80
|
+
expect(
|
|
81
|
+
isHumanComment(
|
|
82
|
+
{
|
|
83
|
+
author: 'stranger',
|
|
84
|
+
content: 'From: :robot: agent (model)\n\nImpersonated report.',
|
|
85
|
+
},
|
|
86
|
+
(author: string): boolean => author === 'bot',
|
|
87
|
+
),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AUTO_STATUS_CHECK_MESSAGE_HEAD } from './autoStatusCheckComments';
|
|
2
|
+
import { isAgentReportBody } from './isAgentReportBody';
|
|
3
|
+
import { NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD } from './nextStepAgentDispatchRepeatedMessage';
|
|
4
|
+
|
|
5
|
+
const MACHINE_GENERATED_COMMENT_HEADS = [
|
|
6
|
+
AUTO_STATUS_CHECK_MESSAGE_HEAD,
|
|
7
|
+
NEXT_STEP_AGENT_DISPATCH_REPEATED_MESSAGE_HEAD,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export const isHumanComment = (
|
|
11
|
+
comment: { author: string; content: string },
|
|
12
|
+
isTrustedAuthor: (author: string) => boolean,
|
|
13
|
+
): boolean => {
|
|
14
|
+
if (!isTrustedAuthor(comment.author)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (isAgentReportBody(comment.content)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return !MACHINE_GENERATED_COMMENT_HEADS.some((head) =>
|
|
21
|
+
comment.content.startsWith(head),
|
|
22
|
+
);
|
|
23
|
+
};
|