github-issue-tower-defence-management 2.6.1 → 2.7.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 +7 -0
- package/README.md +0 -2
- package/bin/adapter/entry-points/cli/index.js +0 -5
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +0 -3
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -3
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +4 -2
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -6
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/IssueRejectionEvaluator.js +0 -23
- package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +39 -69
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +3 -2
- package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +0 -43
- package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/autoStatusCheckComments.js +1 -11
- package/bin/domain/usecases/autoStatusCheckComments.js.map +1 -1
- package/bin/domain/usecases/extractNextStepAgentFromComments.js +14 -0
- package/bin/domain/usecases/extractNextStepAgentFromComments.js.map +1 -0
- package/bin/domain/usecases/triagerAgentName.js +7 -0
- package/bin/domain/usecases/triagerAgentName.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +0 -2
- package/src/adapter/entry-points/cli/index.ts +0 -9
- package/src/adapter/entry-points/cli/projectConfig.ts +0 -8
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -7
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +26 -4
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +5 -3
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +0 -28
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +0 -6
- package/src/domain/usecases/IssueRejectionEvaluator.test.ts +0 -119
- package/src/domain/usecases/IssueRejectionEvaluator.ts +0 -21
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +185 -327
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +68 -138
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +4 -4
- package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +11 -5
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +0 -313
- package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -102
- package/src/domain/usecases/autoStatusCheckComments.ts +0 -17
- package/src/domain/usecases/extractNextStepAgentFromComments.ts +13 -0
- package/src/domain/usecases/isHumanComment.test.ts +0 -13
- package/src/domain/usecases/triagerAgentName.ts +4 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +0 -1
- 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/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -3
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts +0 -1
- package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.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/autoStatusCheckComments.d.ts +0 -4
- package/types/domain/usecases/autoStatusCheckComments.d.ts.map +1 -1
- package/types/domain/usecases/extractNextStepAgentFromComments.d.ts +5 -0
- package/types/domain/usecases/extractNextStepAgentFromComments.d.ts.map +1 -0
- package/types/domain/usecases/triagerAgentName.d.ts +3 -0
- package/types/domain/usecases/triagerAgentName.d.ts.map +1 -0
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +0 -1051
- package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +0 -274
- package/src/domain/usecases/awaitingOwnerApprovalMessage.ts +0 -5
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +0 -162
- package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +0 -29
- package/src/domain/usecases/isWaitingForOwnerApproval.test.ts +0 -91
- package/src/domain/usecases/isWaitingForOwnerApproval.ts +0 -17
- package/src/domain/usecases/returnedToAwaitingWorkspaceMessage.ts +0 -5
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
|
|
2
|
-
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
3
|
-
import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
|
|
4
|
-
import { Comment } from '../entities/Comment';
|
|
5
|
-
import {
|
|
6
|
-
isAgentReportBody,
|
|
7
|
-
isAgentReportBodyFromAgent,
|
|
8
|
-
} from './isAgentReportBody';
|
|
9
|
-
import { ensureAgentOptionAndGetId } from './ensureAgentOptionAndGetId';
|
|
10
|
-
import { isAuthorAuthorizedForAutoStatusCheck } from './isAuthorAuthorizedForAutoStatusCheck';
|
|
11
|
-
import { isRecord } from './isRecord';
|
|
12
|
-
import {
|
|
13
|
-
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
14
|
-
AWAITING_WORKSPACE_STATUS_NAME,
|
|
15
|
-
} from '../entities/WorkflowStatus';
|
|
16
|
-
import { normalizeReportBody } from './normalizeReportBody';
|
|
17
|
-
|
|
18
|
-
const TRIAGER_AGENT_NAME = 'triager';
|
|
19
|
-
const MAX_COMMENT_FETCHES_PER_CYCLE = 20;
|
|
20
|
-
|
|
21
|
-
type TriagerProposal = {
|
|
22
|
-
recommendedAgent: string;
|
|
23
|
-
recommendedStory: string;
|
|
24
|
-
storyAlreadySet: boolean;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const parseTriagerProposalBlock = (
|
|
28
|
-
commentContent: string,
|
|
29
|
-
): TriagerProposal | null => {
|
|
30
|
-
if (!isAgentReportBodyFromAgent(commentContent, TRIAGER_AGENT_NAME)) {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const jsonBlockMatches = [
|
|
34
|
-
...normalizeReportBody(commentContent).matchAll(
|
|
35
|
-
/```json\n([\s\S]*?)\n```/g,
|
|
36
|
-
),
|
|
37
|
-
];
|
|
38
|
-
for (let i = 0; i < jsonBlockMatches.length; i++) {
|
|
39
|
-
const blockContent = jsonBlockMatches[i][1];
|
|
40
|
-
if (!blockContent) {
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
let parsed: unknown;
|
|
44
|
-
try {
|
|
45
|
-
parsed = JSON.parse(blockContent);
|
|
46
|
-
} catch {
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (!isRecord(parsed)) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const proposalValue = parsed['triagerProposal'];
|
|
53
|
-
if (!isRecord(proposalValue)) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (
|
|
57
|
-
typeof proposalValue['recommendedAgent'] !== 'string' ||
|
|
58
|
-
typeof proposalValue['recommendedStory'] !== 'string' ||
|
|
59
|
-
typeof proposalValue['storyAlreadySet'] !== 'boolean'
|
|
60
|
-
) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
recommendedAgent: proposalValue['recommendedAgent'],
|
|
65
|
-
recommendedStory: proposalValue['recommendedStory'],
|
|
66
|
-
storyAlreadySet: proposalValue['storyAlreadySet'],
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
return null;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const isNotFoundError = (error: unknown): boolean =>
|
|
73
|
-
(error instanceof Error ? error.message : String(error)).includes('404');
|
|
74
|
-
|
|
75
|
-
const isApprovalComment = (
|
|
76
|
-
content: string,
|
|
77
|
-
author: string,
|
|
78
|
-
allowedIssueAuthors: string[],
|
|
79
|
-
): boolean => {
|
|
80
|
-
if (!isAuthorAuthorizedForAutoStatusCheck(author, allowedIssueAuthors)) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
if (isAgentReportBody(content)) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
return /^(ok|オーケー|はい[\s\S]*)$/i.test(content.trim());
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export class TriagerApprovalDispatchUseCase {
|
|
90
|
-
constructor(
|
|
91
|
-
private readonly projectRepository: Pick<
|
|
92
|
-
ProjectRepository,
|
|
93
|
-
| 'findProjectIdByUrl'
|
|
94
|
-
| 'getProject'
|
|
95
|
-
| 'createField'
|
|
96
|
-
| 'getByUrl'
|
|
97
|
-
| 'updateAgentList'
|
|
98
|
-
>,
|
|
99
|
-
private readonly issueRepository: Pick<
|
|
100
|
-
IssueRepository,
|
|
101
|
-
'getAllIssues' | 'updateStatus' | 'updateStory' | 'setIssueAgentField'
|
|
102
|
-
>,
|
|
103
|
-
private readonly issueCommentRepository: Pick<
|
|
104
|
-
IssueCommentRepository,
|
|
105
|
-
'getCommentsFromIssue' | 'createComment'
|
|
106
|
-
>,
|
|
107
|
-
) {}
|
|
108
|
-
|
|
109
|
-
run = async (params: {
|
|
110
|
-
projectUrl: string;
|
|
111
|
-
allowedIssueAuthors?: string[] | null;
|
|
112
|
-
cycleIndex?: number;
|
|
113
|
-
}): Promise<void> => {
|
|
114
|
-
const allowedIssueAuthors = params.allowedIssueAuthors ?? null;
|
|
115
|
-
const cycleIndex = params.cycleIndex ?? Math.floor(Date.now() / 60_000);
|
|
116
|
-
|
|
117
|
-
const projectId = await this.projectRepository.findProjectIdByUrl(
|
|
118
|
-
params.projectUrl,
|
|
119
|
-
);
|
|
120
|
-
if (!projectId) {
|
|
121
|
-
throw new Error(`Project not found. projectUrl: ${params.projectUrl}`);
|
|
122
|
-
}
|
|
123
|
-
const project = await this.projectRepository.getProject(projectId);
|
|
124
|
-
if (!project) {
|
|
125
|
-
throw new Error(
|
|
126
|
-
`Project not found. projectId: ${projectId} projectUrl: ${params.projectUrl}`,
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const awaitingWorkspaceStatus = project.status.statuses.find(
|
|
131
|
-
(s) => s.name === AWAITING_WORKSPACE_STATUS_NAME,
|
|
132
|
-
);
|
|
133
|
-
if (!awaitingWorkspaceStatus) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const { issues } = await this.issueRepository.getAllIssues(projectId);
|
|
138
|
-
|
|
139
|
-
const candidateIssues = issues
|
|
140
|
-
.filter(
|
|
141
|
-
(issue) =>
|
|
142
|
-
(issue.status === AWAITING_WORKSPACE_STATUS_NAME ||
|
|
143
|
-
issue.status === AWAITING_QUALITY_CHECK_STATUS_NAME) &&
|
|
144
|
-
(issue.agent === null || issue.agent === TRIAGER_AGENT_NAME) &&
|
|
145
|
-
isAuthorAuthorizedForAutoStatusCheck(
|
|
146
|
-
issue.author,
|
|
147
|
-
allowedIssueAuthors,
|
|
148
|
-
),
|
|
149
|
-
)
|
|
150
|
-
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
|
151
|
-
|
|
152
|
-
if (candidateIssues.length === 0) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const windowSize = Math.min(
|
|
157
|
-
MAX_COMMENT_FETCHES_PER_CYCLE,
|
|
158
|
-
candidateIssues.length,
|
|
159
|
-
);
|
|
160
|
-
const windowStart =
|
|
161
|
-
(cycleIndex * MAX_COMMENT_FETCHES_PER_CYCLE) % candidateIssues.length;
|
|
162
|
-
|
|
163
|
-
for (let slot = 0; slot < windowSize; slot++) {
|
|
164
|
-
const issue =
|
|
165
|
-
candidateIssues[(windowStart + slot) % candidateIssues.length];
|
|
166
|
-
|
|
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
|
-
}
|
|
180
|
-
|
|
181
|
-
let firstProposalIndex = -1;
|
|
182
|
-
let proposal: TriagerProposal | null = null;
|
|
183
|
-
for (let i = 0; i < comments.length; i++) {
|
|
184
|
-
const parsed = parseTriagerProposalBlock(comments[i].content);
|
|
185
|
-
if (parsed !== null) {
|
|
186
|
-
if (firstProposalIndex === -1) {
|
|
187
|
-
firstProposalIndex = i;
|
|
188
|
-
}
|
|
189
|
-
proposal = parsed;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (firstProposalIndex === -1 || proposal === null) {
|
|
194
|
-
console.log(
|
|
195
|
-
`[TriagerApprovalDispatch] No machine-readable triager proposal block found, skipping. issueUrl: ${issue.url}`,
|
|
196
|
-
);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
let approvalIndex = -1;
|
|
201
|
-
for (let i = firstProposalIndex + 1; i < comments.length; i++) {
|
|
202
|
-
const comment = comments[i];
|
|
203
|
-
if (
|
|
204
|
-
isApprovalComment(
|
|
205
|
-
comment.content,
|
|
206
|
-
comment.author,
|
|
207
|
-
allowedIssueAuthors ?? [],
|
|
208
|
-
)
|
|
209
|
-
) {
|
|
210
|
-
approvalIndex = i;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (approvalIndex === -1) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const alreadyDispatched = comments
|
|
219
|
-
.slice(approvalIndex + 1)
|
|
220
|
-
.some((c) =>
|
|
221
|
-
c.content.startsWith('Auto Status Check: TRIAGER_PROPOSAL_APPROVED'),
|
|
222
|
-
);
|
|
223
|
-
if (alreadyDispatched) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const agentOptionId = await ensureAgentOptionAndGetId(
|
|
228
|
-
this.projectRepository,
|
|
229
|
-
project,
|
|
230
|
-
proposal.recommendedAgent,
|
|
231
|
-
);
|
|
232
|
-
if (agentOptionId === null) {
|
|
233
|
-
console.warn(
|
|
234
|
-
`[TriagerApprovalDispatch] Agent '${proposal.recommendedAgent}' could not be resolved, skipping. issueUrl: ${issue.url}`,
|
|
235
|
-
);
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (!proposal.storyAlreadySet && project.story) {
|
|
240
|
-
const storyOption = project.story.stories.find(
|
|
241
|
-
(s) => s.name === proposal.recommendedStory,
|
|
242
|
-
);
|
|
243
|
-
if (storyOption) {
|
|
244
|
-
await this.issueRepository.updateStory(
|
|
245
|
-
{ ...project, story: project.story },
|
|
246
|
-
issue,
|
|
247
|
-
storyOption.id,
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
console.warn(
|
|
251
|
-
`[TriagerApprovalDispatch] Story '${proposal.recommendedStory}' not found in project, skipping story update. issueUrl: ${issue.url}`,
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
await this.issueRepository.setIssueAgentField(
|
|
257
|
-
issue.url,
|
|
258
|
-
project,
|
|
259
|
-
agentOptionId,
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
await this.issueRepository.updateStatus(
|
|
263
|
-
project,
|
|
264
|
-
issue,
|
|
265
|
-
awaitingWorkspaceStatus.id,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
await this.issueCommentRepository.createComment(
|
|
269
|
-
issue,
|
|
270
|
-
`Auto Status Check: TRIAGER_PROPOSAL_APPROVED\nAgent: ${proposal.recommendedAgent}\nStory: ${proposal.storyAlreadySet ? '(already set)' : proposal.recommendedStory}`,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export const AWAITING_OWNER_APPROVAL_MESSAGE_HEAD =
|
|
2
|
-
'Auto Status Check: AWAITING_OWNER_APPROVAL';
|
|
3
|
-
|
|
4
|
-
export const AWAITING_OWNER_APPROVAL_MESSAGE = `${AWAITING_OWNER_APPROVAL_MESSAGE_HEAD}
|
|
5
|
-
The last report declared that this task is waiting for owner approval. Moving it to Awaiting Quality Check so that it waits for the owner's judgement instead of being dispatched again.`;
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
|
|
2
|
-
|
|
3
|
-
const trustEveryAuthor = (): boolean => true;
|
|
4
|
-
|
|
5
|
-
const reportComment = (
|
|
6
|
-
reportJson: string,
|
|
7
|
-
author = 'agent-bot',
|
|
8
|
-
): { author: string; content: string } => ({
|
|
9
|
-
author,
|
|
10
|
-
content: `From: :robot: agent report\n\`\`\`json\n${reportJson}\n\`\`\``,
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe('isPullRequestDeclaredUnnecessary', () => {
|
|
14
|
-
it('returns true when the last report declares pullRequestRequired as false', () => {
|
|
15
|
-
expect(
|
|
16
|
-
isPullRequestDeclaredUnnecessary(
|
|
17
|
-
[reportComment('{"pullRequestRequired": false}')],
|
|
18
|
-
trustEveryAuthor,
|
|
19
|
-
),
|
|
20
|
-
).toBe(true);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns true when the code fence backticks of that report are backslash escaped', () => {
|
|
24
|
-
expect(
|
|
25
|
-
isPullRequestDeclaredUnnecessary(
|
|
26
|
-
[
|
|
27
|
-
{
|
|
28
|
-
author: 'agent-bot',
|
|
29
|
-
content:
|
|
30
|
-
'From: :robot: agent report\n\\`\\`\\`json\n{"pullRequestRequired": false}\n\\`\\`\\`',
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
trustEveryAuthor,
|
|
34
|
-
),
|
|
35
|
-
).toBe(true);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns false when the last report declares pullRequestRequired as true', () => {
|
|
39
|
-
expect(
|
|
40
|
-
isPullRequestDeclaredUnnecessary(
|
|
41
|
-
[reportComment('{"pullRequestRequired": true}')],
|
|
42
|
-
trustEveryAuthor,
|
|
43
|
-
),
|
|
44
|
-
).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('returns false when the report carries no pullRequestRequired field', () => {
|
|
48
|
-
expect(
|
|
49
|
-
isPullRequestDeclaredUnnecessary(
|
|
50
|
-
[reportComment('{"nextStep": null}')],
|
|
51
|
-
trustEveryAuthor,
|
|
52
|
-
),
|
|
53
|
-
).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('returns false when pullRequestRequired is the string false rather than the boolean', () => {
|
|
57
|
-
expect(
|
|
58
|
-
isPullRequestDeclaredUnnecessary(
|
|
59
|
-
[reportComment('{"pullRequestRequired": "false"}')],
|
|
60
|
-
trustEveryAuthor,
|
|
61
|
-
),
|
|
62
|
-
).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('returns false when the author of the last comment is not trusted', () => {
|
|
66
|
-
expect(
|
|
67
|
-
isPullRequestDeclaredUnnecessary(
|
|
68
|
-
[reportComment('{"pullRequestRequired": false}', 'stranger')],
|
|
69
|
-
(author) => author === 'agent-bot',
|
|
70
|
-
),
|
|
71
|
-
).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('returns false when the last comment is not an agent report', () => {
|
|
75
|
-
expect(
|
|
76
|
-
isPullRequestDeclaredUnnecessary(
|
|
77
|
-
[
|
|
78
|
-
reportComment('{"pullRequestRequired": false}'),
|
|
79
|
-
{
|
|
80
|
-
author: 'agent-bot',
|
|
81
|
-
content:
|
|
82
|
-
'A later human comment\n```json\n{"pullRequestRequired": false}\n```',
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
trustEveryAuthor,
|
|
86
|
-
),
|
|
87
|
-
).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('reads only the last comment and ignores an earlier declaration', () => {
|
|
91
|
-
expect(
|
|
92
|
-
isPullRequestDeclaredUnnecessary(
|
|
93
|
-
[
|
|
94
|
-
reportComment('{"pullRequestRequired": false}'),
|
|
95
|
-
reportComment('{"pullRequestRequired": true}'),
|
|
96
|
-
],
|
|
97
|
-
trustEveryAuthor,
|
|
98
|
-
),
|
|
99
|
-
).toBe(false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('returns false when there is no comment at all', () => {
|
|
103
|
-
expect(isPullRequestDeclaredUnnecessary([], trustEveryAuthor)).toBe(false);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('returns false when the JSON block cannot be parsed', () => {
|
|
107
|
-
const consoleWarn = jest
|
|
108
|
-
.spyOn(console, 'warn')
|
|
109
|
-
.mockImplementation(() => {});
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
isPullRequestDeclaredUnnecessary(
|
|
113
|
-
[reportComment('{"pullRequestRequired": false')],
|
|
114
|
-
trustEveryAuthor,
|
|
115
|
-
),
|
|
116
|
-
).toBe(false);
|
|
117
|
-
|
|
118
|
-
consoleWarn.mockRestore();
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('returns false when the report carries no JSON block', () => {
|
|
122
|
-
expect(
|
|
123
|
-
isPullRequestDeclaredUnnecessary(
|
|
124
|
-
[{ author: 'agent-bot', content: 'From: :robot: agent report' }],
|
|
125
|
-
trustEveryAuthor,
|
|
126
|
-
),
|
|
127
|
-
).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('returns true when pullRequestRequired is declared in a later fenced json block', () => {
|
|
131
|
-
expect(
|
|
132
|
-
isPullRequestDeclaredUnnecessary(
|
|
133
|
-
[
|
|
134
|
-
{
|
|
135
|
-
author: 'agent-bot',
|
|
136
|
-
content:
|
|
137
|
-
'From: :robot: agent report\n\n```json\n{ "nextStepAgent": "pr-reviewer" }\n```\n\n## summary\n\n```json\n{ "pullRequestRequired": false }\n```',
|
|
138
|
-
},
|
|
139
|
-
],
|
|
140
|
-
trustEveryAuthor,
|
|
141
|
-
),
|
|
142
|
-
).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('reads a later block and reports the earlier unparseable block', () => {
|
|
146
|
-
const consoleWarn = jest.spyOn(console, 'warn').mockImplementation();
|
|
147
|
-
expect(
|
|
148
|
-
isPullRequestDeclaredUnnecessary(
|
|
149
|
-
[
|
|
150
|
-
{
|
|
151
|
-
author: 'agent-bot',
|
|
152
|
-
content:
|
|
153
|
-
'From: :robot: agent report\n\n```json\n{ "pullRequestRequired": false,\n```\n\n```json\n{ "pullRequestRequired": false }\n```',
|
|
154
|
-
},
|
|
155
|
-
],
|
|
156
|
-
trustEveryAuthor,
|
|
157
|
-
),
|
|
158
|
-
).toBe(true);
|
|
159
|
-
expect(consoleWarn).toHaveBeenCalled();
|
|
160
|
-
consoleWarn.mockRestore();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { extractFencedJsonBlocks } from './extractFencedJsonBlocks';
|
|
2
|
-
import { isAgentReportBody } from './isAgentReportBody';
|
|
3
|
-
|
|
4
|
-
export const isPullRequestDeclaredUnnecessary = (
|
|
5
|
-
comments: { author: string; content: string }[],
|
|
6
|
-
isTrustedAuthor: (author: string) => boolean,
|
|
7
|
-
): boolean => {
|
|
8
|
-
const lastComment = comments[comments.length - 1];
|
|
9
|
-
if (
|
|
10
|
-
!lastComment ||
|
|
11
|
-
!isTrustedAuthor(lastComment.author) ||
|
|
12
|
-
!isAgentReportBody(lastComment.content)
|
|
13
|
-
) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
for (const block of extractFencedJsonBlocks(
|
|
17
|
-
lastComment.content,
|
|
18
|
-
'pullRequestRequired',
|
|
19
|
-
)) {
|
|
20
|
-
if (typeof block !== 'object' || block === null) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const report: Record<string, unknown> = { ...block };
|
|
24
|
-
if (report.pullRequestRequired === false) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
};
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { isWaitingForOwnerApproval } from './isWaitingForOwnerApproval';
|
|
2
|
-
|
|
3
|
-
const agentReport = (reportJson: string): string =>
|
|
4
|
-
`From: :robot: agent-name (model-id)\n\`\`\`json\n${reportJson}\n\`\`\``;
|
|
5
|
-
|
|
6
|
-
describe('isWaitingForOwnerApproval', () => {
|
|
7
|
-
it('returns true when the first block has pullRequestRequired false and waitingForOwnerApproval true', () => {
|
|
8
|
-
expect(
|
|
9
|
-
isWaitingForOwnerApproval(
|
|
10
|
-
agentReport(
|
|
11
|
-
'{"pullRequestRequired": false, "waitingForOwnerApproval": true}',
|
|
12
|
-
),
|
|
13
|
-
),
|
|
14
|
-
).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns false when waitingForOwnerApproval is missing', () => {
|
|
18
|
-
expect(
|
|
19
|
-
isWaitingForOwnerApproval(agentReport('{"pullRequestRequired": false}')),
|
|
20
|
-
).toBe(false);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns false when pullRequestRequired is missing', () => {
|
|
24
|
-
expect(
|
|
25
|
-
isWaitingForOwnerApproval(
|
|
26
|
-
agentReport('{"waitingForOwnerApproval": true}'),
|
|
27
|
-
),
|
|
28
|
-
).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns false when pullRequestRequired is true', () => {
|
|
32
|
-
expect(
|
|
33
|
-
isWaitingForOwnerApproval(
|
|
34
|
-
agentReport(
|
|
35
|
-
'{"pullRequestRequired": true, "waitingForOwnerApproval": true}',
|
|
36
|
-
),
|
|
37
|
-
),
|
|
38
|
-
).toBe(false);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('returns false when waitingForOwnerApproval is false', () => {
|
|
42
|
-
expect(
|
|
43
|
-
isWaitingForOwnerApproval(
|
|
44
|
-
agentReport(
|
|
45
|
-
'{"pullRequestRequired": false, "waitingForOwnerApproval": false}',
|
|
46
|
-
),
|
|
47
|
-
),
|
|
48
|
-
).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('returns false when waitingForOwnerApproval is a string rather than the boolean true', () => {
|
|
52
|
-
expect(
|
|
53
|
-
isWaitingForOwnerApproval(
|
|
54
|
-
agentReport(
|
|
55
|
-
'{"pullRequestRequired": false, "waitingForOwnerApproval": "true"}',
|
|
56
|
-
),
|
|
57
|
-
),
|
|
58
|
-
).toBe(false);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('returns false when there is no JSON block', () => {
|
|
62
|
-
expect(
|
|
63
|
-
isWaitingForOwnerApproval('From: :robot: agent-name (model-id)'),
|
|
64
|
-
).toBe(false);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns false when the JSON block cannot be parsed', () => {
|
|
68
|
-
const consoleWarn = jest
|
|
69
|
-
.spyOn(console, 'warn')
|
|
70
|
-
.mockImplementation(() => {});
|
|
71
|
-
expect(
|
|
72
|
-
isWaitingForOwnerApproval(
|
|
73
|
-
agentReport(
|
|
74
|
-
'{"pullRequestRequired": false, "waitingForOwnerApproval": true',
|
|
75
|
-
),
|
|
76
|
-
),
|
|
77
|
-
).toBe(false);
|
|
78
|
-
consoleWarn.mockRestore();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('reads only the first block and ignores subsequent blocks', () => {
|
|
82
|
-
const content = `From: :robot: agent-name (model-id)\n\`\`\`json\n{"pullRequestRequired": false, "nextStep": null}\n\`\`\`\n\n\`\`\`json\n{"pullRequestRequired": false, "waitingForOwnerApproval": true}\n\`\`\``;
|
|
83
|
-
expect(isWaitingForOwnerApproval(content)).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('returns true when backticks in the block are backslash-escaped', () => {
|
|
87
|
-
const content =
|
|
88
|
-
'From: :robot: agent-name (model-id)\n\\`\\`\\`json\n{"pullRequestRequired": false, "waitingForOwnerApproval": true}\n\\`\\`\\`';
|
|
89
|
-
expect(isWaitingForOwnerApproval(content)).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { extractFencedJsonBlocks } from './extractFencedJsonBlocks';
|
|
2
|
-
|
|
3
|
-
export const isWaitingForOwnerApproval = (reportContent: string): boolean => {
|
|
4
|
-
const blocks = extractFencedJsonBlocks(
|
|
5
|
-
reportContent,
|
|
6
|
-
'waitingForOwnerApproval',
|
|
7
|
-
);
|
|
8
|
-
const firstBlock = blocks[0];
|
|
9
|
-
if (typeof firstBlock !== 'object' || firstBlock === null) {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
const report: Record<string, unknown> = { ...firstBlock };
|
|
13
|
-
return (
|
|
14
|
-
report.pullRequestRequired === false &&
|
|
15
|
-
report.waitingForOwnerApproval === true
|
|
16
|
-
);
|
|
17
|
-
};
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export const RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD =
|
|
2
|
-
'Auto Status Check: RETURNED_TO_AWAITING_WORKSPACE';
|
|
3
|
-
|
|
4
|
-
export const RETURNED_TO_AWAITING_WORKSPACE_MESSAGE = `${RETURNED_TO_AWAITING_WORKSPACE_MESSAGE_HEAD}
|
|
5
|
-
The last report declared that this task needs no pull request, so the task returns to the workspace queue to run under the agent its label selects. A later report carrying the same declaration takes the awaiting quality check path instead of returning here again.`;
|