github-issue-tower-defence-management 1.50.2 → 1.51.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 +2 -2
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js +5 -83
- package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +11 -0
- package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
- package/bin/domain/entities/WorkflowStatus.js +6 -1
- package/bin/domain/entities/WorkflowStatus.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +7 -2
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +0 -1
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +102 -0
- package/src/adapter/repositories/GitHubIssueCommentRepository.ts +13 -134
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -1
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +1 -1
- package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +16 -0
- package/src/domain/entities/WorkflowStatus.ts +5 -0
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +11 -5
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +12 -2
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +10 -1
- package/src/domain/usecases/StartPreparationUseCase.test.ts +21 -26
- package/src/domain/usecases/StartPreparationUseCase.ts +0 -1
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +0 -1
- package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
- package/types/domain/entities/WorkflowStatus.d.ts +1 -0
- package/types/domain/entities/WorkflowStatus.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
|
@@ -170,4 +170,106 @@ describe('GitHubIssueCommentRepository', () => {
|
|
|
170
170
|
).rejects.toThrow('Not Found');
|
|
171
171
|
});
|
|
172
172
|
});
|
|
173
|
+
|
|
174
|
+
describe('createComment', () => {
|
|
175
|
+
it('should call the REST endpoint with correct URL, headers, and body for an issue', async () => {
|
|
176
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
177
|
+
new Response(JSON.stringify({ id: 1 }), {
|
|
178
|
+
status: 201,
|
|
179
|
+
headers: { 'Content-Type': 'application/json' },
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const issue = buildIssue(
|
|
184
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
185
|
+
);
|
|
186
|
+
await repository.createComment(issue, 'hello world');
|
|
187
|
+
|
|
188
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
189
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/issues/42/comments',
|
|
190
|
+
{
|
|
191
|
+
method: 'POST',
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: 'Bearer test-token',
|
|
194
|
+
Accept: 'application/vnd.github+json',
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
},
|
|
197
|
+
body: JSON.stringify({ body: 'hello world' }),
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should call the REST endpoint with correct URL for a pull request', async () => {
|
|
203
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
204
|
+
new Response(JSON.stringify({ id: 2 }), {
|
|
205
|
+
status: 201,
|
|
206
|
+
headers: { 'Content-Type': 'application/json' },
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const issue = buildIssue(
|
|
211
|
+
'https://github.com/HiromiShikata/test-repository/pull/10',
|
|
212
|
+
);
|
|
213
|
+
await repository.createComment(issue, 'pr comment');
|
|
214
|
+
|
|
215
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
216
|
+
'https://api.github.com/repos/HiromiShikata/test-repository/issues/10/comments',
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
method: 'POST',
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should throw an error when the response is not 2xx', async () => {
|
|
224
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
225
|
+
new Response('Not Found', {
|
|
226
|
+
status: 404,
|
|
227
|
+
statusText: 'Not Found',
|
|
228
|
+
}),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const issue = buildIssue(
|
|
232
|
+
'https://github.com/HiromiShikata/test-repository/issues/42',
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
await expect(
|
|
236
|
+
repository.createComment(issue, 'hello world'),
|
|
237
|
+
).rejects.toThrow('404');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should issue exactly one HTTP request per call', async () => {
|
|
241
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
242
|
+
new Response(JSON.stringify({ id: 3 }), {
|
|
243
|
+
status: 201,
|
|
244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const issue = buildIssue(
|
|
249
|
+
'https://github.com/HiromiShikata/test-repository/issues/5',
|
|
250
|
+
);
|
|
251
|
+
await repository.createComment(issue, 'single request');
|
|
252
|
+
|
|
253
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should not call the GraphQL endpoint', async () => {
|
|
257
|
+
jest.spyOn(global, 'fetch').mockResolvedValue(
|
|
258
|
+
new Response(JSON.stringify({ id: 4 }), {
|
|
259
|
+
status: 201,
|
|
260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const issue = buildIssue(
|
|
265
|
+
'https://github.com/HiromiShikata/test-repository/issues/7',
|
|
266
|
+
);
|
|
267
|
+
await repository.createComment(issue, 'no graphql');
|
|
268
|
+
|
|
269
|
+
expect(global.fetch).not.toHaveBeenCalledWith(
|
|
270
|
+
'https://api.github.com/graphql',
|
|
271
|
+
expect.anything(),
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
173
275
|
});
|
|
@@ -8,32 +8,6 @@ type RestCommentPayload = {
|
|
|
8
8
|
created_at: string;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
type CreateCommentResponse = {
|
|
12
|
-
data?: {
|
|
13
|
-
addComment?: {
|
|
14
|
-
commentEdge: {
|
|
15
|
-
node: {
|
|
16
|
-
id: string;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
errors?: Array<{ message: string }>;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type IssueIdResponse = {
|
|
25
|
-
data?: {
|
|
26
|
-
repository?: {
|
|
27
|
-
issue?: {
|
|
28
|
-
id: string;
|
|
29
|
-
};
|
|
30
|
-
pullRequest?: {
|
|
31
|
-
id: string;
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
|
|
37
11
|
function isRestCommentPayloadArray(
|
|
38
12
|
value: unknown,
|
|
39
13
|
): value is RestCommentPayload[] {
|
|
@@ -41,18 +15,6 @@ function isRestCommentPayloadArray(
|
|
|
41
15
|
return true;
|
|
42
16
|
}
|
|
43
17
|
|
|
44
|
-
function isCreateCommentResponse(
|
|
45
|
-
value: unknown,
|
|
46
|
-
): value is CreateCommentResponse {
|
|
47
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
48
|
-
return true;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function isIssueIdResponse(value: unknown): value is IssueIdResponse {
|
|
52
|
-
if (typeof value !== 'object' || value === null) return false;
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
18
|
export class GitHubIssueCommentRepository implements IssueCommentRepository {
|
|
57
19
|
constructor(private readonly token: string) {}
|
|
58
20
|
|
|
@@ -122,108 +84,25 @@ export class GitHubIssueCommentRepository implements IssueCommentRepository {
|
|
|
122
84
|
return comments;
|
|
123
85
|
}
|
|
124
86
|
|
|
125
|
-
private async getIssueNodeId(issue: Issue): Promise<string> {
|
|
126
|
-
const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(issue);
|
|
127
|
-
|
|
128
|
-
const entityType = isPr ? 'pullRequest' : 'issue';
|
|
129
|
-
const query = `
|
|
130
|
-
query($owner: String!, $repo: String!, $issueNumber: Int!) {
|
|
131
|
-
repository(owner: $owner, name: $repo) {
|
|
132
|
-
${entityType}(number: $issueNumber) {
|
|
133
|
-
id
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
`;
|
|
138
|
-
|
|
139
|
-
const response = await fetch('https://api.github.com/graphql', {
|
|
140
|
-
method: 'POST',
|
|
141
|
-
headers: {
|
|
142
|
-
Authorization: `Bearer ${this.token}`,
|
|
143
|
-
'Content-Type': 'application/json',
|
|
144
|
-
},
|
|
145
|
-
body: JSON.stringify({
|
|
146
|
-
query,
|
|
147
|
-
variables: {
|
|
148
|
-
owner,
|
|
149
|
-
repo,
|
|
150
|
-
issueNumber,
|
|
151
|
-
},
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`Failed to fetch issue ID from GitHub GraphQL API: ${response.status} ${response.statusText}`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const responseData: unknown = await response.json();
|
|
162
|
-
if (!isIssueIdResponse(responseData)) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
'Unexpected response shape when fetching issue ID from GitHub GraphQL API',
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const issueId = isPr
|
|
169
|
-
? responseData.data?.repository?.pullRequest?.id
|
|
170
|
-
: responseData.data?.repository?.issue?.id;
|
|
171
|
-
if (!issueId) {
|
|
172
|
-
throw new Error(
|
|
173
|
-
`${isPr ? 'Pull request' : 'Issue'} not found when fetching issue ID from GitHub GraphQL API`,
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return issueId;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
87
|
async createComment(issue: Issue, commentContent: string): Promise<void> {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
const mutation = `
|
|
184
|
-
mutation($issueId: ID!, $body: String!) {
|
|
185
|
-
addComment(input: {
|
|
186
|
-
subjectId: $issueId
|
|
187
|
-
body: $body
|
|
188
|
-
}) {
|
|
189
|
-
commentEdge {
|
|
190
|
-
node {
|
|
191
|
-
id
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
`;
|
|
88
|
+
const { owner, repo, issueNumber } = this.parseIssueUrl(issue);
|
|
197
89
|
|
|
198
|
-
const response = await fetch(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
variables: {
|
|
207
|
-
issueId,
|
|
208
|
-
body: commentContent,
|
|
90
|
+
const response = await fetch(
|
|
91
|
+
`https://api.github.com/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
|
|
92
|
+
{
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: {
|
|
95
|
+
Authorization: `Bearer ${this.token}`,
|
|
96
|
+
Accept: 'application/vnd.github+json',
|
|
97
|
+
'Content-Type': 'application/json',
|
|
209
98
|
},
|
|
210
|
-
|
|
211
|
-
|
|
99
|
+
body: JSON.stringify({ body: commentContent }),
|
|
100
|
+
},
|
|
101
|
+
);
|
|
212
102
|
|
|
213
103
|
if (!response.ok) {
|
|
214
104
|
throw new Error(
|
|
215
|
-
`Failed to create comment via GitHub
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const responseData: unknown = await response.json();
|
|
220
|
-
if (!isCreateCommentResponse(responseData)) {
|
|
221
|
-
throw new Error('Invalid API response format when creating comment');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (responseData.errors) {
|
|
225
|
-
throw new Error(
|
|
226
|
-
`GraphQL errors when creating comment: ${JSON.stringify(responseData.errors)}`,
|
|
105
|
+
`Failed to create comment via GitHub REST API: ${response.status} ${response.statusText}`,
|
|
227
106
|
);
|
|
228
107
|
}
|
|
229
108
|
}
|
|
@@ -31,6 +31,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
31
31
|
labels: [],
|
|
32
32
|
assignees: [],
|
|
33
33
|
createdAt: '2024-01-01T00:00:00Z',
|
|
34
|
+
author: 'test-author',
|
|
34
35
|
customFields: [
|
|
35
36
|
{ name: 'nextActionDate', value: '2000-01-01' },
|
|
36
37
|
{ name: 'nextActionHour', value: '1' },
|
|
@@ -63,7 +64,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
63
64
|
isInProgress: false,
|
|
64
65
|
isClosed: false,
|
|
65
66
|
createdAt: new Date('2024-01-01T00:00:00Z'),
|
|
66
|
-
author: '',
|
|
67
|
+
author: 'test-author',
|
|
67
68
|
},
|
|
68
69
|
},
|
|
69
70
|
{
|
|
@@ -80,6 +81,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
80
81
|
labels: [],
|
|
81
82
|
assignees: [],
|
|
82
83
|
createdAt: '2024-01-01T00:00:00Z',
|
|
84
|
+
author: '',
|
|
83
85
|
customFields: [
|
|
84
86
|
{
|
|
85
87
|
name: 'DependedIssueUrls',
|
|
@@ -376,7 +376,7 @@ export class ApiV3CheerioRestIssueRepository
|
|
|
376
376
|
isInProgress: normalizeFieldName(status || '').includes('progress'),
|
|
377
377
|
isClosed: item.state !== 'OPEN',
|
|
378
378
|
createdAt: new Date(item.createdAt || '2000-01-01'),
|
|
379
|
-
author:
|
|
379
|
+
author: item.author,
|
|
380
380
|
};
|
|
381
381
|
};
|
|
382
382
|
getAllIssuesFromCache = async (
|
|
@@ -13,6 +13,7 @@ export type ProjectItem = {
|
|
|
13
13
|
labels: string[];
|
|
14
14
|
assignees: string[];
|
|
15
15
|
createdAt: string;
|
|
16
|
+
author: string;
|
|
16
17
|
customFields: {
|
|
17
18
|
name: string;
|
|
18
19
|
value: string | null;
|
|
@@ -159,6 +160,9 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
159
160
|
state
|
|
160
161
|
url
|
|
161
162
|
createdAt
|
|
163
|
+
author {
|
|
164
|
+
login
|
|
165
|
+
}
|
|
162
166
|
labels(first: 100) {
|
|
163
167
|
nodes {
|
|
164
168
|
name
|
|
@@ -179,6 +183,9 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
179
183
|
state
|
|
180
184
|
url
|
|
181
185
|
createdAt
|
|
186
|
+
author {
|
|
187
|
+
login
|
|
188
|
+
}
|
|
182
189
|
labels(first: 100) {
|
|
183
190
|
nodes {
|
|
184
191
|
name
|
|
@@ -231,6 +238,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
231
238
|
state: string;
|
|
232
239
|
url: string;
|
|
233
240
|
createdAt: string;
|
|
241
|
+
author: { login: string } | null;
|
|
234
242
|
labels: { nodes: { name: string }[] };
|
|
235
243
|
assignees: { nodes: { login: string }[] };
|
|
236
244
|
};
|
|
@@ -281,6 +289,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
281
289
|
state: string;
|
|
282
290
|
url: string;
|
|
283
291
|
createdAt: string;
|
|
292
|
+
author: { login: string } | null;
|
|
284
293
|
labels: { nodes: { name: string }[] };
|
|
285
294
|
assignees: { nodes: { login: string }[] };
|
|
286
295
|
};
|
|
@@ -347,6 +356,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
347
356
|
state: string;
|
|
348
357
|
url: string;
|
|
349
358
|
createdAt: string;
|
|
359
|
+
author: { login: string } | null;
|
|
350
360
|
labels: { nodes: { name: string }[] };
|
|
351
361
|
assignees: { nodes: { login: string }[] };
|
|
352
362
|
};
|
|
@@ -366,6 +376,7 @@ query GetProjectItems($projectId: ID!, $after: String) {
|
|
|
366
376
|
labels: item.content.labels?.nodes?.map((l) => l.name) || [],
|
|
367
377
|
assignees: item.content.assignees?.nodes?.map((a) => a.login) || [],
|
|
368
378
|
createdAt: item.content.createdAt || new Date().toISOString(),
|
|
379
|
+
author: item.content.author?.login || '',
|
|
369
380
|
customFields: item.fieldValues.nodes
|
|
370
381
|
.filter((field) => !!field.field)
|
|
371
382
|
.map((field) => {
|
|
@@ -587,6 +598,9 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
587
598
|
url
|
|
588
599
|
body
|
|
589
600
|
createdAt
|
|
601
|
+
author {
|
|
602
|
+
login
|
|
603
|
+
}
|
|
590
604
|
labels(first: 100) {
|
|
591
605
|
nodes {
|
|
592
606
|
name
|
|
@@ -678,6 +692,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
678
692
|
url: string;
|
|
679
693
|
body: string;
|
|
680
694
|
createdAt: string;
|
|
695
|
+
author: { login: string } | null;
|
|
681
696
|
labels: { nodes: { name: string }[] };
|
|
682
697
|
assignees: { nodes: { login: string }[] };
|
|
683
698
|
repository: { nameWithOwner: string };
|
|
@@ -744,6 +759,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
|
|
|
744
759
|
assignees:
|
|
745
760
|
data.repository.issue.assignees?.nodes?.map((a) => a.login) || [],
|
|
746
761
|
createdAt: data.repository.issue.createdAt || new Date().toISOString(),
|
|
762
|
+
author: data.repository.issue.author?.login || '',
|
|
747
763
|
customFields: item.fieldValues.nodes
|
|
748
764
|
.filter((field) => !!field.field)
|
|
749
765
|
.map((field) => {
|
|
@@ -4,6 +4,7 @@ export const DEFAULT_STATUS_NAME = 'Unread';
|
|
|
4
4
|
export const AWAITING_TASK_BREAKDOWN_STATUS_NAME = 'Awaiting Task Breakdown';
|
|
5
5
|
export const AWAITING_WORKSPACE_STATUS_NAME = 'Awaiting Workspace';
|
|
6
6
|
export const PREPARATION_STATUS_NAME = 'Preparation';
|
|
7
|
+
export const FAILED_PREPARATION_STATUS_NAME = 'Failed Preparation';
|
|
7
8
|
export const AWAITING_QUALITY_CHECK_STATUS_NAME = 'Awaiting Quality Check';
|
|
8
9
|
export const TODO_STATUS_NAME = 'Todo';
|
|
9
10
|
export const PC_TODO_STATUS_NAME = 'PC Todo';
|
|
@@ -33,6 +34,10 @@ export const REQUIRED_WORKFLOW_STATUSES: WorkflowStatusDefinition[] = [
|
|
|
33
34
|
name: PREPARATION_STATUS_NAME,
|
|
34
35
|
color: 'YELLOW',
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: FAILED_PREPARATION_STATUS_NAME,
|
|
39
|
+
color: 'RED',
|
|
40
|
+
},
|
|
36
41
|
{
|
|
37
42
|
name: AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
38
43
|
color: 'GREEN',
|
|
@@ -25,6 +25,12 @@ const createMockProject = (overrides: Partial<Project> = {}): Project => ({
|
|
|
25
25
|
color: 'GRAY',
|
|
26
26
|
description: '',
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
id: 'failed-preparation-id',
|
|
30
|
+
name: 'Failed Preparation',
|
|
31
|
+
color: 'RED',
|
|
32
|
+
description: '',
|
|
33
|
+
},
|
|
28
34
|
{
|
|
29
35
|
id: 'awaiting-quality-check-id',
|
|
30
36
|
name: 'Awaiting Quality Check',
|
|
@@ -639,7 +645,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
639
645
|
);
|
|
640
646
|
});
|
|
641
647
|
|
|
642
|
-
it('should auto-escalate to
|
|
648
|
+
it('should auto-escalate to Failed Preparation after threshold rejections', async () => {
|
|
643
649
|
const issue = createMockIssue({
|
|
644
650
|
url: 'https://github.com/user/repo/issues/1',
|
|
645
651
|
status: 'Preparation',
|
|
@@ -663,14 +669,14 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
663
669
|
|
|
664
670
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
665
671
|
expect.objectContaining({
|
|
666
|
-
status: '
|
|
672
|
+
status: 'Failed Preparation',
|
|
667
673
|
}),
|
|
668
674
|
mockProject,
|
|
669
675
|
);
|
|
670
676
|
expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
|
|
671
677
|
mockProject,
|
|
672
|
-
expect.objectContaining({ status: '
|
|
673
|
-
'
|
|
678
|
+
expect.objectContaining({ status: 'Failed Preparation' }),
|
|
679
|
+
'failed-preparation-id',
|
|
674
680
|
);
|
|
675
681
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
676
682
|
expect.objectContaining({
|
|
@@ -727,7 +733,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
|
|
|
727
733
|
});
|
|
728
734
|
|
|
729
735
|
expect(mockIssueRepository.update).toHaveBeenCalledWith(
|
|
730
|
-
expect.objectContaining({ status: '
|
|
736
|
+
expect.objectContaining({ status: 'Failed Preparation' }),
|
|
731
737
|
mockProject,
|
|
732
738
|
);
|
|
733
739
|
expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
|
|
@@ -8,6 +8,7 @@ import { WebhookRepository } from './adapter-interfaces/WebhookRepository';
|
|
|
8
8
|
import {
|
|
9
9
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
10
10
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
11
|
+
FAILED_PREPARATION_STATUS_NAME,
|
|
11
12
|
PREPARATION_STATUS_NAME,
|
|
12
13
|
} from '../entities/WorkflowStatus';
|
|
13
14
|
|
|
@@ -88,6 +89,15 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
88
89
|
);
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
92
|
+
const failedPreparationStatusOption = project.status.statuses.find(
|
|
93
|
+
(s) => s.name === FAILED_PREPARATION_STATUS_NAME,
|
|
94
|
+
);
|
|
95
|
+
if (!failedPreparationStatusOption) {
|
|
96
|
+
console.error(
|
|
97
|
+
`Failed preparation status option '${FAILED_PREPARATION_STATUS_NAME}' not found in project.`,
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
91
101
|
|
|
92
102
|
const issue = await this.issueRepository.get(params.issueUrl, project);
|
|
93
103
|
|
|
@@ -180,12 +190,12 @@ export class NotifyFinishedIssuePreparationUseCase {
|
|
|
180
190
|
.includes('failed to pass the check automatically'),
|
|
181
191
|
)
|
|
182
192
|
) {
|
|
183
|
-
issue.status =
|
|
193
|
+
issue.status = FAILED_PREPARATION_STATUS_NAME;
|
|
184
194
|
await this.issueRepository.update(issue, project);
|
|
185
195
|
await this.issueRepository.updateStatus(
|
|
186
196
|
project,
|
|
187
197
|
issue,
|
|
188
|
-
|
|
198
|
+
failedPreparationStatusOption.id,
|
|
189
199
|
);
|
|
190
200
|
const escalationStatusLine =
|
|
191
201
|
rejections.length > 0
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
9
9
|
DEFAULT_STATUS_NAME,
|
|
10
10
|
DONE_STATUS_NAME,
|
|
11
|
+
FAILED_PREPARATION_STATUS_NAME,
|
|
11
12
|
ICEBOX_STATUS_NAME,
|
|
12
13
|
IN_TMUX_STATUS_NAME,
|
|
13
14
|
PC_TODO_STATUS_NAME,
|
|
@@ -43,12 +44,13 @@ const buildCanonicalStatuses = (): FieldOption[] =>
|
|
|
43
44
|
}));
|
|
44
45
|
|
|
45
46
|
describe('SetupTowerDefenceProjectUseCase', () => {
|
|
46
|
-
it('should define exactly the
|
|
47
|
+
it('should define exactly the 11 required statuses in the documented order with the documented colors and no descriptions', () => {
|
|
47
48
|
expect(REQUIRED_WORKFLOW_STATUSES).toEqual([
|
|
48
49
|
{ name: DEFAULT_STATUS_NAME, color: 'ORANGE' },
|
|
49
50
|
{ name: AWAITING_TASK_BREAKDOWN_STATUS_NAME, color: 'ORANGE' },
|
|
50
51
|
{ name: AWAITING_WORKSPACE_STATUS_NAME, color: 'BLUE' },
|
|
51
52
|
{ name: PREPARATION_STATUS_NAME, color: 'YELLOW' },
|
|
53
|
+
{ name: FAILED_PREPARATION_STATUS_NAME, color: 'RED' },
|
|
52
54
|
{ name: AWAITING_QUALITY_CHECK_STATUS_NAME, color: 'GREEN' },
|
|
53
55
|
{ name: TODO_STATUS_NAME, color: 'PINK' },
|
|
54
56
|
{ name: PC_TODO_STATUS_NAME, color: 'PINK' },
|
|
@@ -167,6 +169,12 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
167
169
|
color: 'YELLOW',
|
|
168
170
|
description: '',
|
|
169
171
|
},
|
|
172
|
+
{
|
|
173
|
+
id: null,
|
|
174
|
+
name: FAILED_PREPARATION_STATUS_NAME,
|
|
175
|
+
color: 'RED',
|
|
176
|
+
description: '',
|
|
177
|
+
},
|
|
170
178
|
{
|
|
171
179
|
id: null,
|
|
172
180
|
name: AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
@@ -238,6 +246,7 @@ describe('SetupTowerDefenceProjectUseCase', () => {
|
|
|
238
246
|
AWAITING_TASK_BREAKDOWN_STATUS_NAME,
|
|
239
247
|
AWAITING_WORKSPACE_STATUS_NAME,
|
|
240
248
|
PREPARATION_STATUS_NAME,
|
|
249
|
+
FAILED_PREPARATION_STATUS_NAME,
|
|
241
250
|
AWAITING_QUALITY_CHECK_STATUS_NAME,
|
|
242
251
|
TODO_STATUS_NAME,
|
|
243
252
|
PC_TODO_STATUS_NAME,
|
|
@@ -2039,25 +2039,27 @@ describe('StartPreparationUseCase', () => {
|
|
|
2039
2039
|
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
|
|
2040
2040
|
});
|
|
2041
2041
|
|
|
2042
|
-
it('should
|
|
2043
|
-
const
|
|
2042
|
+
it('should skip issues with empty author when allowedIssueAuthors is configured (deny-by-default)', async () => {
|
|
2043
|
+
const issueWithEmptyAuthor = createMockIssue({
|
|
2044
2044
|
url: 'https://github.com/user/repo/issues/1',
|
|
2045
|
-
title: '
|
|
2045
|
+
title: 'Issue with empty author',
|
|
2046
2046
|
labels: [],
|
|
2047
2047
|
status: 'Awaiting Workspace',
|
|
2048
2048
|
author: '',
|
|
2049
2049
|
});
|
|
2050
|
-
const
|
|
2050
|
+
const issueWithKnownAuthor = createMockIssue({
|
|
2051
2051
|
url: 'https://github.com/user/repo/issues/2',
|
|
2052
|
-
title: '
|
|
2052
|
+
title: 'Issue with known author',
|
|
2053
2053
|
labels: [],
|
|
2054
2054
|
status: 'Awaiting Workspace',
|
|
2055
2055
|
author: 'user1',
|
|
2056
|
+
number: 2,
|
|
2057
|
+
itemId: 'item-2',
|
|
2056
2058
|
});
|
|
2057
2059
|
|
|
2058
2060
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2059
2061
|
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2060
|
-
createMockStoryObjectMap([
|
|
2062
|
+
createMockStoryObjectMap([issueWithEmptyAuthor, issueWithKnownAuthor]),
|
|
2061
2063
|
);
|
|
2062
2064
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2063
2065
|
stdout: '',
|
|
@@ -2078,33 +2080,26 @@ describe('StartPreparationUseCase', () => {
|
|
|
2078
2080
|
allowIssueCacheMinutes: 0,
|
|
2079
2081
|
});
|
|
2080
2082
|
|
|
2081
|
-
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(
|
|
2082
|
-
expect(
|
|
2083
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
|
|
2084
|
+
expect(mockIssueRepository.updateStatus.mock.calls[0][1]).toMatchObject({
|
|
2085
|
+
url: 'https://github.com/user/repo/issues/2',
|
|
2086
|
+
});
|
|
2087
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
|
|
2083
2088
|
});
|
|
2084
2089
|
|
|
2085
|
-
it('should
|
|
2086
|
-
const
|
|
2090
|
+
it('should skip issue with empty author when allowedIssueAuthors is set', async () => {
|
|
2091
|
+
const issueWithEmptyAuthor = createMockIssue({
|
|
2087
2092
|
url: 'https://github.com/user/repo/issues/1',
|
|
2088
|
-
title: 'Issue
|
|
2093
|
+
title: 'Issue with empty author',
|
|
2089
2094
|
labels: [],
|
|
2090
2095
|
status: 'Awaiting Workspace',
|
|
2091
2096
|
author: '',
|
|
2092
2097
|
});
|
|
2093
2098
|
|
|
2094
|
-
const storyObjectMap: StoryObjectMap = new Map();
|
|
2095
|
-
storyObjectMap.set('Default Story', {
|
|
2096
|
-
story: {
|
|
2097
|
-
id: 'story-1',
|
|
2098
|
-
name: 'Default Story',
|
|
2099
|
-
color: 'GRAY',
|
|
2100
|
-
description: '',
|
|
2101
|
-
},
|
|
2102
|
-
storyIssue: null,
|
|
2103
|
-
issues: [issueWithoutAuthor],
|
|
2104
|
-
});
|
|
2105
|
-
|
|
2106
2099
|
mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
|
|
2107
|
-
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2100
|
+
mockIssueRepository.getStoryObjectMap.mockResolvedValue(
|
|
2101
|
+
createMockStoryObjectMap([issueWithEmptyAuthor]),
|
|
2102
|
+
);
|
|
2108
2103
|
mockLocalCommandRunner.runCommand.mockResolvedValue({
|
|
2109
2104
|
stdout: '',
|
|
2110
2105
|
stderr: '',
|
|
@@ -2124,8 +2119,8 @@ describe('StartPreparationUseCase', () => {
|
|
|
2124
2119
|
allowIssueCacheMinutes: 0,
|
|
2125
2120
|
});
|
|
2126
2121
|
|
|
2127
|
-
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(
|
|
2128
|
-
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(
|
|
2122
|
+
expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
|
|
2123
|
+
expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(0);
|
|
2129
2124
|
});
|
|
2130
2125
|
|
|
2131
2126
|
it('should not pass --codexHome when codexHomeCandidates is null', async () => {
|
|
@@ -6,7 +6,6 @@ export declare class GitHubIssueCommentRepository implements IssueCommentReposit
|
|
|
6
6
|
constructor(token: string);
|
|
7
7
|
private parseIssueUrl;
|
|
8
8
|
getCommentsFromIssue(issue: Issue): Promise<Comment[]>;
|
|
9
|
-
private getIssueNodeId;
|
|
10
9
|
createComment(issue: Issue, commentContent: string): Promise<void>;
|
|
11
10
|
}
|
|
12
11
|
//# sourceMappingURL=GitHubIssueCommentRepository.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GitHubIssueCommentRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubIssueCommentRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"GitHubIssueCommentRepository.d.ts","sourceRoot":"","sources":["../../../src/adapter/repositories/GitHubIssueCommentRepository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,KAAK,EAAE,MAAM,6BAA6B,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,+BAA+B,CAAC;AAexD,qBAAa,4BAA6B,YAAW,sBAAsB;IAC7D,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,MAAM;IAE1C,OAAO,CAAC,aAAa;IAoBf,oBAAoB,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IA8CtD,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAsBzE"}
|