github-issue-tower-defence-management 2.6.1 → 2.7.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 (75) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +0 -2
  3. package/bin/adapter/entry-points/cli/index.js +0 -5
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +0 -3
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +1 -3
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +4 -2
  10. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  11. package/bin/domain/usecases/HandleScheduledEventUseCase.js +1 -6
  12. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  13. package/bin/domain/usecases/IssueRejectionEvaluator.js +0 -23
  14. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  15. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +39 -69
  16. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  17. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +3 -2
  18. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  19. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +0 -43
  20. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  21. package/bin/domain/usecases/autoStatusCheckComments.js +1 -11
  22. package/bin/domain/usecases/autoStatusCheckComments.js.map +1 -1
  23. package/bin/domain/usecases/extractNextStepAgentFromComments.js +14 -0
  24. package/bin/domain/usecases/extractNextStepAgentFromComments.js.map +1 -0
  25. package/bin/domain/usecases/triagerAgentName.js +7 -0
  26. package/bin/domain/usecases/triagerAgentName.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/adapter/entry-points/cli/index.test.ts +0 -2
  29. package/src/adapter/entry-points/cli/index.ts +0 -9
  30. package/src/adapter/entry-points/cli/projectConfig.ts +0 -8
  31. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +0 -7
  32. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +26 -4
  33. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +5 -3
  34. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +0 -28
  35. package/src/domain/usecases/HandleScheduledEventUseCase.ts +0 -6
  36. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +0 -119
  37. package/src/domain/usecases/IssueRejectionEvaluator.ts +0 -21
  38. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +185 -327
  39. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +68 -138
  40. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +4 -4
  41. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +11 -5
  42. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +0 -313
  43. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -102
  44. package/src/domain/usecases/autoStatusCheckComments.ts +0 -17
  45. package/src/domain/usecases/extractNextStepAgentFromComments.ts +13 -0
  46. package/src/domain/usecases/isHumanComment.test.ts +0 -13
  47. package/src/domain/usecases/triagerAgentName.ts +4 -0
  48. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  49. package/types/adapter/entry-points/cli/projectConfig.d.ts +0 -1
  50. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  51. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  52. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  53. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +1 -3
  54. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  55. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +0 -1
  56. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  57. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +0 -1
  58. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  59. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  60. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts +0 -1
  61. package/types/domain/usecases/RevertOrphanedPreparationUseCase.d.ts.map +1 -1
  62. package/types/domain/usecases/autoStatusCheckComments.d.ts +0 -4
  63. package/types/domain/usecases/autoStatusCheckComments.d.ts.map +1 -1
  64. package/types/domain/usecases/extractNextStepAgentFromComments.d.ts +5 -0
  65. package/types/domain/usecases/extractNextStepAgentFromComments.d.ts.map +1 -0
  66. package/types/domain/usecases/triagerAgentName.d.ts +3 -0
  67. package/types/domain/usecases/triagerAgentName.d.ts.map +1 -0
  68. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +0 -1051
  69. package/src/domain/usecases/TriagerApprovalDispatchUseCase.ts +0 -274
  70. package/src/domain/usecases/awaitingOwnerApprovalMessage.ts +0 -5
  71. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.test.ts +0 -162
  72. package/src/domain/usecases/isPullRequestDeclaredUnnecessary.ts +0 -29
  73. package/src/domain/usecases/isWaitingForOwnerApproval.test.ts +0 -91
  74. package/src/domain/usecases/isWaitingForOwnerApproval.ts +0 -17
  75. package/src/domain/usecases/returnedToAwaitingWorkspaceMessage.ts +0 -5
