github-issue-tower-defence-management 1.175.1 → 1.177.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 (77) hide show
  1. package/.github/workflows/umino-project.yml +4 -107
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +2 -0
  4. package/bin/adapter/entry-points/cli/index.js +2 -0
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +6 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/ui-dist/assets/{index-BhDothvr.js → index-D6aAQrf7.js} +2 -2
  9. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -0
  11. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  12. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +1 -0
  13. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  14. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +6 -0
  15. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  16. package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js +3 -0
  17. package/bin/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.js.map +1 -1
  18. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +1 -1
  19. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  20. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -0
  21. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  22. package/bin/domain/usecases/IssueRejectionEvaluator.js +4 -3
  23. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  24. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +3 -3
  25. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  26. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -1
  27. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  28. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +23 -10
  29. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/adapter/entry-points/cli/index.test.ts +4 -0
  32. package/src/adapter/entry-points/cli/index.ts +2 -0
  33. package/src/adapter/entry-points/cli/projectConfig.test.ts +58 -0
  34. package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleItemList.stories.tsx +36 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.test.ts +88 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/logic/grouping.ts +16 -4
  38. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +13 -3
  39. package/src/adapter/entry-points/console/ui-dist/assets/{index-BhDothvr.js → index-D6aAQrf7.js} +2 -2
  40. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  41. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
  42. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +5 -0
  43. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +1 -0
  44. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +12 -5
  45. package/src/domain/entities/Issue.ts +1 -0
  46. package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.test.ts +49 -0
  47. package/src/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.ts +4 -0
  48. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -0
  49. package/src/domain/usecases/HandleScheduledEventUseCase.ts +2 -0
  50. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +55 -0
  51. package/src/domain/usecases/IssueRejectionEvaluator.ts +11 -2
  52. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +31 -0
  53. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +4 -0
  54. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +3 -0
  55. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +138 -0
  56. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +41 -17
  57. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  58. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  59. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  60. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  61. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  62. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  63. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  64. package/types/domain/entities/Issue.d.ts +1 -0
  65. package/types/domain/entities/Issue.d.ts.map +1 -1
  66. package/types/domain/usecases/AssignNoAssigneeIssueToManagerUseCase.d.ts.map +1 -1
  67. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +1 -0
  68. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  69. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -0
  70. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  71. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +2 -1
  72. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  73. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
  74. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  75. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts +1 -0
  76. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  77. package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
@@ -4,7 +4,7 @@
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-BhDothvr.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-D6aAQrf7.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/assets/index-Bh2slMtk.css">
9
9
  </head>
10
10
  <body>
@@ -297,6 +297,8 @@ export class HandleScheduledEventUseCaseHandler {
297
297
  input.claudeCodeOauthTokenListJsonPath,
298
298
  thresholdForAutoReject:
299
299
  readmeConfig.thresholdForAutoReject ?? input.thresholdForAutoReject,
300
+ developerAgentName:
301
+ readmeConfig.developerAgentName ?? input.developerAgentName ?? null,
300
302
  startPreparation: input.startPreparation
301
303
  ? {
302
304
  ...input.startPreparation,
@@ -72,6 +72,7 @@ const buildProjectItem = (url: string, title: string): ProjectItem => ({
72
72
  updatedAt: '2026-07-06T00:00:00.000Z',
73
73
  author: '',
74
74
  closingIssueReferenceUrls: [],
75
+ isRepoArchived: false,
75
76
  customFields: [],
76
77
  });
77
78
 
@@ -116,6 +117,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
116
117
  closingIssueReferenceUrls: [
117
118
  'https://github.com/HiromiShikata/test-repository/issues/7',
118
119
  ],
120
+ isRepoArchived: false,
119
121
  customFields: [
120
122
  { name: 'nextActionDate', value: '2000-01-01' },
121
123
  { name: 'nextActionHour', value: '1' },
@@ -153,6 +155,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
153
155
  'https://github.com/HiromiShikata/test-repository/issues/7',
154
156
  ],
155
157
  agent: null,
158
+ isRepoArchived: false,
156
159
  },
157
160
  },
