github-issue-tower-defence-management 1.77.3 → 1.79.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 (31) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -2
  3. package/bin/adapter/entry-points/cli/index.js +11 -9
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -3
  6. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  7. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +57 -9
  8. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  9. package/bin/domain/usecases/HandleScheduledEventUseCase.js +3 -3
  10. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  11. package/bin/domain/usecases/{RevertNotReadyAwaitingQualityCheckUseCase.js → RevertNotReadyReviewQueueIssueUseCase.js} +20 -4
  12. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -0
  13. package/package.json +1 -1
  14. package/src/adapter/entry-points/cli/index.test.ts +7 -37
  15. package/src/adapter/entry-points/cli/index.ts +12 -15
  16. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -4
  17. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +168 -76
  18. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +89 -15
  19. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +8 -8
  20. package/src/domain/usecases/HandleScheduledEventUseCase.ts +3 -3
  21. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +1127 -0
  22. package/src/domain/usecases/{RevertNotReadyAwaitingQualityCheckUseCase.ts → RevertNotReadyReviewQueueIssueUseCase.ts} +40 -1
  23. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +9 -5
  24. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  25. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -3
  26. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  27. package/types/domain/usecases/{RevertNotReadyAwaitingQualityCheckUseCase.d.ts → RevertNotReadyReviewQueueIssueUseCase.d.ts} +3 -3
  28. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -0
  29. package/bin/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.js.map +0 -1
  30. package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +0 -728
  31. package/types/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.d.ts.map +0 -1
