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.
Files changed (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +2 -2
  3. package/bin/adapter/repositories/GitHubIssueCommentRepository.js +5 -83
  4. package/bin/adapter/repositories/GitHubIssueCommentRepository.js.map +1 -1
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +1 -1
  6. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  7. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +11 -0
  8. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  9. package/bin/domain/entities/WorkflowStatus.js +6 -1
  10. package/bin/domain/entities/WorkflowStatus.js.map +1 -1
  11. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +7 -2
  12. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  13. package/bin/domain/usecases/StartPreparationUseCase.js +0 -1
  14. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  15. package/package.json +1 -1
  16. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +102 -0
  17. package/src/adapter/repositories/GitHubIssueCommentRepository.ts +13 -134
  18. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +3 -1
  19. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +1 -1
  20. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +16 -0
  21. package/src/domain/entities/WorkflowStatus.ts +5 -0
  22. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +11 -5
  23. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +12 -2
  24. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +10 -1
  25. package/src/domain/usecases/StartPreparationUseCase.test.ts +21 -26
  26. package/src/domain/usecases/StartPreparationUseCase.ts +0 -1
  27. package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts +0 -1
  28. package/types/adapter/repositories/GitHubIssueCommentRepository.d.ts.map +1 -1
  29. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  30. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  31. package/types/domain/entities/WorkflowStatus.d.ts +1 -0
  32. package/types/domain/entities/WorkflowStatus.d.ts.map +1 -1
  33. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  34. 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 issueId = await this.getIssueNodeId(issue);
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('https://api.github.com/graphql', {
199
- method: 'POST',
200
- headers: {
201
- Authorization: `Bearer ${this.token}`,
202
- 'Content-Type': 'application/json',
203
- },
204
- body: JSON.stringify({
205
- query: mutation,
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 GraphQL API: ${response.status} ${response.statusText}`,
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 Awaiting Quality Check after threshold rejections', async () => {
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: 'Awaiting Quality Check',
672
+ status: 'Failed Preparation',
667
673
  }),
668
674
  mockProject,
669
675
  );
670
676
  expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
671
677
  mockProject,
672
- expect.objectContaining({ status: 'Awaiting Quality Check' }),
673
- 'awaiting-quality-check-id',
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: 'Awaiting Quality Check' }),
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 = AWAITING_QUALITY_CHECK_STATUS_NAME;
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
- awaitingQualityCheckStatusOption.id,
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 10 required statuses in the documented order with the documented colors and no descriptions', () => {
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 not skip issues with empty author (tower defence issues)', async () => {
2043
- const towerDefenceIssue = createMockIssue({
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: 'Tower defence issue',
2045
+ title: 'Issue with empty author',
2046
2046
  labels: [],
2047
2047
  status: 'Awaiting Workspace',
2048
2048
  author: '',
2049
2049
  });
2050
- const normalIssue = createMockIssue({
2050
+ const issueWithKnownAuthor = createMockIssue({
2051
2051
  url: 'https://github.com/user/repo/issues/2',
2052
- title: 'Normal issue',
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([towerDefenceIssue, normalIssue]),
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(2);
2082
- expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(2);
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 not skip issues without author property when allowedIssueAuthors is set', async () => {
2086
- const issueWithoutAuthor = createMockIssue({
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 without author property',
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(storyObjectMap);
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(1);
2128
- expect(mockLocalCommandRunner.runCommand.mock.calls).toHaveLength(1);
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 () => {
@@ -149,7 +149,6 @@ export class StartPreparationUseCase {
149
149
  }
150
150
  if (
151
151
  params.allowedIssueAuthors !== null &&
152
- issue.author !== '' &&
153
152
  !params.allowedIssueAuthors.includes(issue.author)
154
153
  ) {
155
154
  continue;
@@ -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;AAqDxD,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;YA8C9C,cAAc;IAuDtB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAkDzE"}
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"}
@@ -12,6 +12,7 @@ export type ProjectItem = {
12
12
  labels: string[];
13
13
  assignees: string[];
14
14
  createdAt: string;
15
+ author: string;
15
16
  customFields: {
16
17
  name: string;
17
18
  value: string | null;