158
161
  {
@@ -172,6 +175,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
172
175
  updatedAt: '2024-01-02T00:00:00Z',
173
176
  author: '',
174
177
  closingIssueReferenceUrls: [],
178
+ isRepoArchived: false,
175
179
  customFields: [
176
180
  {
177
181
  name: 'DependedIssueUrls',
@@ -210,6 +214,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
210
214
  author: '',
211
215
  closingIssueReferenceUrls: [],
212
216
  agent: null,
217
+ isRepoArchived: false,
213
218
  },
214
219
  },
215
220
  ];
@@ -686,6 +686,7 @@ export class ApiV3CheerioRestIssueRepository
686
686
  author: item.author,
687
687
  closingIssueReferenceUrls: item.closingIssueReferenceUrls,
688
688
  agent,
689
+ isRepoArchived: item.isRepoArchived,
689
690
  };
690
691
  };
691
692
  private restoreIssuesFromCache = (rawIssues: unknown): Issue[] | null => {
@@ -17,6 +17,7 @@ export type ProjectItem = {
17
17
  updatedAt: string;
18
18
  author: string;
19
19
  closingIssueReferenceUrls: string[];
20
+ isRepoArchived: boolean;
20
21
  customFields: {
21
22
  name: string;
22
23
  value: string | null;
@@ -38,7 +39,7 @@ type ProjectV2ItemFieldValueNode = {
38
39
  };
39
40
  };
40
41
  type ProjectV2ItemContentNode = {
41
- repository: { nameWithOwner: string };
42
+ repository: { nameWithOwner: string; isArchived: boolean };
42
43
  number: number;
43
44
  title: string;
44
45
  state: string;
@@ -139,6 +140,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
139
140
  }
140
141
  repository {
141
142
  nameWithOwner
143
+ isArchived
142
144
  }
143
145
  }
144
146
  ... on PullRequest {
@@ -163,6 +165,7 @@ const PROJECT_V2_ITEM_FIELD_VALUES_AND_CONTENT_SELECTION = `
163
165
  }
164
166
  repository {
165
167
  nameWithOwner
168
+ isArchived
166
169
  }
167
170
  closingIssuesReferences(first: 50) {
168
171
  nodes {
@@ -393,7 +396,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
393
396
  }[];
394
397
  };
395
398
  content: {
396
- repository: { nameWithOwner: string };
399
+ repository: { nameWithOwner: string; isArchived: boolean };
397
400
  number: number;
398
401
  title: string;
399
402
  state: string;
@@ -443,7 +446,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
443
446
  }[];
444
447
  };
445
448
  content: {
446
- repository: { nameWithOwner: string };
449
+ repository: { nameWithOwner: string; isArchived: boolean };
447
450
  number: number;
448
451
  title: string;
449
452
  state: string;
@@ -506,7 +509,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
506
509
  }[];
507
510
  };
508
511
  content: {
509
- repository: { nameWithOwner: string };
512
+ repository: { nameWithOwner: string; isArchived: boolean };
510
513
  number: number;
511
514
  title: string;
512
515
  state: string;
@@ -630,6 +633,7 @@ query GetProjectItems($projectId: ID!, $after: String, $first: Int!, $query: Str
630
633
  item.content.closingIssuesReferences?.nodes
631
634
  ?.map((node) => node.url)
632
635
  .filter((url) => url.length > 0) || [],
636
+ isRepoArchived: item.content.repository.isArchived ?? false,
633
637
  customFields: item.fieldValues.nodes
634
638
  .filter((field) => !!field.field)
635
639
  .map((field) => {
@@ -1064,6 +1068,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1064
1068
  }
1065
1069
  repository {
1066
1070
  nameWithOwner
1071
+ isArchived
1067
1072
  }
1068
1073
  projectItems(first: 10) {
1069
1074
  nodes {
@@ -1142,6 +1147,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1142
1147
  }
1143
1148
  repository {
1144
1149
  nameWithOwner
1150
+ isArchived
1145
1151
  }
1146
1152
  closingIssuesReferences(first: 50) {
1147
1153
  nodes {
@@ -1223,7 +1229,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1223
1229
  author: { login: string } | null;
1224
1230
  labels: { nodes: { name: string }[] };
1225
1231
  assignees: { nodes: { login: string }[] };
1226
- repository: { nameWithOwner: string };
1232
+ repository: { nameWithOwner: string; isArchived: boolean };
1227
1233
  closingIssuesReferences?: { nodes: { url: string }[] };
1228
1234
  projectItems: {
1229
1235
  nodes: {
@@ -1302,6 +1308,7 @@ query GetProjectFields($owner: String!, $repository: String!, $issueNumber: Int!
1302
1308
  content.closingIssuesReferences?.nodes
1303
1309
  ?.map((node) => node.url)
1304
1310
  .filter((url) => url.length > 0) || [],
1311
+ isRepoArchived: content.repository.isArchived ?? false,
1305
1312
  customFields: item.fieldValues.nodes
1306
1313
  .filter((field) => !!field.field)
1307
1314
  .map((field) => {
@@ -26,4 +26,5 @@ export type Issue = {
26
26
  author: string;
27
27
  closingIssueReferenceUrls: string[];
28
28
  agent: string | null;
29
+ isRepoArchived?: boolean;
29
30
  };
@@ -507,6 +507,55 @@ describe('AssignNoAssigneeIssueToManagerUseCase', () => {
507
507
  });
508
508
  });
509
509
 
510
+ it('should skip issues in archived repositories', async () => {
511
+ const archivedIssue = {
512
+ ...basicIssue,
513
+ isRepoArchived: true,
514
+ };
515
+
516
+ await useCase.run({
517
+ issues: [archivedIssue],
518
+ manager: 'manager1',
519
+ cacheUsed: false,
520
+ });
521
+
522
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([]);
523
+ });
524
+
525
+ it('should assign manager to issues in non-archived repositories', async () => {
526
+ const nonArchivedIssue = {
527
+ ...basicIssue,
528
+ isRepoArchived: false,
529
+ };
530
+
531
+ await useCase.run({
532
+ issues: [nonArchivedIssue],
533
+ manager: 'manager1',
534
+ cacheUsed: false,
535
+ });
536
+
537
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
538
+ [nonArchivedIssue, ['manager1']],
539
+ ]);
540
+ });
541
+
542
+ it('should assign manager to issues where isRepoArchived is undefined', async () => {
543
+ const issueWithoutArchivedFlag = {
544
+ ...basicIssue,
545
+ isRepoArchived: undefined,
546
+ };
547
+
548
+ await useCase.run({
549
+ issues: [issueWithoutArchivedFlag],
550
+ manager: 'manager1',
551
+ cacheUsed: false,
552
+ });
553
+
554
+ expect(mockIssueRepository.updateAssigneeList.mock.calls).toEqual([
555
+ [issueWithoutArchivedFlag, ['manager1']],
556
+ ]);
557
+ });
558
+
510
559
  it('should rethrow non-Error thrown values without logging', async () => {
511
560
  const failingIssue = {
512
561
  ...basicIssue,
@@ -30,7 +30,11 @@ export class AssignNoAssigneeIssueToManagerUseCase {
30
30
  assignees: Member['name'][];
31
31
  state: Issue['state'];
32
32
  author: string;
33
+ isRepoArchived?: boolean;
33
34
  }): boolean => {
35
+ if (target.isRepoArchived) {
36
+ return false;
37
+ }
34
38
  if (target.assignees.length > 0 || target.state !== 'OPEN') {
35
39
  return false;
36
40
  }
@@ -44,6 +44,7 @@ export class CheckIssueReviewReadinessUseCase {
44
44
  allowedIssueAuthors?: string[] | null;
45
45
  labelsAsLlmAgentName?: string[] | null;
46
46
  labelsNotRequiringPullRequest?: string[] | null;
47
+ developerAgentName?: string | null;
47
48
  }): Promise<IssueReviewReadinessResult> => {
48
49
  const issue = await this.issueRepository.getIssueByUrl(params.issueUrl);
49
50
 
@@ -88,6 +89,7 @@ export class CheckIssueReviewReadinessUseCase {
88
89
  await this.issueRejectionEvaluator.evaluate(
89
90
  issue,
90
91
  resolveLabelsNotRequiringPullRequest(params),
92
+ { developerAgentName: params.developerAgentName },
91
93
  );
92
94
 
93
95
  const requiredPrRejections = isPullRequestDeclaredUnnecessary(
@@ -188,6 +188,7 @@ export class HandleScheduledEventUseCase {
188
188
  queryToAddProject?: string | null;
189
189
  storyProgressCommentEnabled?: boolean;
190
190
  dailySecurityScan?: DailySecurityScanConfig | null;
191
+ developerAgentName?: string | null;
191
192
  }): Promise<{
192
193
  project: Project;
193
194
  issues: Issue[];
@@ -436,6 +437,7 @@ ${JSON.stringify(e)}
436
437
  labelsNotRequiringPullRequest: input.labelsNotRequiringPullRequest,
437
438
  changeTargetPathAliases: input.changeTargetPathAliases,
438
439
  allowedIssueAuthors,
440
+ developerAgentName: input.developerAgentName,
439
441
  });
440
442
  await this.triagerApprovalDispatchUseCase.run({
441
443
  projectUrl: input.projectUrl,
@@ -203,6 +203,61 @@ describe('IssueRejectionEvaluator', () => {
203
203
  expect(result.approvedPrUrl).toBeNull();
204
204
  });
205
205
 
206
+ it('should require PR evaluation for custom developer agent name when it matches issue agent', async () => {
207
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
208
+
209
+ const result = await evaluator.evaluate(
210
+ {
211
+ url: 'https://github.com/user/repo/issues/1',
212
+ labels: [],
213
+ isPr: false,
214
+ agent: 'my-developer',
215
+ },
216
+ [],
217
+ { developerAgentName: 'my-developer' },
218
+ );
219
+
220
+ expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
221
+ expect(result.rejections).toHaveLength(1);
222
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
223
+ });
224
+
225
+ it('should not require PR evaluation when issue agent does not match custom developer agent name', async () => {
226
+ const result = await evaluator.evaluate(
227
+ {
228
+ url: 'https://github.com/user/repo/issues/1',
229
+ labels: [],
230
+ isPr: false,
231
+ agent: 'other-agent',
232
+ },
233
+ [],
234
+ { developerAgentName: 'my-developer' },
235
+ );
236
+
237
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
238
+ expect(result.rejections).toHaveLength(0);
239
+ expect(result.approvedPrUrl).toBeNull();
240
+ });
241
+
242
+ it('should default to developer when developerAgentName is null', async () => {
243
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
244
+
245
+ const result = await evaluator.evaluate(
246
+ {
247
+ url: 'https://github.com/user/repo/issues/1',
248
+ labels: [],
249
+ isPr: false,
250
+ agent: 'developer',
251
+ },
252
+ [],
253
+ { developerAgentName: null },
254
+ );
255
+
256
+ expect(mockIssueRepository.findRelatedOpenPRs).toHaveBeenCalled();
257
+ expect(result.rejections).toHaveLength(1);
258
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_NOT_FOUND');
259
+ });
260
+
206
261
  it('should not reject for draft state when issue has non-e2e category label', async () => {
207
262
  const result = await evaluator.evaluate({
208
263
  url: 'https://github.com/user/repo/issues/1',
@@ -34,6 +34,9 @@ export type EvaluateOptions = {
34
34
  // getOpenPullRequest call, which keeps the transient-failure handling that
35
35
  // distinguishes an unknown state from an absent pull request.
36
36
  resolvedOpenPrByUrl?: ReadonlyMap<string, RelatedPullRequest | null> | null;
37
+ // The agent name treated as the developer agent for PR/CI/conflict checks.
38
+ // When null or undefined, defaults to 'developer'.
39
+ developerAgentName?: string | null;
37
40
  };
38
41
 
39
42
  export class IssueRejectionEvaluator {
@@ -62,7 +65,11 @@ export class IssueRejectionEvaluator {
62
65
  let approvedPrUrl: string | null = null;
63
66
 
64
67
  if (
65
- this.requiresPullRequestEvaluation(issue, labelsNotRequiringPullRequest)
68
+ this.requiresPullRequestEvaluation(
69
+ issue,
70
+ labelsNotRequiringPullRequest,
71
+ options.developerAgentName,
72
+ )
66
73
  ) {
67
74
  let prsToCheck: RelatedPullRequest[];
68
75
  let anyPrResolutionFailed = false;
@@ -190,12 +197,14 @@ export class IssueRejectionEvaluator {
190
197
  requiresPullRequestEvaluation = (
191
198
  issue: { labels: string[]; body?: string | null; agent?: string | null },
192
199
  labelsNotRequiringPullRequest: string[] = [],
200
+ developerAgentName?: string | null,
193
201
  ): boolean => {
194
202
  const categoryLabels = issue.labels.filter((label) =>
195
203
  label.startsWith('category:'),
196
204
  );
205
+ const effectiveDeveloperAgentName = developerAgentName ?? 'developer';
197
206
  const isNonDeveloperAgent =
198
- issue.agent != null && issue.agent !== 'developer';
207
+ issue.agent != null && issue.agent !== effectiveDeveloperAgentName;
199
208
  const hasLabelNotRequiringPullRequest = issue.labels.some((label) =>
200
209
  labelsNotRequiringPullRequest.includes(label),
201
210
  );
@@ -1703,6 +1703,37 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1703
1703
  );
1704
1704
  });
1705
1705
 
1706
+ it('should reject a missing PR when the issue agent matches developerAgentName', async () => {
1707
+ const issue = createMockIssue({
1708
+ url: 'https://github.com/user/repo/issues/1',
1709
+ status: 'Preparation',
1710
+ agent: 'my-agent',
1711
+ });
1712
+
1713
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1714
+ mockIssueRepository.get.mockResolvedValue(issue);
1715
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1716
+ createMockComment({ content: 'From: :robot: Test report' }),
1717
+ ]);
1718
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
1719
+
1720
+ await useCase.run({
1721
+ projectUrl: 'https://github.com/users/user/projects/1',
1722
+ issueUrl: 'https://github.com/user/repo/issues/1',
1723
+ thresholdForAutoReject: 3,
1724
+ workflowBlockerResolvedWebhookUrl: null,
1725
+ allowedIssueAuthors: null,
1726
+ developerAgentName: 'my-agent',
1727
+ });
1728
+
1729
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1730
+ expect.objectContaining({
1731
+ url: 'https://github.com/user/repo/issues/1',
1732
+ }),
1733
+ expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
1734
+ );
1735
+ });
1736
+
1706
1737
  it('should reject when multiple PRs are found', async () => {
1707
1738
  const issue = createMockIssue({
1708
1739
  url: 'https://github.com/user/repo/issues/1',
@@ -109,6 +109,7 @@ export class NotifyFinishedIssuePreparationUseCase {
109
109
  missingAgentName?: string | null;
110
110
  sessionErrorLine?: string | null;
111
111
  manager?: string | null;
112
+ developerAgentName?: string | null;
112
113
  }): Promise<void> => {
113
114
  const project = await this.projectRepository.getByUrl(params.projectUrl);
114
115
 
@@ -256,6 +257,7 @@ export class NotifyFinishedIssuePreparationUseCase {
256
257
  comments,
257
258
  isTrustedAuthor,
258
259
  resolveLabelsNotRequiringPullRequest(params),
260
+ params.developerAgentName,
259
261
  );
260
262
 
261
263
  const rejectionStatusMessage =
@@ -468,6 +470,7 @@ export class NotifyFinishedIssuePreparationUseCase {
468
470
  comments: { author: string; content: string }[],
469
471
  isTrustedAuthor: (author: string) => boolean,
470
472
  labelsNotRequiringPullRequest: string[],
473
+ developerAgentName?: string | null,
471
474
  ): Promise<{
472
475
  rejections: { type: RejectedReasonType; detail: string }[];
473
476
  approvedPrUrl: string | null;
@@ -495,6 +498,7 @@ export class NotifyFinishedIssuePreparationUseCase {
495
498
  await this.issueRejectionEvaluator.evaluate(
496
499
  issue,
497
500
  labelsNotRequiringPullRequest,
501
+ { developerAgentName },
498
502
  );
499
503
  const requiredPrRejections = isPullRequestDeclaredUnnecessary(
500
504
  comments,
@@ -64,6 +64,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
64
64
  labelsNotRequiringPullRequest?: string[] | null;
65
65
  changeTargetPathAliases?: Record<string, string> | null;
66
66
  allowedIssueAuthors?: string[] | null;
67
+ developerAgentName?: string | null;
67
68
  }): Promise<void> => {
68
69
  const allowedIssueAuthors = params.allowedIssueAuthors ?? null;
69
70
  const projectId = await this.projectRepository.findProjectIdByUrl(
@@ -102,6 +103,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
102
103
  this.issueRejectionEvaluator.requiresPullRequestEvaluation(
103
104
  item,
104
105
  labelsNotRequiringPullRequest,
106
+ params.developerAgentName,
105
107
  );
106
108
 
107
109
  const resolvedOpenPrByUrl = await this.issueRepository.getOpenPullRequests(
@@ -132,6 +134,7 @@ export class RevertNotReadyReviewQueueIssueUseCase {
132
134
  relatedOpenPrUrls:
133
135
  relatedOpenPrUrlsByIssueUrl.get(issue.url) ?? null,
134
136
  resolvedOpenPrByUrl,
137
+ developerAgentName: params.developerAgentName,
135
138
  },
136
139
  );
137
140
  if (
@@ -1013,6 +1013,144 @@ describe('SetupTowerDefenceProjectUseCase', () => {
1013
1013
  ).toBe(false);
1014
1014
  });
1015
1015
 
1016
+ it('should move open issue with Done status to Awaiting Workspace even when project has canonical statuses', async () => {
1017
+ const mockProjectRepository =
1018
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
1019
+ const mockIssueRepository =
1020
+ mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
1021
+ const canonicalStatuses = buildCanonicalStatuses();
1022
+ const awaitingWorkspaceId = canonicalStatuses[0].id;
1023
+ const project = buildProject(canonicalStatuses);
1024
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
1025
+ const openDoneIssue = buildIssue({
1026
+ number: 10,
1027
+ url: 'https://github.com/test-org/test-repo/issues/10',
1028
+ itemId: 'item-10',
1029
+ state: 'OPEN',
1030
+ status: DONE_STATUS_NAME,
1031
+ });
1032
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1033
+ project: mock<Project>(),
1034
+ issues: [openDoneIssue],
1035
+ cacheUsed: false,
1036
+ });
1037
+ mockIssueRepository.updateStatus.mockResolvedValue(undefined);
1038
+
1039
+ const useCase = new SetupTowerDefenceProjectUseCase(
1040
+ mockProjectRepository,
1041
+ mockIssueRepository,
1042
+ );
1043
+ await useCase.run({ projectUrl: project.url });
1044
+
1045
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(1);
1046
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1047
+ project,
1048
+ openDoneIssue,
1049
+ awaitingWorkspaceId,
1050
+ );
1051
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
1052
+ });
1053
+
1054
+ it('should move open issue with null status to Awaiting Workspace even when project has canonical statuses', async () => {
1055
+ const mockProjectRepository =
1056
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
1057
+ const mockIssueRepository =
1058
+ mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
1059
+ const canonicalStatuses = buildCanonicalStatuses();
1060
+ const awaitingWorkspaceId = canonicalStatuses[0].id;
1061
+ const project = buildProject(canonicalStatuses);
1062
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
1063
+ const openNullStatusIssue = buildIssue({
1064
+ number: 11,
1065
+ url: 'https://github.com/test-org/test-repo/issues/11',
1066
+ itemId: 'item-11',
1067
+ state: 'OPEN',
1068
+ status: null,
1069
+ });
1070
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1071
+ project: mock<Project>(),
1072
+ issues: [openNullStatusIssue],
1073
+ cacheUsed: false,
1074
+ });
1075
+ mockIssueRepository.updateStatus.mockResolvedValue(undefined);
1076
+
1077
+ const useCase = new SetupTowerDefenceProjectUseCase(
1078
+ mockProjectRepository,
1079
+ mockIssueRepository,
1080
+ );
1081
+ await useCase.run({ projectUrl: project.url });
1082
+
1083
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(1);
1084
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
1085
+ project,
1086
+ openNullStatusIssue,
1087
+ awaitingWorkspaceId,
1088
+ );
1089
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
1090
+ });
1091
+
1092
+ it('should not move closed issue with Done status to Awaiting Workspace', async () => {
1093
+ const mockProjectRepository =
1094
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
1095
+ const mockIssueRepository =
1096
+ mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
1097
+ const project = buildProject(buildCanonicalStatuses());
1098
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
1099
+ const closedDoneIssue = buildIssue({
1100
+ number: 12,
1101
+ url: 'https://github.com/test-org/test-repo/issues/12',
1102
+ itemId: 'item-12',
1103
+ state: 'CLOSED',
1104
+ isClosed: true,
1105
+ status: DONE_STATUS_NAME,
1106
+ });
1107
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1108
+ project: mock<Project>(),
1109
+ issues: [closedDoneIssue],
1110
+ cacheUsed: false,
1111
+ });
1112
+
1113
+ const useCase = new SetupTowerDefenceProjectUseCase(
1114
+ mockProjectRepository,
1115
+ mockIssueRepository,
1116
+ );
1117
+ await useCase.run({ projectUrl: project.url });
1118
+
1119
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1120
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
1121
+ });
1122
+
1123
+ it('should not move closed issue with null status to Awaiting Workspace', async () => {
1124
+ const mockProjectRepository =
1125
+ mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
1126
+ const mockIssueRepository =
1127
+ mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
1128
+ const project = buildProject(buildCanonicalStatuses());
1129
+ mockProjectRepository.getByUrl.mockResolvedValue(project);
1130
+ const closedNullStatusIssue = buildIssue({
1131
+ number: 13,
1132
+ url: 'https://github.com/test-org/test-repo/issues/13',
1133
+ itemId: 'item-13',
1134
+ state: 'CLOSED',
1135
+ isClosed: true,
1136
+ status: null,
1137
+ });
1138
+ mockIssueRepository.getAllIssues.mockResolvedValue({
1139
+ project: mock<Project>(),
1140
+ issues: [closedNullStatusIssue],
1141
+ cacheUsed: false,
1142
+ });
1143
+
1144
+ const useCase = new SetupTowerDefenceProjectUseCase(
1145
+ mockProjectRepository,
1146
+ mockIssueRepository,
1147
+ );
1148
+ await useCase.run({ projectUrl: project.url });
1149
+
1150
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
1151
+ expect(mockProjectRepository.updateStatusList).not.toHaveBeenCalled();
1152
+ });
1153
+
1016
1154
  it('should migrate issues from Unread to Awaiting Workspace when both statuses exist in the project', async () => {
1017
1155
  const mockProjectRepository =
1018
1156
  mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();