github-issue-tower-defence-management 1.189.2 → 1.191.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 (57) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/cli/index.js +5 -0
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +3 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/domain/usecases/ConflictedIssueRevertUseCase.js +84 -0
  10. package/bin/domain/usecases/ConflictedIssueRevertUseCase.js.map +1 -0
  11. package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -1
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  13. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +23 -0
  14. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  15. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +19 -0
  16. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/StartPreparationUseCase.js +9 -5
  18. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  19. package/bin/domain/usecases/awaitingOwnerApprovalMessage.js +7 -0
  20. package/bin/domain/usecases/awaitingOwnerApprovalMessage.js.map +1 -0
  21. package/bin/domain/usecases/isWaitingForOwnerApproval.js +16 -0
  22. package/bin/domain/usecases/isWaitingForOwnerApproval.js.map +1 -0
  23. package/package.json +1 -1
  24. package/src/adapter/entry-points/cli/index.test.ts +2 -0
  25. package/src/adapter/entry-points/cli/index.ts +9 -0
  26. package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
  27. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -0
  28. package/src/domain/usecases/ConflictedIssueRevertUseCase.test.ts +629 -0
  29. package/src/domain/usecases/ConflictedIssueRevertUseCase.ts +138 -0
  30. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +30 -0
  31. package/src/domain/usecases/HandleScheduledEventUseCase.ts +5 -0
  32. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +86 -0
  33. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +52 -0
  34. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +93 -0
  35. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +47 -1
  36. package/src/domain/usecases/StartPreparationUseCase.test.ts +76 -0
  37. package/src/domain/usecases/StartPreparationUseCase.ts +9 -5
  38. package/src/domain/usecases/awaitingOwnerApprovalMessage.ts +5 -0
  39. package/src/domain/usecases/isWaitingForOwnerApproval.test.ts +91 -0
  40. package/src/domain/usecases/isWaitingForOwnerApproval.ts +17 -0
  41. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  42. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  43. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  44. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  45. package/types/domain/usecases/ConflictedIssueRevertUseCase.d.ts +14 -0
  46. package/types/domain/usecases/ConflictedIssueRevertUseCase.d.ts.map +1 -0
  47. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  48. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  49. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +1 -0
  50. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  51. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +1 -0
  52. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  53. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  54. package/types/domain/usecases/awaitingOwnerApprovalMessage.d.ts +3 -0
  55. package/types/domain/usecases/awaitingOwnerApprovalMessage.d.ts.map +1 -0
  56. package/types/domain/usecases/isWaitingForOwnerApproval.d.ts +2 -0
  57. package/types/domain/usecases/isWaitingForOwnerApproval.d.ts.map +1 -0