@@ -1,728 +0,0 @@
1
- import { RevertNotReadyAwaitingQualityCheckUseCase } from './RevertNotReadyAwaitingQualityCheckUseCase';
2
- import { Issue } from '../entities/Issue';
3
- import { Project } from '../entities/Project';
4
-
5
- const createMockProject = (overrides: Partial<Project> = {}): Project => ({
6
- id: 'project-1',
7
- url: 'https://github.com/users/user/projects/1',
8
- databaseId: 1,
9
- name: 'Test Project',
10
- status: {
11
- name: 'Status',
12
- fieldId: 'field-1',
13
- statuses: [
14
- {
15
- id: 'awaiting-workspace-id',
16
- name: 'Awaiting Workspace',
17
- color: 'GRAY',
18
- description: '',
19
- },
20
- {
21
- id: 'awaiting-quality-check-id',
22
- name: 'Awaiting Quality Check',
23
- color: 'BLUE',
24
- description: '',
25
- },
26
- ],
27
- },
28
- nextActionDate: null,
29
- nextActionHour: null,
30
- story: null,
31
- remainingEstimationMinutes: null,
32
- dependedIssueUrlSeparatedByComma: null,
33
- completionDate50PercentConfidence: null,
34
- ...overrides,
35
- });
36
-
37
- const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
38
- nameWithOwner: 'user/repo',
39
- number: 1,
40
- title: 'Test Issue',
41
- state: 'OPEN',
42
- status: 'Awaiting Quality Check',
43
- story: null,
44
- nextActionDate: null,
45
- nextActionHour: null,
46
- estimationMinutes: null,
47
- dependedIssueUrls: [],
48
- completionDate50PercentConfidence: null,
49
- url: 'https://github.com/user/repo/issues/1',
50
- assignees: [],
51
- labels: [],
52
- org: 'user',
53
- repo: 'repo',
54
- body: '',
55
- itemId: 'item-1',
56
- isPr: false,
57
- isInProgress: false,
58
- isClosed: false,
59
- createdAt: new Date(),
60
- author: '',
61
- ...overrides,
62
- });
63
-
64
- const createReadyPr = (url = 'https://github.com/user/repo/pull/1') => ({
65
- url,
66
- isConflicted: false,
67
- isPassedAllCiJob: true,
68
- isCiStateSuccess: true,
69
- isResolvedAllReviewComments: true,
70
- isBranchOutOfDate: false,
71
- missingRequiredCheckNames: [],
72
- });
73
-
74
- describe('RevertNotReadyAwaitingQualityCheckUseCase', () => {
75
- let mockProjectRepository: {
76
- findProjectIdByUrl: jest.Mock;
77
- getProject: jest.Mock;
78
- };
79
- let mockIssueRepository: {
80
- getAllIssues: jest.Mock;
81
- updateStatus: jest.Mock;
82
- findRelatedOpenPRs: jest.Mock;
83
- getOpenPullRequest: jest.Mock;
84
- getPullRequestChangedFilePaths: jest.Mock;
85
- approvePullRequest: jest.Mock;
86
- requestChangesWithInlineComment: jest.Mock;
87
- };
88
- let mockIssueCommentRepository: {
89
- createComment: jest.Mock;
90
- };
91
- let mockProject: Project;
92
- let useCase: RevertNotReadyAwaitingQualityCheckUseCase;
93
-
94
- beforeEach(() => {
95
- jest.resetAllMocks();
96
-
97
- mockProject = createMockProject();
98
-
99
- mockProjectRepository = {
100
- findProjectIdByUrl: jest.fn().mockResolvedValue('project-1'),
101
- getProject: jest.fn().mockResolvedValue(mockProject),
102
- };
103
-
104
- mockIssueRepository = {
105
- getAllIssues: jest
106
- .fn()
107
- .mockResolvedValue({ issues: [], cacheUsed: false }),
108
- updateStatus: jest.fn().mockResolvedValue(undefined),
109
- findRelatedOpenPRs: jest.fn().mockResolvedValue([]),
110
- getOpenPullRequest: jest.fn().mockResolvedValue(null),
111
- getPullRequestChangedFilePaths: jest.fn().mockResolvedValue([]),
112
- approvePullRequest: jest.fn().mockResolvedValue(undefined),
113
- requestChangesWithInlineComment: jest.fn().mockResolvedValue(undefined),
114
- };
115
-
116
- mockIssueCommentRepository = {
117
- createComment: jest.fn().mockResolvedValue(undefined),
118
- };
119
-
120
- useCase = new RevertNotReadyAwaitingQualityCheckUseCase(
121
- mockProjectRepository,
122
- mockIssueRepository,
123
- mockIssueCommentRepository,
124
- );
125
- });
126
-
127
- it('should do nothing when there are no Awaiting Quality Check issues', async () => {
128
- mockIssueRepository.getAllIssues.mockResolvedValue({
129
- issues: [
130
- createMockIssue({ status: 'Awaiting Workspace' }),
131
- createMockIssue({ status: 'Preparation' }),
132
- ],
133
- cacheUsed: false,
134
- });
135
-
136
- await useCase.run({
137
- projectUrl: 'https://github.com/users/user/projects/1',
138
- allowIssueCacheMinutes: 10,
139
- });
140
-
141
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
142
- expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
143
- });
144
-
145
- it('should skip Awaiting Quality Check issue with llm-agent label', async () => {
146
- const issue = createMockIssue({
147
- status: 'Awaiting Quality Check',
148
- labels: ['llm-agent'],
149
- });
150
- mockIssueRepository.getAllIssues.mockResolvedValue({
151
- issues: [issue],
152
- cacheUsed: false,
153
- });
154
-
155
- await useCase.run({
156
- projectUrl: 'https://github.com/users/user/projects/1',
157
- allowIssueCacheMinutes: 10,
158
- });
159
-
160
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
161
- expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
162
- });
163
-
164
- it('should revert issue when no linked PR is found', async () => {
165
- const issue = createMockIssue({
166
- status: 'Awaiting Quality Check',
167
- });
168
- mockIssueRepository.getAllIssues.mockResolvedValue({
169
- issues: [issue],
170
- cacheUsed: false,
171
- });
172
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
173
-
174
- await useCase.run({
175
- projectUrl: 'https://github.com/users/user/projects/1',
176
- allowIssueCacheMinutes: 10,
177
- });
178
-
179
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
180
- mockProject,
181
- issue,
182
- 'awaiting-workspace-id',
183
- );
184
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
185
- issue,
186
- expect.stringContaining('Auto Status Check: REJECTED'),
187
- );
188
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
189
- issue,
190
- expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
191
- );
192
- });
193
-
194
- it('should not revert a story-labeled issue with no linked PR when story is in labelsAsLlmAgentName', async () => {
195
- const issue = createMockIssue({
196
- status: 'Awaiting Quality Check',
197
- labels: ['story'],
198
- });
199
- mockIssueRepository.getAllIssues.mockResolvedValue({
200
- issues: [issue],
201
- cacheUsed: false,
202
- });
203
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
204
-
205
- await useCase.run({
206
- projectUrl: 'https://github.com/users/user/projects/1',
207
- allowIssueCacheMinutes: 10,
208
- labelsAsLlmAgentName: ['story', 'chore', 'accounting'],
209
- });
210
-
211
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
212
- expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
213
- });
214
-
215
- it('should not revert a chore-labeled issue with no linked PR when chore is in labelsAsLlmAgentName', async () => {
216
- const issue = createMockIssue({
217
- status: 'Awaiting Quality Check',
218
- labels: ['chore'],
219
- });
220
- mockIssueRepository.getAllIssues.mockResolvedValue({
221
- issues: [issue],
222
- cacheUsed: false,
223
- });
224
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
225
-
226
- await useCase.run({
227
- projectUrl: 'https://github.com/users/user/projects/1',
228
- allowIssueCacheMinutes: 10,
229
- labelsAsLlmAgentName: ['story', 'chore'],
230
- });
231
-
232
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
233
- expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
234
- });
235
-
236
- it('should still revert a story-labeled issue with no linked PR when labelsAsLlmAgentName is not provided', async () => {
237
- const issue = createMockIssue({
238
- status: 'Awaiting Quality Check',
239
- labels: ['story'],
240
- });
241
- mockIssueRepository.getAllIssues.mockResolvedValue({
242
- issues: [issue],
243
- cacheUsed: false,
244
- });
245
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
246
-
247
- await useCase.run({
248
- projectUrl: 'https://github.com/users/user/projects/1',
249
- allowIssueCacheMinutes: 10,
250
- });
251
-
252
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
253
- mockProject,
254
- issue,
255
- 'awaiting-workspace-id',
256
- );
257
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
258
- issue,
259
- expect.stringContaining('PULL_REQUEST_NOT_FOUND'),
260
- );
261
- });
262
-
263
- it('should not revert a story-labeled issue with no linked PR when labelsAsLlmAgentName is null', async () => {
264
- const issue = createMockIssue({
265
- status: 'Awaiting Quality Check',
266
- labels: ['story'],
267
- });
268
- mockIssueRepository.getAllIssues.mockResolvedValue({
269
- issues: [issue],
270
- cacheUsed: false,
271
- });
272
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
273
-
274
- await useCase.run({
275
- projectUrl: 'https://github.com/users/user/projects/1',
276
- allowIssueCacheMinutes: 10,
277
- labelsAsLlmAgentName: null,
278
- });
279
-
280
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
281
- mockProject,
282
- issue,
283
- 'awaiting-workspace-id',
284
- );
285
- });
286
-
287
- it('should not revert issue when PR is ready', async () => {
288
- const issue = createMockIssue({
289
- status: 'Awaiting Quality Check',
290
- });
291
- mockIssueRepository.getAllIssues.mockResolvedValue({
292
- issues: [issue],
293
- cacheUsed: false,
294
- });
295
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([createReadyPr()]);
296
-
297
- await useCase.run({
298
- projectUrl: 'https://github.com/users/user/projects/1',
299
- allowIssueCacheMinutes: 10,
300
- });
301
-
302
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
303
- expect(mockIssueCommentRepository.createComment).not.toHaveBeenCalled();
304
- });
305
-
306
- it('should revert issue when PR is conflicted', async () => {
307
- const issue = createMockIssue({
308
- status: 'Awaiting Quality Check',
309
- });
310
- mockIssueRepository.getAllIssues.mockResolvedValue({
311
- issues: [issue],
312
- cacheUsed: false,
313
- });
314
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
315
- { ...createReadyPr(), isConflicted: true },
316
- ]);
317
-
318
- await useCase.run({
319
- projectUrl: 'https://github.com/users/user/projects/1',
320
- allowIssueCacheMinutes: 10,
321
- });
322
-
323
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
324
- mockProject,
325
- issue,
326
- 'awaiting-workspace-id',
327
- );
328
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
329
- issue,
330
- expect.stringContaining('Auto Status Check: REJECTED'),
331
- );
332
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
333
- issue,
334
- expect.stringContaining('PULL_REQUEST_CONFLICTED'),
335
- );
336
- });
337
-
338
- it('should revert issue when CI is failing', async () => {
339
- const issue = createMockIssue({
340
- status: 'Awaiting Quality Check',
341
- });
342
- mockIssueRepository.getAllIssues.mockResolvedValue({
343
- issues: [issue],
344
- cacheUsed: false,
345
- });
346
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
347
- {
348
- ...createReadyPr(),
349
- isPassedAllCiJob: false,
350
- isCiStateSuccess: false,
351
- },
352
- ]);
353
-
354
- await useCase.run({
355
- projectUrl: 'https://github.com/users/user/projects/1',
356
- allowIssueCacheMinutes: 10,
357
- });
358
-
359
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
360
- mockProject,
361
- issue,
362
- 'awaiting-workspace-id',
363
- );
364
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
365
- issue,
366
- expect.stringContaining('Auto Status Check: REJECTED'),
367
- );
368
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
369
- issue,
370
- expect.stringContaining('ANY_CI_JOB_FAILED_OR_IN_PROGRESS'),
371
- );
372
- });
373
-
374
- it('should revert issue when review comments are not resolved', async () => {
375
- const issue = createMockIssue({
376
- status: 'Awaiting Quality Check',
377
- });
378
- mockIssueRepository.getAllIssues.mockResolvedValue({
379
- issues: [issue],
380
- cacheUsed: false,
381
- });
382
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
383
- { ...createReadyPr(), isResolvedAllReviewComments: false },
384
- ]);
385
-
386
- await useCase.run({
387
- projectUrl: 'https://github.com/users/user/projects/1',
388
- allowIssueCacheMinutes: 10,
389
- });
390
-
391
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
392
- mockProject,
393
- issue,
394
- 'awaiting-workspace-id',
395
- );
396
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
397
- issue,
398
- expect.stringContaining('Auto Status Check: REJECTED'),
399
- );
400
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
401
- issue,
402
- expect.stringContaining('ANY_REVIEW_COMMENT_NOT_RESOLVED'),
403
- );
404
- });
405
-
406
- it('should revert issue when linked PR is in draft state', async () => {
407
- const issue = createMockIssue({
408
- status: 'Awaiting Quality Check',
409
- });
410
- mockIssueRepository.getAllIssues.mockResolvedValue({
411
- issues: [issue],
412
- cacheUsed: false,
413
- });
414
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
415
- { ...createReadyPr(), isDraft: true },
416
- ]);
417
-
418
- await useCase.run({
419
- projectUrl: 'https://github.com/users/user/projects/1',
420
- allowIssueCacheMinutes: 10,
421
- });
422
-
423
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
424
- mockProject,
425
- issue,
426
- 'awaiting-workspace-id',
427
- );
428
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
429
- issue,
430
- expect.stringContaining('Auto Status Check: REJECTED'),
431
- );
432
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
433
- issue,
434
- expect.stringContaining('PULL_REQUEST_IS_DRAFT'),
435
- );
436
- });
437
-
438
- it('should revert issue when multiple linked open PRs are found', async () => {
439
- const issue = createMockIssue({
440
- status: 'Awaiting Quality Check',
441
- });
442
- mockIssueRepository.getAllIssues.mockResolvedValue({
443
- issues: [issue],
444
- cacheUsed: false,
445
- });
446
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
447
- createReadyPr('https://github.com/user/repo/pull/1'),
448
- createReadyPr('https://github.com/user/repo/pull/2'),
449
- ]);
450
-
451
- await useCase.run({
452
- projectUrl: 'https://github.com/users/user/projects/1',
453
- allowIssueCacheMinutes: 10,
454
- });
455
-
456
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
457
- mockProject,
458
- issue,
459
- 'awaiting-workspace-id',
460
- );
461
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
462
- issue,
463
- expect.stringContaining('Auto Status Check: REJECTED'),
464
- );
465
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
466
- issue,
467
- expect.stringContaining('MULTIPLE_PULL_REQUESTS_FOUND'),
468
- );
469
- });
470
-
471
- it('should revert issue when CI is SUCCESS but required check never started', async () => {
472
- const issue = createMockIssue({
473
- status: 'Awaiting Quality Check',
474
- });
475
- mockIssueRepository.getAllIssues.mockResolvedValue({
476
- issues: [issue],
477
- cacheUsed: false,
478
- });
479
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
480
- {
481
- ...createReadyPr(),
482
- isPassedAllCiJob: false,
483
- isCiStateSuccess: true,
484
- missingRequiredCheckNames: ['E2E Tests'],
485
- },
486
- ]);
487
-
488
- await useCase.run({
489
- projectUrl: 'https://github.com/users/user/projects/1',
490
- allowIssueCacheMinutes: 10,
491
- });
492
-
493
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
494
- mockProject,
495
- issue,
496
- 'awaiting-workspace-id',
497
- );
498
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
499
- issue,
500
- expect.stringContaining('Auto Status Check: REJECTED'),
501
- );
502
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
503
- issue,
504
- expect.stringContaining('REQUIRED_CI_JOB_NEVER_STARTED'),
505
- );
506
- });
507
-
508
- describe('change-target label auto-approve', () => {
509
- const setupReadyIssue = (labels: string[]) => {
510
- const issue = createMockIssue({
511
- status: 'Awaiting Quality Check',
512
- labels,
513
- });
514
- mockIssueRepository.getAllIssues.mockResolvedValue({
515
- issues: [issue],
516
- cacheUsed: false,
517
- });
518
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
519
- createReadyPr(),
520
- ]);
521
- return issue;
522
- };
523
-
524
- const runCycle = () =>
525
- useCase.run({
526
- projectUrl: 'https://github.com/users/user/projects/1',
527
- allowIssueCacheMinutes: 10,
528
- });
529
-
530
- it('should not approve PR when issue has no change-target label', async () => {
531
- setupReadyIssue([]);
532
-
533
- await runCycle();
534
-
535
- expect(
536
- mockIssueRepository.getPullRequestChangedFilePaths,
537
- ).not.toHaveBeenCalled();
538
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
539
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
540
- });
541
-
542
- it('should approve PR when issue has change-target label and all files are confined', async () => {
543
- setupReadyIssue(['change-target:src/domain']);
544
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
545
- 'src/domain/entities/Foo.ts',
546
- 'src/domain/usecases/Bar.ts',
547
- ]);
548
-
549
- await runCycle();
550
-
551
- expect(
552
- mockIssueRepository.getPullRequestChangedFilePaths,
553
- ).toHaveBeenCalledWith('https://github.com/user/repo/pull/1');
554
- expect(mockIssueRepository.approvePullRequest).toHaveBeenCalledWith(
555
- 'https://github.com/user/repo/pull/1',
556
- );
557
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
558
- });
559
-
560
- it('should not approve PR when any changed file is outside the labeled path', async () => {
561
- setupReadyIssue(['change-target:src/domain']);
562
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
563
- 'src/domain/entities/Foo.ts',
564
- 'src/adapter/repositories/Outside.ts',
565
- ]);
566
-
567
- await runCycle();
568
-
569
- expect(
570
- mockIssueRepository.getPullRequestChangedFilePaths,
571
- ).toHaveBeenCalledWith('https://github.com/user/repo/pull/1');
572
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
573
- });
574
-
575
- it('should approve when files are confined under any of multiple change-target labels', async () => {
576
- setupReadyIssue(['change-target:src/domain', 'change-target:docs']);
577
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
578
- 'src/domain/entities/Foo.ts',
579
- 'docs/intro.md',
580
- ]);
581
-
582
- await runCycle();
583
-
584
- expect(mockIssueRepository.approvePullRequest).toHaveBeenCalledWith(
585
- 'https://github.com/user/repo/pull/1',
586
- );
587
- });
588
-
589
- it('should not approve when PR has more than 100 changed files and one file beyond entry 100 is outside the labeled path', async () => {
590
- setupReadyIssue(['change-target:src/domain']);
591
- const filePaths: string[] = [];
592
- for (let i = 0; i < 150; i += 1) {
593
- filePaths.push(`src/domain/file${i}.ts`);
594
- }
595
- filePaths.push('src/adapter/Outside.ts');
596
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue(
597
- filePaths,
598
- );
599
-
600
- await runCycle();
601
-
602
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
603
- });
604
-
605
- it('should match boundary-safely (change-target:foo matches foo/bar.ts but not foobar/baz.ts)', async () => {
606
- setupReadyIssue(['change-target:foo']);
607
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
608
- 'foo/bar.ts',
609
- 'foobar/baz.ts',
610
- ]);
611
-
612
- await runCycle();
613
-
614
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
615
- });
616
-
617
- it('should approve when changed files match exact path or subpath of the labeled path', async () => {
618
- setupReadyIssue(['change-target:foo']);
619
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
620
- 'foo/bar.ts',
621
- 'foo/nested/baz.ts',
622
- ]);
623
-
624
- await runCycle();
625
-
626
- expect(mockIssueRepository.approvePullRequest).toHaveBeenCalledWith(
627
- 'https://github.com/user/repo/pull/1',
628
- );
629
- });
630
-
631
- it('should not approve when PR has zero changed files', async () => {
632
- setupReadyIssue(['change-target:src/domain']);
633
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([]);
634
-
635
- await runCycle();
636
-
637
- expect(
638
- mockIssueRepository.getPullRequestChangedFilePaths,
639
- ).toHaveBeenCalledWith('https://github.com/user/repo/pull/1');
640
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
641
- });
642
-
643
- it('should not approve when there is no ready PR even if change-target label is present', async () => {
644
- const issue = createMockIssue({
645
- status: 'Awaiting Quality Check',
646
- labels: ['change-target:src/domain'],
647
- });
648
- mockIssueRepository.getAllIssues.mockResolvedValue({
649
- issues: [issue],
650
- cacheUsed: false,
651
- });
652
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
653
-
654
- await runCycle();
655
-
656
- expect(
657
- mockIssueRepository.getPullRequestChangedFilePaths,
658
- ).not.toHaveBeenCalled();
659
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
660
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
661
- mockProject,
662
- issue,
663
- 'awaiting-workspace-id',
664
- );
665
- });
666
-
667
- it('should not approve when PR has unresolved rejections even with change-target label', async () => {
668
- const issue = createMockIssue({
669
- status: 'Awaiting Quality Check',
670
- labels: ['change-target:src/domain'],
671
- });
672
- mockIssueRepository.getAllIssues.mockResolvedValue({
673
- issues: [issue],
674
- cacheUsed: false,
675
- });
676
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
677
- { ...createReadyPr(), isConflicted: true },
678
- ]);
679
-
680
- await runCycle();
681
-
682
- expect(
683
- mockIssueRepository.getPullRequestChangedFilePaths,
684
- ).not.toHaveBeenCalled();
685
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
686
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
687
- mockProject,
688
- issue,
689
- 'awaiting-workspace-id',
690
- );
691
- });
692
-
693
- it('should skip change-target auto-approve for issue with llm-agent label', async () => {
694
- const issue = createMockIssue({
695
- status: 'Awaiting Quality Check',
696
- labels: ['llm-agent', 'change-target:src/domain'],
697
- });
698
- mockIssueRepository.getAllIssues.mockResolvedValue({
699
- issues: [issue],
700
- cacheUsed: false,
701
- });
702
- mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
703
- createReadyPr(),
704
- ]);
705
-
706
- await runCycle();
707
-
708
- expect(
709
- mockIssueRepository.getPullRequestChangedFilePaths,
710
- ).not.toHaveBeenCalled();
711
- expect(mockIssueRepository.approvePullRequest).not.toHaveBeenCalled();
712
- expect(mockIssueRepository.updateStatus).not.toHaveBeenCalled();
713
- });
714
-
715
- it('should normalize trailing slashes in change-target label paths', async () => {
716
- setupReadyIssue(['change-target:src/domain/']);
717
- mockIssueRepository.getPullRequestChangedFilePaths.mockResolvedValue([
718
- 'src/domain/entities/Foo.ts',
719
- ]);
720
-
721
- await runCycle();
722
-
723
- expect(mockIssueRepository.approvePullRequest).toHaveBeenCalledWith(
724
- 'https://github.com/user/repo/pull/1',
725
- );
726
- });
727
- });
728
- });