@@ -1,1051 +0,0 @@
1
- import { TriagerApprovalDispatchUseCase } from './TriagerApprovalDispatchUseCase';
2
- import { IssueCommentRepository } from './adapter-interfaces/IssueCommentRepository';
3
- import { IssueRepository } from './adapter-interfaces/IssueRepository';
4
- import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
5
- import { Comment } from '../entities/Comment';
6
- import { Issue } from '../entities/Issue';
7
- import { Project } from '../entities/Project';
8
-
9
- type MockedProjectRepository = jest.Mocked<
10
- Pick<
11
- ProjectRepository,
12
- | 'findProjectIdByUrl'
13
- | 'getProject'
14
- | 'createField'
15
- | 'getByUrl'
16
- | 'updateAgentList'
17
- >
18
- >;
19
- type MockedIssueRepository = jest.Mocked<
20
- Pick<
21
- IssueRepository,
22
- 'getAllIssues' | 'updateStatus' | 'updateStory' | 'setIssueAgentField'
23
- >
24
- >;
25
- type MockedIssueCommentRepository = jest.Mocked<
26
- Pick<IssueCommentRepository, 'getCommentsFromIssue' | 'createComment'>
27
- >;
28
-
29
- const TRIAGER_PROPOSAL_COMMENT = (
30
- agent: string,
31
- story: string,
32
- storyAlreadySet: boolean,
33
- ): string =>
34
- `From: :robot: triager (claude-sonnet-4-6)\n\`\`\`json\n{ "pullRequestRequired": false, "nextStep": null }\n\`\`\`\n\n## Proposal\n\n\`\`\`json\n${JSON.stringify({ triagerProposal: { recommendedAgent: agent, recommendedStory: story, storyAlreadySet } })}\n\`\`\``;
35
-
36
- const createComment = (
37
- author: string,
38
- content: string,
39
- createdAt = new Date('2000-01-01T00:00:00Z'),
40
- ): Comment => ({ author, content, createdAt });
41
-
42
- const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
43
- nameWithOwner: 'owner/repo',
44
- number: 1,
45
- title: 'Test Issue',
46
- state: 'OPEN',
47
- status: 'Awaiting Quality Check',
48
- story: null,
49
- nextActionDate: null,
50
- nextActionHour: null,
51
- estimationMinutes: null,
52
- dependedIssueUrls: [],
53
- completionDate50PercentConfidence: null,
54
- url: 'https://github.com/owner/repo/issues/1',
55
- assignees: [],
56
- labels: [],
57
- org: 'owner',
58
- repo: 'repo',
59
- body: '',
60
- itemId: 'item-1',
61
- isPr: false,
62
- isInProgress: false,
63
- isClosed: false,
64
- createdAt: new Date('2000-01-01T00:00:00Z'),
65
- author: 'owner-user',
66
- closingIssueReferenceUrls: [],
67
- agent: null,
68
- stateReason: null,
69
- ...overrides,
70
- });
71
-
72
- const createMockProject = (overrides: Partial<Project> = {}): Project => ({
73
- id: 'project-1',
74
- url: 'https://github.com/orgs/owner/projects/1',
75
- databaseId: 1,
76
- name: 'Test Project',
77
- status: {
78
- name: 'Status',
79
- fieldId: 'status-field-id',
80
- statuses: [
81
- {
82
- id: 'awaiting-workspace-id',
83
- name: 'Awaiting Workspace',
84
- color: 'BLUE',
85
- description: '',
86
- },
87
- {
88
- id: 'awaiting-quality-check-id',
89
- name: 'Awaiting Quality Check',
90
- color: 'GREEN',
91
- description: '',
92
- },
93
- ],
94
- },
95
- nextActionDate: null,
96
- nextActionHour: null,
97
- story: {
98
- name: 'Story',
99
- fieldId: 'story-field-id',
100
- databaseId: 2,
101
- stories: [
102
- {
103
- id: 'story-regular-workflow-id',
104
- name: 'regular / workflow improvement',
105
- color: 'GRAY',
106
- description: '',
107
- },
108
- ],
109
- workflowManagementStory: {
110
- id: 'wf-story-id',
111
- name: 'workflow management',
112
- },
113
- },
114
- remainingEstimationMinutes: null,
115
- dependedIssueUrlSeparatedByComma: null,
116
- completionDate50PercentConfidence: null,
117
- agent: {
118
- name: 'Agent',
119
- fieldId: 'agent-field-id',
120
- options: [
121
- {
122
- id: 'developer-option-id',
123
- name: 'developer',
124
- color: 'GRAY',
125
- description: '',
126
- },
127
- ],
128
- },
129
- ...overrides,
130
- });
131
-
132
- describe('TriagerApprovalDispatchUseCase', () => {
133
- let useCase: TriagerApprovalDispatchUseCase;
134
- let mockProjectRepository: MockedProjectRepository;
135
- let mockIssueRepository: MockedIssueRepository;
136
- let mockIssueCommentRepository: MockedIssueCommentRepository;
137
- let mockProject: Project;
138
-
139
- beforeEach(() => {
140
- jest.resetAllMocks();
141
- mockProject = createMockProject();
142
- mockProjectRepository = {
143
- findProjectIdByUrl: jest.fn().mockResolvedValue('project-1'),
144
- getProject: jest.fn().mockResolvedValue(mockProject),
145
- createField: jest.fn().mockResolvedValue(undefined),
146
- getByUrl: jest.fn().mockResolvedValue(mockProject),
147
- updateAgentList: jest.fn().mockResolvedValue([]),
148
- };
149
- mockIssueRepository = {
150
- getAllIssues: jest.fn().mockResolvedValue({
151
- project: mockProject,
152
- issues: [],
153
- cacheUsed: false,
154
- }),
155
- updateStatus: jest.fn().mockResolvedValue(undefined),
156
- updateStory: jest.fn().mockResolvedValue(undefined),
157
- setIssueAgentField: jest.fn().mockResolvedValue(undefined),
158
- };
159
- mockIssueCommentRepository = {
160
- getCommentsFromIssue: jest.fn().mockResolvedValue([]),
161
- createComment: jest.fn().mockResolvedValue(undefined),
162
- };
163
- useCase = new TriagerApprovalDispatchUseCase(
164
- mockProjectRepository,
165
- mockIssueRepository,
166
- mockIssueCommentRepository,
167
- );
168
- });
169
-
170
- describe('run', () => {
171
- it('should do nothing when no issues are in candidate statuses', async () => {
172
- const irrelevantIssue = createMockIssue({
173
- status: 'Preparation',
174
- author: 'owner-user',
175
- });
176
- mockIssueRepository.getAllIssues.mockResolvedValue({
177
- project: mockProject,
178
- issues: [irrelevantIssue],
179
- cacheUsed: false,
180
- });
181
-
182
- await useCase.run({
183
- projectUrl: 'https://github.com/orgs/owner/projects/1',
184
- allowedIssueAuthors: ['owner-user'],
185
- });
186
-
187
- expect(
188
- mockIssueCommentRepository.getCommentsFromIssue.mock.calls,
189
- ).toEqual([]);
190
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
191
- });
192
-
193
- it('should do nothing when the triager proposal comment has no machine-readable block', async () => {
194
- const issue = createMockIssue({
195
- status: 'Awaiting Quality Check',
196
- author: 'owner-user',
197
- });
198
- mockIssueRepository.getAllIssues.mockResolvedValue({
199
- project: mockProject,
200
- issues: [issue],
201
- cacheUsed: false,
202
- });
203
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
204
- createComment(
205
- 'bot',
206
- 'From: :robot: triager (claude-sonnet-4-6)\n```json\n{ "pullRequestRequired": false, "nextStep": null }\n```\n\nRecommended agent: `developer`\nRecommended story: regular / workflow improvement',
207
- ),
208
- createComment('owner-user', 'ok'),
209
- ]);
210
-
211
- await useCase.run({
212
- projectUrl: 'https://github.com/orgs/owner/projects/1',
213
- allowedIssueAuthors: ['owner-user'],
214
- });
215
-
216
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
217
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
218
- });
219
-
220
- it('should do nothing when there is a proposal but no approval comment', async () => {
221
- const issue = createMockIssue({
222
- status: 'Awaiting Quality Check',
223
- author: 'owner-user',
224
- });
225
- mockIssueRepository.getAllIssues.mockResolvedValue({
226
- project: mockProject,
227
- issues: [issue],
228
- cacheUsed: false,
229
- });
230
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
231
- createComment(
232
- 'bot',
233
- TRIAGER_PROPOSAL_COMMENT(
234
- 'developer',
235
- 'regular / workflow improvement',
236
- false,
237
- ),
238
- ),
239
- ]);
240
-
241
- await useCase.run({
242
- projectUrl: 'https://github.com/orgs/owner/projects/1',
243
- allowedIssueAuthors: ['owner-user'],
244
- });
245
-
246
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
247
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
248
- });
249
-
250
- it('should do nothing when the approval comment is posted before the proposal', async () => {
251
- const issue = createMockIssue({
252
- status: 'Awaiting Quality Check',
253
- author: 'owner-user',
254
- });
255
- mockIssueRepository.getAllIssues.mockResolvedValue({
256
- project: mockProject,
257
- issues: [issue],
258
- cacheUsed: false,
259
- });
260
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
261
- createComment('owner-user', 'ok'),
262
- createComment(
263
- 'bot',
264
- TRIAGER_PROPOSAL_COMMENT(
265
- 'developer',
266
- 'regular / workflow improvement',
267
- false,
268
- ),
269
- ),
270
- ]);
271
-
272
- await useCase.run({
273
- projectUrl: 'https://github.com/orgs/owner/projects/1',
274
- allowedIssueAuthors: ['owner-user'],
275
- });
276
-
277
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
278
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
279
- });
280
-
281
- it('should skip the story update when the proposal reports storyAlreadySet true', async () => {
282
- const issue = createMockIssue({
283
- status: 'Awaiting Quality Check',
284
- author: 'owner-user',
285
- story: 'regular / workflow improvement',
286
- });
287
- mockIssueRepository.getAllIssues.mockResolvedValue({
288
- project: mockProject,
289
- issues: [issue],
290
- cacheUsed: false,
291
- });
292
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
293
- createComment(
294
- 'bot',
295
- TRIAGER_PROPOSAL_COMMENT(
296
- 'developer',
297
- 'regular / workflow improvement',
298
- true,
299
- ),
300
- ),
301
- createComment('owner-user', 'ok'),
302
- ]);
303
-
304
- await useCase.run({
305
- projectUrl: 'https://github.com/orgs/owner/projects/1',
306
- allowedIssueAuthors: ['owner-user'],
307
- });
308
-
309
- expect(mockIssueRepository.updateStory.mock.calls).toEqual([]);
310
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([
311
- [
312
- 'https://github.com/owner/repo/issues/1',
313
- mockProject,
314
- 'developer-option-id',
315
- ],
316
- ]);
317
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([
318
- [mockProject, issue, 'awaiting-workspace-id'],
319
- ]);
320
- expect(mockIssueCommentRepository.createComment.mock.calls).toHaveLength(
321
- 1,
322
- );
323
- });
324
-
325
- it('should skip the issue when the recommended agent name cannot be resolved', async () => {
326
- const issue = createMockIssue({
327
- status: 'Awaiting Quality Check',
328
- author: 'owner-user',
329
- });
330
- mockIssueRepository.getAllIssues.mockResolvedValue({
331
- project: mockProject,
332
- issues: [issue],
333
- cacheUsed: false,
334
- });
335
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
336
- createComment(
337
- 'bot',
338
- TRIAGER_PROPOSAL_COMMENT(
339
- 'unknown-agent-xyz',
340
- 'regular / workflow improvement',
341
- false,
342
- ),
343
- ),
344
- createComment('owner-user', 'ok'),
345
- ]);
346
- mockProjectRepository.updateAgentList.mockResolvedValue([]);
347
-
348
- await useCase.run({
349
- projectUrl: 'https://github.com/orgs/owner/projects/1',
350
- allowedIssueAuthors: ['owner-user'],
351
- });
352
-
353
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
354
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
355
- expect(mockIssueCommentRepository.createComment.mock.calls).toEqual([]);
356
- });
357
-
358
- it('should not treat a comment from an author not in the allowed list as an approval', async () => {
359
- const issue = createMockIssue({
360
- status: 'Awaiting Quality Check',
361
- author: 'owner-user',
362
- });
363
- mockIssueRepository.getAllIssues.mockResolvedValue({
364
- project: mockProject,
365
- issues: [issue],
366
- cacheUsed: false,
367
- });
368
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
369
- createComment(
370
- 'bot',
371
- TRIAGER_PROPOSAL_COMMENT(
372
- 'developer',
373
- 'regular / workflow improvement',
374
- false,
375
- ),
376
- ),
377
- createComment('external-user', 'ok'),
378
- ]);
379
-
380
- await useCase.run({
381
- projectUrl: 'https://github.com/orgs/owner/projects/1',
382
- allowedIssueAuthors: ['owner-user'],
383
- });
384
-
385
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
386
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
387
- });
388
-
389
- it('should set story, agent, and status to Awaiting Workspace on approval', async () => {
390
- const issue = createMockIssue({
391
- status: 'Awaiting Quality Check',
392
- author: 'owner-user',
393
- });
394
- mockIssueRepository.getAllIssues.mockResolvedValue({
395
- project: mockProject,
396
- issues: [issue],
397
- cacheUsed: false,
398
- });
399
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
400
- createComment(
401
- 'bot',
402
- TRIAGER_PROPOSAL_COMMENT(
403
- 'developer',
404
- 'regular / workflow improvement',
405
- false,
406
- ),
407
- ),
408
- createComment('owner-user', 'ok'),
409
- ]);
410
-
411
- await useCase.run({
412
- projectUrl: 'https://github.com/orgs/owner/projects/1',
413
- allowedIssueAuthors: ['owner-user'],
414
- });
415
-
416
- expect(mockIssueRepository.updateStory.mock.calls).toEqual([
417
- [mockProject, issue, 'story-regular-workflow-id'],
418
- ]);
419
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([
420
- [
421
- 'https://github.com/owner/repo/issues/1',
422
- mockProject,
423
- 'developer-option-id',
424
- ],
425
- ]);
426
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([
427
- [mockProject, issue, 'awaiting-workspace-id'],
428
- ]);
429
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
430
- issue,
431
- expect.stringContaining('TRIAGER_PROPOSAL_APPROVED'),
432
- );
433
- });
434
-
435
- it('should accept OK (uppercase) as an approval token', async () => {
436
- const issue = createMockIssue({
437
- status: 'Awaiting Workspace',
438
- author: 'owner-user',
439
- });
440
- mockIssueRepository.getAllIssues.mockResolvedValue({
441
- project: mockProject,
442
- issues: [issue],
443
- cacheUsed: false,
444
- });
445
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
446
- createComment(
447
- 'bot',
448
- TRIAGER_PROPOSAL_COMMENT(
449
- 'developer',
450
- 'regular / workflow improvement',
451
- false,
452
- ),
453
- ),
454
- createComment('owner-user', 'OK'),
455
- ]);
456
-
457
- await useCase.run({
458
- projectUrl: 'https://github.com/orgs/owner/projects/1',
459
- allowedIssueAuthors: ['owner-user'],
460
- });
461
-
462
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
463
- });
464
-
465
- it('should accept オーケー as an approval token', async () => {
466
- const issue = createMockIssue({
467
- status: 'Awaiting Quality Check',
468
- author: 'owner-user',
469
- });
470
- mockIssueRepository.getAllIssues.mockResolvedValue({
471
- project: mockProject,
472
- issues: [issue],
473
- cacheUsed: false,
474
- });
475
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
476
- createComment(
477
- 'bot',
478
- TRIAGER_PROPOSAL_COMMENT(
479
- 'developer',
480
- 'regular / workflow improvement',
481
- false,
482
- ),
483
- ),
484
- createComment('owner-user', 'オーケー'),
485
- ]);
486
-
487
- await useCase.run({
488
- projectUrl: 'https://github.com/orgs/owner/projects/1',
489
- allowedIssueAuthors: ['owner-user'],
490
- });
491
-
492
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
493
- });
494
-
495
- it('should not treat a long comment containing ok as an approval', async () => {
496
- const issue = createMockIssue({
497
- status: 'Awaiting Quality Check',
498
- author: 'owner-user',
499
- });
500
- mockIssueRepository.getAllIssues.mockResolvedValue({
501
- project: mockProject,
502
- issues: [issue],
503
- cacheUsed: false,
504
- });
505
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
506
- createComment(
507
- 'bot',
508
- TRIAGER_PROPOSAL_COMMENT(
509
- 'developer',
510
- 'regular / workflow improvement',
511
- false,
512
- ),
513
- ),
514
- createComment(
515
- 'owner-user',
516
- 'Looks ok but I have some concerns about the story choice.',
517
- ),
518
- ]);
519
-
520
- await useCase.run({
521
- projectUrl: 'https://github.com/orgs/owner/projects/1',
522
- allowedIssueAuthors: ['owner-user'],
523
- });
524
-
525
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
526
- });
527
-
528
- it('should not treat an agent report comment starting with From: :robot: as an approval', async () => {
529
- const issue = createMockIssue({
530
- status: 'Awaiting Quality Check',
531
- author: 'owner-user',
532
- });
533
- mockIssueRepository.getAllIssues.mockResolvedValue({
534
- project: mockProject,
535
- issues: [issue],
536
- cacheUsed: false,
537
- });
538
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
539
- createComment(
540
- 'bot',
541
- TRIAGER_PROPOSAL_COMMENT(
542
- 'developer',
543
- 'regular / workflow improvement',
544
- false,
545
- ),
546
- ),
547
- createComment('owner-user', 'From: :robot: ok'),
548
- ]);
549
-
550
- await useCase.run({
551
- projectUrl: 'https://github.com/orgs/owner/projects/1',
552
- allowedIssueAuthors: ['owner-user'],
553
- });
554
-
555
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
556
- });
557
-
558
- it('should skip issues where the agent field is already set', async () => {
559
- const issueWithAgent = createMockIssue({
560
- status: 'Awaiting Quality Check',
561
- author: 'owner-user',
562
- agent: 'developer',
563
- });
564
- mockIssueRepository.getAllIssues.mockResolvedValue({
565
- project: mockProject,
566
- issues: [issueWithAgent],
567
- cacheUsed: false,
568
- });
569
-
570
- await useCase.run({
571
- projectUrl: 'https://github.com/orgs/owner/projects/1',
572
- allowedIssueAuthors: ['owner-user'],
573
- });
574
-
575
- expect(
576
- mockIssueCommentRepository.getCommentsFromIssue.mock.calls,
577
- ).toEqual([]);
578
- });
579
-
580
- it('should fetch comments for at most 20 candidate issues per cycle when the backlog is large', async () => {
581
- const manyIssues = Array.from({ length: 25 }, (_, i) =>
582
- createMockIssue({
583
- number: i + 1,
584
- url: `https://github.com/owner/repo/issues/${i + 1}`,
585
- itemId: `item-${i + 1}`,
586
- status: 'Awaiting Quality Check',
587
- author: 'owner-user',
588
- }),
589
- );
590
- mockIssueRepository.getAllIssues.mockResolvedValue({
591
- project: mockProject,
592
- issues: manyIssues,
593
- cacheUsed: false,
594
- });
595
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
596
-
597
- await useCase.run({
598
- projectUrl: 'https://github.com/orgs/owner/projects/1',
599
- allowedIssueAuthors: ['owner-user'],
600
- });
601
-
602
- expect(
603
- mockIssueCommentRepository.getCommentsFromIssue,
604
- ).toHaveBeenCalledTimes(20);
605
- });
606
-
607
- it('should dispatch an approval on a candidate beyond position 20 when the cycle index advances to the next window', async () => {
608
- const candidates = Array.from({ length: 21 }, (_, i) =>
609
- createMockIssue({
610
- number: i + 1,
611
- url: `https://github.com/owner/repo/issues/${i + 1}`,
612
- itemId: `item-${i + 1}`,
613
- status: 'Awaiting Quality Check',
614
- author: 'owner-user',
615
- createdAt: new Date(2000 + i, 0, 1),
616
- }),
617
- );
618
- mockIssueRepository.getAllIssues.mockResolvedValue({
619
- project: mockProject,
620
- issues: candidates,
621
- cacheUsed: false,
622
- });
623
- mockIssueCommentRepository.getCommentsFromIssue.mockImplementation(
624
- async (issue) => {
625
- if (issue.number === 21) {
626
- return [
627
- createComment(
628
- 'bot',
629
- TRIAGER_PROPOSAL_COMMENT(
630
- 'developer',
631
- 'regular / workflow improvement',
632
- false,
633
- ),
634
- ),
635
- createComment('owner-user', 'ok'),
636
- ];
637
- }
638
- return [];
639
- },
640
- );
641
-
642
- // cycleIndex=0: windowStart=0, covers positions 0-19 (issues 1-20); issue 21 not reached
643
- const firstUseCase = new TriagerApprovalDispatchUseCase(
644
- mockProjectRepository,
645
- mockIssueRepository,
646
- mockIssueCommentRepository,
647
- );
648
- await firstUseCase.run({
649
- projectUrl: 'https://github.com/orgs/owner/projects/1',
650
- allowedIssueAuthors: ['owner-user'],
651
- cycleIndex: 0,
652
- });
653
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(0);
654
-
655
- // cycleIndex=1: windowStart=20, covers position 20 (issue 21)
656
- const secondUseCase = new TriagerApprovalDispatchUseCase(
657
- mockProjectRepository,
658
- mockIssueRepository,
659
- mockIssueCommentRepository,
660
- );
661
- await secondUseCase.run({
662
- projectUrl: 'https://github.com/orgs/owner/projects/1',
663
- allowedIssueAuthors: ['owner-user'],
664
- cycleIndex: 1,
665
- });
666
-
667
- expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledWith(
668
- 'https://github.com/owner/repo/issues/21',
669
- mockProject,
670
- 'developer-option-id',
671
- );
672
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
673
- });
674
-
675
- it('should include issues where the agent field is already set to triager in the candidate list', async () => {
676
- const issueWithTriagerAgent = createMockIssue({
677
- status: 'Awaiting Quality Check',
678
- author: 'owner-user',
679
- agent: 'triager',
680
- });
681
- mockIssueRepository.getAllIssues.mockResolvedValue({
682
- project: mockProject,
683
- issues: [issueWithTriagerAgent],
684
- cacheUsed: false,
685
- });
686
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
687
- createComment(
688
- 'bot',
689
- TRIAGER_PROPOSAL_COMMENT(
690
- 'developer',
691
- 'regular / workflow improvement',
692
- false,
693
- ),
694
- ),
695
- createComment('owner-user', 'ok'),
696
- ]);
697
-
698
- await useCase.run({
699
- projectUrl: 'https://github.com/orgs/owner/projects/1',
700
- allowedIssueAuthors: ['owner-user'],
701
- });
702
-
703
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
704
- expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledWith(
705
- 'https://github.com/owner/repo/issues/1',
706
- mockProject,
707
- 'developer-option-id',
708
- );
709
- });
710
-
711
- it('should accept はい followed by natural language as an approval token', async () => {
712
- const issue = createMockIssue({
713
- status: 'Awaiting Quality Check',
714
- author: 'owner-user',
715
- });
716
- mockIssueRepository.getAllIssues.mockResolvedValue({
717
- project: mockProject,
718
- issues: [issue],
719
- cacheUsed: false,
720
- });
721
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
722
- createComment(
723
- 'bot',
724
- TRIAGER_PROPOSAL_COMMENT(
725
- 'developer',
726
- 'regular / workflow improvement',
727
- false,
728
- ),
729
- ),
730
- createComment('owner-user', 'はい、それで良いです。進めてください'),
731
- ]);
732
-
733
- await useCase.run({
734
- projectUrl: 'https://github.com/orgs/owner/projects/1',
735
- allowedIssueAuthors: ['owner-user'],
736
- });
737
-
738
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
739
- });
740
-
741
- it('should read all candidates across consecutive cycles even when candidate count evenly divides MAX_COMMENT_FETCHES_PER_CYCLE', async () => {
742
- const candidates = Array.from({ length: 40 }, (_, i) =>
743
- createMockIssue({
744
- number: i + 1,
745
- url: `https://github.com/owner/repo/issues/${i + 1}`,
746
- itemId: `item-${i + 1}`,
747
- status: 'Awaiting Quality Check',
748
- author: 'owner-user',
749
- createdAt: new Date(2000 + i, 0, 1),
750
- }),
751
- );
752
- mockIssueRepository.getAllIssues.mockResolvedValue({
753
- project: mockProject,
754
- issues: candidates,
755
- cacheUsed: false,
756
- });
757
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([]);
758
-
759
- // cycleIndex=0: windowStart=0, covers positions 0-19 (issues 1-20)
760
- const firstUseCase = new TriagerApprovalDispatchUseCase(
761
- mockProjectRepository,
762
- mockIssueRepository,
763
- mockIssueCommentRepository,
764
- );
765
- await firstUseCase.run({
766
- projectUrl: 'https://github.com/orgs/owner/projects/1',
767
- allowedIssueAuthors: ['owner-user'],
768
- cycleIndex: 0,
769
- });
770
-
771
- // cycleIndex=1: windowStart=20, covers positions 20-39 (issues 21-40)
772
- const secondUseCase = new TriagerApprovalDispatchUseCase(
773
- mockProjectRepository,
774
- mockIssueRepository,
775
- mockIssueCommentRepository,
776
- );
777
- await secondUseCase.run({
778
- projectUrl: 'https://github.com/orgs/owner/projects/1',
779
- allowedIssueAuthors: ['owner-user'],
780
- cycleIndex: 1,
781
- });
782
-
783
- expect(
784
- mockIssueCommentRepository.getCommentsFromIssue,
785
- ).toHaveBeenCalledTimes(40);
786
- const fetchedNumbers =
787
- mockIssueCommentRepository.getCommentsFromIssue.mock.calls.map(
788
- ([issue]) => issue.number,
789
- );
790
- for (let i = 1; i <= 40; i++) {
791
- expect(fetchedNumbers).toContain(i);
792
- }
793
- });
794
-
795
- it('should dispatch using the last proposal when the owner approved after the first of multiple proposals', async () => {
796
- const issue = createMockIssue({
797
- status: 'Awaiting Quality Check',
798
- author: 'owner-user',
799
- });
800
- mockIssueRepository.getAllIssues.mockResolvedValue({
801
- project: mockProject,
802
- issues: [issue],
803
- cacheUsed: false,
804
- });
805
- // p1 (storyAlreadySet=false) → owner ok → p2 (storyAlreadySet=true)
806
- // The ok after p1 satisfies the approval gate (firstProposalIndex=0);
807
- // the last proposal (p2, storyAlreadySet=true) is used for recommendation.
808
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
809
- createComment(
810
- 'bot',
811
- TRIAGER_PROPOSAL_COMMENT(
812
- 'developer',
813
- 'regular / workflow improvement',
814
- false,
815
- ),
816
- ),
817
- createComment('owner-user', 'ok'),
818
- createComment(
819
- 'bot',
820
- TRIAGER_PROPOSAL_COMMENT(
821
- 'developer',
822
- 'regular / workflow improvement',
823
- true,
824
- ),
825
- ),
826
- ]);
827
-
828
- await useCase.run({
829
- projectUrl: 'https://github.com/orgs/owner/projects/1',
830
- allowedIssueAuthors: ['owner-user'],
831
- });
832
-
833
- expect(mockIssueRepository.updateStory.mock.calls).toEqual([]);
834
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toHaveLength(1);
835
- expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
836
- issue,
837
- expect.stringContaining('TRIAGER_PROPOSAL_APPROVED'),
838
- );
839
- });
840
-
841
- it('should skip a candidate issue when getCommentsFromIssue throws and continue processing the remaining issues', async () => {
842
- const failingIssue = createMockIssue({
843
- number: 1,
844
- url: 'https://github.com/owner/repo/issues/1',
845
- itemId: 'item-1',
846
- status: 'Awaiting Quality Check',
847
- author: 'owner-user',
848
- createdAt: new Date('2000-01-01T00:00:00Z'),
849
- });
850
- const successIssue = createMockIssue({
851
- number: 2,
852
- url: 'https://github.com/owner/repo/issues/2',
853
- itemId: 'item-2',
854
- status: 'Awaiting Quality Check',
855
- author: 'owner-user',
856
- createdAt: new Date('2000-01-02T00:00:00Z'),
857
- });
858
- mockIssueRepository.getAllIssues.mockResolvedValue({
859
- project: mockProject,
860
- issues: [failingIssue, successIssue],
861
- cacheUsed: false,
862
- });
863
- mockIssueCommentRepository.getCommentsFromIssue.mockImplementation(
864
- async (issue) => {
865
- if (issue.number === 1) {
866
- throw new Error(
867
- 'Failed to fetch comments from GitHub REST API: 404 ',
868
- );
869
- }
870
- return [
871
- createComment(
872
- 'bot',
873
- TRIAGER_PROPOSAL_COMMENT(
874
- 'developer',
875
- 'regular / workflow improvement',
876
- false,
877
- ),
878
- ),
879
- createComment('owner-user', 'ok'),
880
- ];
881
- },
882
- );
883
-
884
- await useCase.run({
885
- projectUrl: 'https://github.com/orgs/owner/projects/1',
886
- allowedIssueAuthors: ['owner-user'],
887
- });
888
-
889
- expect(
890
- mockIssueCommentRepository.getCommentsFromIssue,
891
- ).toHaveBeenCalledTimes(2);
892
- expect(mockIssueRepository.setIssueAgentField).toHaveBeenCalledTimes(1);
893
- expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
894
- mockProject,
895
- successIssue,
896
- 'awaiting-workspace-id',
897
- );
898
- });
899
-
900
- it('should propagate non-404 errors from getCommentsFromIssue', async () => {
901
- const issue = createMockIssue({
902
- number: 1,
903
- url: 'https://github.com/owner/repo/issues/1',
904
- itemId: 'item-1',
905
- status: 'Awaiting Quality Check',
906
- author: 'owner-user',
907
- createdAt: new Date('2000-01-01T00:00:00Z'),
908
- });
909
- mockIssueRepository.getAllIssues.mockResolvedValue({
910
- project: mockProject,
911
- issues: [issue],
912
- cacheUsed: false,
913
- });
914
- const authError = new Error(
915
- 'Failed to fetch comments from GitHub REST API: 401 Unauthorized',
916
- );
917
- mockIssueCommentRepository.getCommentsFromIssue.mockRejectedValue(
918
- authError,
919
- );
920
-
921
- await expect(
922
- useCase.run({
923
- projectUrl: 'https://github.com/orgs/owner/projects/1',
924
- allowedIssueAuthors: ['owner-user'],
925
- }),
926
- ).rejects.toThrow(authError);
927
- });
928
-
929
- it('should use the last triager proposal when multiple proposals exist', async () => {
930
- const issue = createMockIssue({
931
- status: 'Awaiting Quality Check',
932
- author: 'owner-user',
933
- });
934
- mockIssueRepository.getAllIssues.mockResolvedValue({
935
- project: mockProject,
936
- issues: [issue],
937
- cacheUsed: false,
938
- });
939
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
940
- createComment(
941
- 'bot',
942
- TRIAGER_PROPOSAL_COMMENT(
943
- 'developer',
944
- 'regular / workflow improvement',
945
- false,
946
- ),
947
- ),
948
- createComment(
949
- 'bot',
950
- TRIAGER_PROPOSAL_COMMENT(
951
- 'developer',
952
- 'regular / workflow improvement',
953
- true,
954
- ),
955
- ),
956
- createComment('owner-user', 'ok'),
957
- ]);
958
-
959
- await useCase.run({
960
- projectUrl: 'https://github.com/orgs/owner/projects/1',
961
- allowedIssueAuthors: ['owner-user'],
962
- });
963
-
964
- expect(mockIssueRepository.updateStory.mock.calls).toEqual([]);
965
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toHaveLength(1);
966
- });
967
-
968
- it('should not dispatch again when TRIAGER_PROPOSAL_APPROVED was already posted after the approval', async () => {
969
- const issue = createMockIssue({
970
- status: 'Awaiting Workspace',
971
- author: 'owner-user',
972
- agent: null,
973
- });
974
- mockIssueRepository.getAllIssues.mockResolvedValue({
975
- project: mockProject,
976
- issues: [issue],
977
- cacheUsed: false,
978
- });
979
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
980
- createComment(
981
- 'bot',
982
- TRIAGER_PROPOSAL_COMMENT(
983
- 'developer',
984
- 'regular / workflow improvement',
985
- false,
986
- ),
987
- ),
988
- createComment('owner-user', 'ok'),
989
- createComment(
990
- 'bot',
991
- 'Auto Status Check: TRIAGER_PROPOSAL_APPROVED\nAgent: developer\nStory: regular / workflow improvement',
992
- ),
993
- ]);
994
-
995
- await useCase.run({
996
- projectUrl: 'https://github.com/orgs/owner/projects/1',
997
- allowedIssueAuthors: ['owner-user'],
998
- });
999
-
1000
- expect(mockIssueRepository.updateStatus.mock.calls).toEqual([]);
1001
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toEqual([]);
1002
- expect(mockIssueCommentRepository.createComment.mock.calls).toEqual([]);
1003
- });
1004
-
1005
- it('should dispatch again when the issue returns to AW after the agent ran and TRIAGER_PROPOSAL_APPROVED was posted without a subsequent new proposal+approval', async () => {
1006
- const issue = createMockIssue({
1007
- status: 'Awaiting Workspace',
1008
- author: 'owner-user',
1009
- agent: null,
1010
- });
1011
- mockIssueRepository.getAllIssues.mockResolvedValue({
1012
- project: mockProject,
1013
- issues: [issue],
1014
- cacheUsed: false,
1015
- });
1016
- // second proposal after TRIAGER_PROPOSAL_APPROVED, with a new owner approval
1017
- mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1018
- createComment(
1019
- 'bot',
1020
- TRIAGER_PROPOSAL_COMMENT(
1021
- 'developer',
1022
- 'regular / workflow improvement',
1023
- false,
1024
- ),
1025
- ),
1026
- createComment('owner-user', 'ok'),
1027
- createComment(
1028
- 'bot',
1029
- 'Auto Status Check: TRIAGER_PROPOSAL_APPROVED\nAgent: developer\nStory: regular / workflow improvement',
1030
- ),
1031
- createComment(
1032
- 'bot',
1033
- TRIAGER_PROPOSAL_COMMENT(
1034
- 'developer',
1035
- 'regular / workflow improvement',
1036
- true,
1037
- ),
1038
- ),
1039
- createComment('owner-user', 'ok'),
1040
- ]);
1041
-
1042
- await useCase.run({
1043
- projectUrl: 'https://github.com/orgs/owner/projects/1',
1044
- allowedIssueAuthors: ['owner-user'],
1045
- });
1046
-
1047
- expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
1048
- expect(mockIssueRepository.setIssueAgentField.mock.calls).toHaveLength(1);
1049
- });
1050
- });
1051
- });