@@ -0,0 +1,629 @@
1
+ import { ConflictedIssueRevertUseCase } from './ConflictedIssueRevertUseCase';
2
+ import { Issue } from '../entities/Issue';
3
+ import { Project } from '../entities/Project';
4
+ import { RelatedPullRequest } from './adapter-interfaces/IssueRepository';
5
+
6
+ const createMockProject = (overrides: Partial<Project> = {}): Project => ({
7
+ id: 'project-1',
8
+ url: 'https://github.com/users/user/projects/1',
9
+ databaseId: 1,
10
+ name: 'Test Project',
11
+ status: {
12
+ name: 'Status',
13
+ fieldId: 'field-1',
14
+ statuses: [
15
+ {
16
+ id: 'awaiting-workspace-id',
17
+ name: 'Awaiting Workspace',
18
+ color: 'GRAY',
19
+ description: '',
20
+ },
21
+ ],
22
+ },
23
+ nextActionDate: null,
24
+ nextActionHour: null,
25
+ story: null,
26
+ remainingEstimationMinutes: null,
27
+ dependedIssueUrlSeparatedByComma: null,
28
+ completionDate50PercentConfidence: null,
29
+ agent: null,
30
+ ...overrides,
31
+ });
32
+
33
+ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
34
+ nameWithOwner: 'user/repo',
35
+ number: 1,
36
+ title: 'Test Issue',
37
+ state: 'OPEN',
38
+ status: 'Preparation',
39
+ story: null,
40
+ nextActionDate: null,
41
+ nextActionHour: null,
42
+ estimationMinutes: null,
43
+ dependedIssueUrls: [],
44
+ completionDate50PercentConfidence: null,
45
+ url: 'https://github.com/user/repo/issues/1',
46
+ assignees: ['manager-user'],
47
+ labels: [],
48
+ org: 'user',
49
+ repo: 'repo',
50
+ body: '',
51
+ itemId: 'item-1',
52
+ isPr: false,
53
+ isInProgress: false,
54
+ isClosed: false,
55
+ createdAt: new Date(),
56
+ author: 'owner',
57
+ stateReason: null,
58
+ closingIssueReferenceUrls: [],
59
+ agent: null,
60
+ ...overrides,
61
+ });
62
+
63
+ const createMockPrItem = (overrides: Partial<Issue> = {}): Issue =>
64
+ createMockIssue({
65
+ title: 'Test PR',
66
+ url: 'https://github.com/user/repo/pull/1',
67
+ isPr: true,
68
+ status: 'Preparation',
69
+ ...overrides,
70
+ });
71
+
72
+ const createMockRelatedPullRequest = (
73
+ overrides: Partial<RelatedPullRequest> = {},
74
+ ): RelatedPullRequest => ({
75
+ url: 'https://github.com/user/repo/pull/1',
76
+ branchName: 'fix/issue-1',
77
+ createdAt: new Date(),
78
+ isDraft: false,
79
+ isConflicted: false,
80
+ mergeable: 'MERGEABLE',
81
+ isPassedAllCiJob: true,
82
+ isCiStateSuccess: true,
83
+ isResolvedAllReviewComments: true,
84
+ isBranchOutOfDate: false,
85
+ missingRequiredCheckNames: [],
86
+ ...overrides,
87
+ });
88
+
89
+ describe('ConflictedIssueRevertUseCase', () => {
90
+ const projectUrl = 'https://github.com/users/user/projects/1';
91
+
92
+ let mockProjectRepository: {
93
+ findProjectIdByUrl: jest.Mock;
94
+ getProject: jest.Mock;
95
+ };
96
+ let mockIssueRepository: {
97
+ getAllIssues: jest.Mock;
98
+ getOpenPullRequests: jest.Mock;
99
+ updateStatus: jest.Mock;
100
+ };
101
+ let mockIssueCommentRepository: {
102
+ createComment: jest.Mock;
103
+ };
104
+ let mockProject: Project;
105
+ let useCase: ConflictedIssueRevertUseCase;
106
+
107
+ beforeEach(() => {
108
+ jest.resetAllMocks();
109
+
110
+ mockProject = createMockProject();
111
+
112
+ mockProjectRepository = {
113
+ findProjectIdByUrl: jest.fn().mockResolvedValue('project-1'),
114
+ getProject: jest.fn().mockResolvedValue(mockProject),
115
+ };
116
+
117
+ mockIssueRepository = {
118
+ getAllIssues: jest.fn().mockResolvedValue({
119
+ project: mockProject,
120
+ issues: [],
121
+ cacheUsed: false,
122
+ }),
123
+ getOpenPullRequests: jest.fn().mockResolvedValue(new Map()),
124
+ updateStatus: jest.fn().mockResolvedValue(undefined),
125
+ };
126
+
127
+ mockIssueCommentRepository = {
128
+ createComment: jest.fn().mockResolvedValue(undefined),
129
+ };
130
+
131
+ useCase = new ConflictedIssueRevertUseCase(
132
+ mockProjectRepository,
133
+ mockIssueRepository,
134
+ mockIssueCommentRepository,
135
+ );
136
+ });
137
+
138
+ describe('status filtering', () => {
139
+ it('should do nothing when there are no issues', async () => {
140
+ await useCase.run({ projectUrl });
141
+
142
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
143
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
144
+ });
145
+
146
+ const excludedStatuses = [
147
+ 'Awaiting Workspace',
148
+ 'Done',
149
+ 'Icebox',
150
+ 'Failed Preparation',
151
+ 'In Tmux by human',
152
+ ];
153
+
154
+ excludedStatuses.forEach((status) => {
155
+ it(`should not process issues in ${status} status`, async () => {
156
+ const issue = createMockIssue({ status });
157
+ const prItem = createMockPrItem({
158
+ closingIssueReferenceUrls: [issue.url],
159
+ });
160
+ mockIssueRepository.getAllIssues.mockResolvedValue({
161
+ project: mockProject,
162
+ issues: [issue, prItem],
163
+ cacheUsed: false,
164
+ });
165
+
166
+ await useCase.run({ projectUrl });
167
+
168
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
169
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
170
+ });
171
+ });
172
+
173
+ it('should skip PR items when scanning for target task issues', async () => {
174
+ const prIssue = createMockIssue({
175
+ url: 'https://github.com/user/repo/issues/1',
176
+ status: 'Preparation',
177
+ isPr: false,
178
+ });
179
+ const prItem = createMockPrItem({
180
+ url: 'https://github.com/user/repo/pull/2',
181
+ status: 'Preparation',
182
+ isPr: true,
183
+ closingIssueReferenceUrls: [prIssue.url],
184
+ });
185
+ mockIssueRepository.getAllIssues.mockResolvedValue({
186
+ project: mockProject,
187
+ issues: [prIssue, prItem],
188
+ cacheUsed: false,
189
+ });
190
+ const conflictedPr = createMockRelatedPullRequest({
191
+ url: prItem.url,
192
+ isConflicted: true,
193
+ mergeable: 'CONFLICTING',
194
+ });
195
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
196
+ new Map([[conflictedPr.url, conflictedPr]]),
197
+ );
198
+
199
+ await useCase.run({ projectUrl });
200
+
201
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
202
+ mockProject,
203
+ prIssue,
204
+ 'awaiting-workspace-id',
205
+ );
206
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
207
+ prIssue,
208
+ 'conflict',
209
+ );
210
+ });
211
+ });
212
+
213
+ describe('conflict detection', () => {
214
+ it('should skip an issue with no linked open PRs', async () => {
215
+ const issue = createMockIssue({ status: 'Preparation' });
216
+ mockIssueRepository.getAllIssues.mockResolvedValue({
217
+ project: mockProject,
218
+ issues: [issue],
219
+ cacheUsed: false,
220
+ });
221
+
222
+ await useCase.run({ projectUrl });
223
+
224
+ expect(mockIssueRepository.getOpenPullRequests).not.toHaveBeenCalled();
225
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
226
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
227
+ });
228
+
229
+ it('should skip an issue whose linked PR is not conflicted', async () => {
230
+ const issue = createMockIssue({
231
+ url: 'https://github.com/user/repo/issues/1',
232
+ status: 'Preparation',
233
+ });
234
+ const prItem = createMockPrItem({
235
+ url: 'https://github.com/user/repo/pull/1',
236
+ closingIssueReferenceUrls: [issue.url],
237
+ });
238
+ mockIssueRepository.getAllIssues.mockResolvedValue({
239
+ project: mockProject,
240
+ issues: [issue, prItem],
241
+ cacheUsed: false,
242
+ });
243
+ const pr = createMockRelatedPullRequest({
244
+ url: prItem.url,
245
+ isConflicted: false,
246
+ mergeable: 'MERGEABLE',
247
+ });
248
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
249
+ new Map([[pr.url, pr]]),
250
+ );
251
+
252
+ await useCase.run({ projectUrl });
253
+
254
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
255
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
256
+ });
257
+
258
+ it('should update status and post conflict comment when linked PR is conflicted', async () => {
259
+ const issue = createMockIssue({
260
+ url: 'https://github.com/user/repo/issues/1',
261
+ status: 'Preparation',
262
+ });
263
+ const prItem = createMockPrItem({
264
+ url: 'https://github.com/user/repo/pull/1',
265
+ closingIssueReferenceUrls: [issue.url],
266
+ });
267
+ mockIssueRepository.getAllIssues.mockResolvedValue({
268
+ project: mockProject,
269
+ issues: [issue, prItem],
270
+ cacheUsed: false,
271
+ });
272
+ const conflictedPr = createMockRelatedPullRequest({
273
+ url: prItem.url,
274
+ isConflicted: true,
275
+ mergeable: 'CONFLICTING',
276
+ });
277
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
278
+ new Map([[conflictedPr.url, conflictedPr]]),
279
+ );
280
+
281
+ await useCase.run({ projectUrl });
282
+
283
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
284
+ mockProject,
285
+ issue,
286
+ 'awaiting-workspace-id',
287
+ );
288
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
289
+ issue,
290
+ 'conflict',
291
+ );
292
+ });
293
+
294
+ it('should post exactly the string conflict as the comment body', async () => {
295
+ const issue = createMockIssue({
296
+ url: 'https://github.com/user/repo/issues/1',
297
+ status: 'In Tmux by agent',
298
+ });
299
+ const prItem = createMockPrItem({
300
+ url: 'https://github.com/user/repo/pull/1',
301
+ closingIssueReferenceUrls: [issue.url],
302
+ });
303
+ mockIssueRepository.getAllIssues.mockResolvedValue({
304
+ project: mockProject,
305
+ issues: [issue, prItem],
306
+ cacheUsed: false,
307
+ });
308
+ const conflictedPr = createMockRelatedPullRequest({
309
+ url: prItem.url,
310
+ isConflicted: true,
311
+ mergeable: 'CONFLICTING',
312
+ });
313
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
314
+ new Map([[conflictedPr.url, conflictedPr]]),
315
+ );
316
+
317
+ await useCase.run({ projectUrl });
318
+
319
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
320
+ issue,
321
+ 'conflict',
322
+ );
323
+ });
324
+
325
+ it('should skip an issue whose linked PR has mergeable UNKNOWN', async () => {
326
+ const issue = createMockIssue({
327
+ url: 'https://github.com/user/repo/issues/1',
328
+ status: 'Preparation',
329
+ });
330
+ const prItem = createMockPrItem({
331
+ url: 'https://github.com/user/repo/pull/1',
332
+ closingIssueReferenceUrls: [issue.url],
333
+ });
334
+ mockIssueRepository.getAllIssues.mockResolvedValue({
335
+ project: mockProject,
336
+ issues: [issue, prItem],
337
+ cacheUsed: false,
338
+ });
339
+ const unknownPr = createMockRelatedPullRequest({
340
+ url: prItem.url,
341
+ isConflicted: false,
342
+ mergeable: 'UNKNOWN',
343
+ });
344
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
345
+ new Map([[unknownPr.url, unknownPr]]),
346
+ );
347
+
348
+ await useCase.run({ projectUrl });
349
+
350
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
351
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
352
+ });
353
+
354
+ it('should skip when any linked PR has mergeable UNKNOWN even if another is conflicted', async () => {
355
+ const issue = createMockIssue({
356
+ url: 'https://github.com/user/repo/issues/1',
357
+ status: 'Preparation',
358
+ });
359
+ const prItem1 = createMockPrItem({
360
+ url: 'https://github.com/user/repo/pull/1',
361
+ number: 1,
362
+ closingIssueReferenceUrls: [issue.url],
363
+ });
364
+ const prItem2 = createMockPrItem({
365
+ url: 'https://github.com/user/repo/pull/2',
366
+ number: 2,
367
+ closingIssueReferenceUrls: [issue.url],
368
+ });
369
+ mockIssueRepository.getAllIssues.mockResolvedValue({
370
+ project: mockProject,
371
+ issues: [issue, prItem1, prItem2],
372
+ cacheUsed: false,
373
+ });
374
+ const conflictedPr = createMockRelatedPullRequest({
375
+ url: prItem1.url,
376
+ isConflicted: true,
377
+ mergeable: 'CONFLICTING',
378
+ });
379
+ const unknownPr = createMockRelatedPullRequest({
380
+ url: prItem2.url,
381
+ isConflicted: false,
382
+ mergeable: 'UNKNOWN',
383
+ });
384
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
385
+ new Map([
386
+ [conflictedPr.url, conflictedPr],
387
+ [unknownPr.url, unknownPr],
388
+ ]),
389
+ );
390
+
391
+ await useCase.run({ projectUrl });
392
+
393
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
394
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
395
+ });
396
+
397
+ it('should process an issue when one of multiple linked PRs is conflicted and none is UNKNOWN', async () => {
398
+ const issue = createMockIssue({
399
+ url: 'https://github.com/user/repo/issues/1',
400
+ status: 'Preparation',
401
+ });
402
+ const prItem1 = createMockPrItem({
403
+ url: 'https://github.com/user/repo/pull/1',
404
+ number: 1,
405
+ closingIssueReferenceUrls: [issue.url],
406
+ });
407
+ const prItem2 = createMockPrItem({
408
+ url: 'https://github.com/user/repo/pull/2',
409
+ number: 2,
410
+ closingIssueReferenceUrls: [issue.url],
411
+ });
412
+ mockIssueRepository.getAllIssues.mockResolvedValue({
413
+ project: mockProject,
414
+ issues: [issue, prItem1, prItem2],
415
+ cacheUsed: false,
416
+ });
417
+ const conflictedPr = createMockRelatedPullRequest({
418
+ url: prItem1.url,
419
+ isConflicted: true,
420
+ mergeable: 'CONFLICTING',
421
+ });
422
+ const mergeablePr = createMockRelatedPullRequest({
423
+ url: prItem2.url,
424
+ isConflicted: false,
425
+ mergeable: 'MERGEABLE',
426
+ });
427
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
428
+ new Map([
429
+ [conflictedPr.url, conflictedPr],
430
+ [mergeablePr.url, mergeablePr],
431
+ ]),
432
+ );
433
+
434
+ await useCase.run({ projectUrl });
435
+
436
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
437
+ mockProject,
438
+ issue,
439
+ 'awaiting-workspace-id',
440
+ );
441
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
442
+ issue,
443
+ 'conflict',
444
+ );
445
+ });
446
+
447
+ it('should skip an issue whose linked PR resolved as null (absent/closed)', async () => {
448
+ const issue = createMockIssue({
449
+ url: 'https://github.com/user/repo/issues/1',
450
+ status: 'Preparation',
451
+ });
452
+ const prItem = createMockPrItem({
453
+ url: 'https://github.com/user/repo/pull/1',
454
+ closingIssueReferenceUrls: [issue.url],
455
+ });
456
+ mockIssueRepository.getAllIssues.mockResolvedValue({
457
+ project: mockProject,
458
+ issues: [issue, prItem],
459
+ cacheUsed: false,
460
+ });
461
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
462
+ new Map([['https://github.com/user/repo/pull/1', null]]),
463
+ );
464
+
465
+ await useCase.run({ projectUrl });
466
+
467
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
468
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
469
+ });
470
+
471
+ it('should not include closed PR items in the related PR map', async () => {
472
+ const issue = createMockIssue({
473
+ url: 'https://github.com/user/repo/issues/1',
474
+ status: 'Preparation',
475
+ });
476
+ const closedPrItem = createMockPrItem({
477
+ url: 'https://github.com/user/repo/pull/1',
478
+ isClosed: true,
479
+ closingIssueReferenceUrls: [issue.url],
480
+ });
481
+ mockIssueRepository.getAllIssues.mockResolvedValue({
482
+ project: mockProject,
483
+ issues: [issue, closedPrItem],
484
+ cacheUsed: false,
485
+ });
486
+
487
+ await useCase.run({ projectUrl });
488
+
489
+ expect(mockIssueRepository.getOpenPullRequests).not.toHaveBeenCalled();
490
+ expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
491
+ expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
492
+ });
493
+
494
+ it('should perform status update before comment posting', async () => {
495
+ const callOrder: string[] = [];
496
+ const issue = createMockIssue({
497
+ url: 'https://github.com/user/repo/issues/1',
498
+ status: 'Preparation',
499
+ });
500
+ const prItem = createMockPrItem({
501
+ url: 'https://github.com/user/repo/pull/1',
502
+ closingIssueReferenceUrls: [issue.url],
503
+ });
504
+ mockIssueRepository.getAllIssues.mockResolvedValue({
505
+ project: mockProject,
506
+ issues: [issue, prItem],
507
+ cacheUsed: false,
508
+ });
509
+ const conflictedPr = createMockRelatedPullRequest({
510
+ url: prItem.url,
511
+ isConflicted: true,
512
+ mergeable: 'CONFLICTING',
513
+ });
514
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
515
+ new Map([[conflictedPr.url, conflictedPr]]),
516
+ );
517
+ mockIssueRepository.updateStatus.mockImplementation(() => {
518
+ callOrder.push('updateStatus');
519
+ return Promise.resolve();
520
+ });
521
+ mockIssueCommentRepository.createComment.mockImplementation(() => {
522
+ callOrder.push('createComment');
523
+ return Promise.resolve();
524
+ });
525
+
526
+ await useCase.run({ projectUrl });
527
+
528
+ expect(callOrder).toEqual(['updateStatus', 'createComment']);
529
+ });
530
+
531
+ it('should fetch all PR URLs in a single batched call', async () => {
532
+ const issue1 = createMockIssue({
533
+ url: 'https://github.com/user/repo/issues/1',
534
+ number: 1,
535
+ status: 'Preparation',
536
+ });
537
+ const issue2 = createMockIssue({
538
+ url: 'https://github.com/user/repo/issues/2',
539
+ number: 2,
540
+ status: 'Todo by human',
541
+ });
542
+ const prItem1 = createMockPrItem({
543
+ url: 'https://github.com/user/repo/pull/10',
544
+ number: 10,
545
+ closingIssueReferenceUrls: [issue1.url],
546
+ });
547
+ const prItem2 = createMockPrItem({
548
+ url: 'https://github.com/user/repo/pull/20',
549
+ number: 20,
550
+ closingIssueReferenceUrls: [issue2.url],
551
+ });
552
+ mockIssueRepository.getAllIssues.mockResolvedValue({
553
+ project: mockProject,
554
+ issues: [issue1, issue2, prItem1, prItem2],
555
+ cacheUsed: false,
556
+ });
557
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(new Map());
558
+
559
+ await useCase.run({ projectUrl });
560
+
561
+ expect(mockIssueRepository.getOpenPullRequests).toHaveBeenCalledTimes(1);
562
+ expect(mockIssueRepository.getOpenPullRequests).toHaveBeenCalledWith(
563
+ expect.arrayContaining([
564
+ 'https://github.com/user/repo/pull/10',
565
+ 'https://github.com/user/repo/pull/20',
566
+ ]),
567
+ );
568
+ });
569
+
570
+ it('should process multiple conflicted issues in one run', async () => {
571
+ const issue1 = createMockIssue({
572
+ url: 'https://github.com/user/repo/issues/1',
573
+ number: 1,
574
+ status: 'Preparation',
575
+ });
576
+ const issue2 = createMockIssue({
577
+ url: 'https://github.com/user/repo/issues/2',
578
+ number: 2,
579
+ status: 'In Tmux by agent',
580
+ });
581
+ const prItem1 = createMockPrItem({
582
+ url: 'https://github.com/user/repo/pull/10',
583
+ number: 10,
584
+ closingIssueReferenceUrls: [issue1.url],
585
+ });
586
+ const prItem2 = createMockPrItem({
587
+ url: 'https://github.com/user/repo/pull/20',
588
+ number: 20,
589
+ closingIssueReferenceUrls: [issue2.url],
590
+ });
591
+ mockIssueRepository.getAllIssues.mockResolvedValue({
592
+ project: mockProject,
593
+ issues: [issue1, issue2, prItem1, prItem2],
594
+ cacheUsed: false,
595
+ });
596
+ const conflictedPr1 = createMockRelatedPullRequest({
597
+ url: prItem1.url,
598
+ isConflicted: true,
599
+ mergeable: 'CONFLICTING',
600
+ });
601
+ const conflictedPr2 = createMockRelatedPullRequest({
602
+ url: prItem2.url,
603
+ isConflicted: true,
604
+ mergeable: 'CONFLICTING',
605
+ });
606
+ mockIssueRepository.getOpenPullRequests.mockResolvedValue(
607
+ new Map([
608
+ [conflictedPr1.url, conflictedPr1],
609
+ [conflictedPr2.url, conflictedPr2],
610
+ ]),
611
+ );
612
+
613
+ await useCase.run({ projectUrl });
614
+
615
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledTimes(2);
616
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
617
+ mockProject,
618
+ issue1,
619
+ 'awaiting-workspace-id',
620
+ );
621
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
622
+ mockProject,
623
+ issue2,
624
+ 'awaiting-workspace-id',
625
+ );
626
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledTimes(2);
627
+ });
628
+ });
629
+ });