github-issue-tower-defence-management 1.60.2 → 1.63.1

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 (117) hide show
  1. package/.github/workflows/publish.yml +13 -0
  2. package/.github/workflows/test.yml +0 -4
  3. package/CHANGELOG.md +7 -0
  4. package/README.md +53 -10
  5. package/bin/adapter/entry-points/cli/index.js +11 -11
  6. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  7. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js +3 -22
  8. package/bin/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +8 -22
  10. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  11. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js +56 -0
  12. package/bin/adapter/entry-points/handlers/rotationOrderFileWriter.js.map +1 -0
  13. package/bin/adapter/entry-points/handlers/situationFileWriter.js +5 -0
  14. package/bin/adapter/entry-points/handlers/situationFileWriter.js.map +1 -1
  15. package/bin/adapter/proxy/TokenListLoader.js +21 -6
  16. package/bin/adapter/proxy/TokenListLoader.js.map +1 -1
  17. package/bin/adapter/proxy/proxyEntry.js +1 -0
  18. package/bin/adapter/proxy/proxyEntry.js.map +1 -1
  19. package/bin/adapter/repositories/BaseGitHubRepository.js +1 -113
  20. package/bin/adapter/repositories/BaseGitHubRepository.js.map +1 -1
  21. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js +5 -3
  22. package/bin/adapter/repositories/ProxyClaudeTokenUsageRepository.js.map +1 -1
  23. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +8 -7
  24. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  25. package/bin/domain/usecases/HandleScheduledEventUseCase.js +14 -3
  26. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  27. package/bin/domain/usecases/IssueRejectionEvaluator.js +8 -1
  28. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  29. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +5 -1
  30. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  31. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js +1 -1
  32. package/bin/domain/usecases/RevertOrphanedPreparationUseCase.js.map +1 -1
  33. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js +32 -1
  34. package/bin/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.js.map +1 -1
  35. package/bin/domain/usecases/StartPreparationUseCase.js +91 -12
  36. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  37. package/package.json +1 -4
  38. package/src/adapter/entry-points/cli/index.test.ts +16 -16
  39. package/src/adapter/entry-points/cli/index.ts +8 -11
  40. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.test.ts +2 -55
  41. package/src/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.ts +1 -11
  42. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +6 -56
  43. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +7 -11
  44. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.test.ts +177 -0
  45. package/src/adapter/entry-points/handlers/rotationOrderFileWriter.ts +20 -0
  46. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +36 -0
  47. package/src/adapter/entry-points/handlers/situationFileWriter.ts +8 -0
  48. package/src/adapter/proxy/TokenListLoader.test.ts +50 -1
  49. package/src/adapter/proxy/TokenListLoader.ts +25 -5
  50. package/src/adapter/proxy/proxyEntry.test.ts +270 -1
  51. package/src/adapter/proxy/proxyEntry.ts +2 -1
  52. package/src/adapter/repositories/BaseGitHubRepository.test.ts +1 -186
  53. package/src/adapter/repositories/BaseGitHubRepository.ts +1 -139
  54. package/src/adapter/repositories/GraphqlProjectRepository.errorHandling.test.ts +0 -1
  55. package/src/adapter/repositories/GraphqlProjectRepository.fetchProjectId.test.ts +4 -1
  56. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.test.ts +60 -19
  57. package/src/adapter/repositories/ProxyClaudeTokenUsageRepository.ts +6 -4
  58. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +23 -13
  59. package/src/adapter/repositories/issue/ApiV3IssueRepository.test.ts +0 -1
  60. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +0 -8
  61. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +0 -1
  62. package/src/domain/entities/ClaudeTokenUsage.ts +1 -0
  63. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  64. package/src/domain/usecases/HandleScheduledEventUseCase.ts +20 -5
  65. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +153 -0
  66. package/src/domain/usecases/IssueRejectionEvaluator.ts +8 -0
  67. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +175 -31
  68. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +7 -1
  69. package/src/domain/usecases/RevertNotReadyAwaitingQualityCheckUseCase.test.ts +32 -0
  70. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +39 -5
  71. package/src/domain/usecases/RevertOrphanedPreparationUseCase.ts +1 -1
  72. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +139 -20
  73. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.ts +62 -2
  74. package/src/domain/usecases/StartPreparationUseCase.test.ts +404 -21
  75. package/src/domain/usecases/StartPreparationUseCase.ts +152 -16
  76. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +16 -0
  77. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  78. package/types/adapter/entry-points/handlers/GetStoryObjectMapUseCaseHandler.d.ts.map +1 -1
  79. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  80. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts +3 -0
  81. package/types/adapter/entry-points/handlers/rotationOrderFileWriter.d.ts.map +1 -0
  82. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts +1 -0
  83. package/types/adapter/entry-points/handlers/situationFileWriter.d.ts.map +1 -1
  84. package/types/adapter/proxy/TokenListLoader.d.ts +5 -0
  85. package/types/adapter/proxy/TokenListLoader.d.ts.map +1 -1
  86. package/types/adapter/proxy/proxyEntry.d.ts +2 -1
  87. package/types/adapter/proxy/proxyEntry.d.ts.map +1 -1
  88. package/types/adapter/repositories/BaseGitHubRepository.d.ts +1 -23
  89. package/types/adapter/repositories/BaseGitHubRepository.d.ts.map +1 -1
  90. package/types/adapter/repositories/ProxyClaudeTokenUsageRepository.d.ts.map +1 -1
  91. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +14 -5
  92. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  93. package/types/domain/entities/ClaudeTokenUsage.d.ts +1 -0
  94. package/types/domain/entities/ClaudeTokenUsage.d.ts.map +1 -1
  95. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  96. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  97. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -1
  98. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  99. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  100. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts +5 -2
  101. package/types/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.d.ts.map +1 -1
  102. package/types/domain/usecases/StartPreparationUseCase.d.ts +15 -1
  103. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  104. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +14 -0
  105. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  106. package/bin/adapter/repositories/issue/CheerioIssueRepository.js +0 -136
  107. package/bin/adapter/repositories/issue/CheerioIssueRepository.js.map +0 -1
  108. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js +0 -1606
  109. package/bin/adapter/repositories/issue/InternalGraphqlIssueRepository.js.map +0 -1
  110. package/src/adapter/repositories/issue/CheerioIssueRepository.test.ts +0 -6552
  111. package/src/adapter/repositories/issue/CheerioIssueRepository.ts +0 -142
  112. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.test.ts +0 -118
  113. package/src/adapter/repositories/issue/InternalGraphqlIssueRepository.ts +0 -584
  114. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts +0 -40
  115. package/types/adapter/repositories/issue/CheerioIssueRepository.d.ts.map +0 -1
  116. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts +0 -220
  117. package/types/adapter/repositories/issue/InternalGraphqlIssueRepository.d.ts.map +0 -1
@@ -0,0 +1,153 @@
1
+ import { IssueRejectionEvaluator } from './IssueRejectionEvaluator';
2
+ import { RelatedPullRequest } from './adapter-interfaces/IssueRepository';
3
+
4
+ const createReadyPr = (
5
+ url = 'https://github.com/user/repo/pull/1',
6
+ overrides: Partial<RelatedPullRequest> = {},
7
+ ): RelatedPullRequest => ({
8
+ url,
9
+ branchName: 'feature-branch',
10
+ createdAt: new Date('2000-01-01T00:00:00Z'),
11
+ isDraft: false,
12
+ isConflicted: false,
13
+ isPassedAllCiJob: true,
14
+ isCiStateSuccess: true,
15
+ isResolvedAllReviewComments: true,
16
+ isBranchOutOfDate: false,
17
+ missingRequiredCheckNames: [],
18
+ ...overrides,
19
+ });
20
+
21
+ describe('IssueRejectionEvaluator', () => {
22
+ let mockIssueRepository: {
23
+ findRelatedOpenPRs: jest.Mock;
24
+ getOpenPullRequest: jest.Mock;
25
+ };
26
+ let evaluator: IssueRejectionEvaluator;
27
+
28
+ beforeEach(() => {
29
+ jest.resetAllMocks();
30
+
31
+ mockIssueRepository = {
32
+ findRelatedOpenPRs: jest.fn(),
33
+ getOpenPullRequest: jest.fn(),
34
+ };
35
+
36
+ evaluator = new IssueRejectionEvaluator(mockIssueRepository);
37
+ });
38
+
39
+ describe('evaluate', () => {
40
+ it('should return no rejections when PR is ready and not draft', async () => {
41
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
42
+ createReadyPr(),
43
+ ]);
44
+
45
+ const result = await evaluator.evaluate({
46
+ url: 'https://github.com/user/repo/issues/1',
47
+ labels: [],
48
+ isPr: false,
49
+ });
50
+
51
+ expect(result.rejections).toHaveLength(0);
52
+ expect(result.approvedPrUrl).toBe('https://github.com/user/repo/pull/1');
53
+ });
54
+
55
+ it('should reject with PULL_REQUEST_IS_DRAFT when PR is in draft state', async () => {
56
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
57
+ createReadyPr('https://github.com/user/repo/pull/1', { isDraft: true }),
58
+ ]);
59
+
60
+ const result = await evaluator.evaluate({
61
+ url: 'https://github.com/user/repo/issues/1',
62
+ labels: [],
63
+ isPr: false,
64
+ });
65
+
66
+ expect(result.rejections).toHaveLength(1);
67
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_IS_DRAFT');
68
+ expect(result.rejections[0].detail).toContain('PULL_REQUEST_IS_DRAFT');
69
+ expect(result.rejections[0].detail).toContain(
70
+ 'https://github.com/user/repo/pull/1',
71
+ );
72
+ expect(result.approvedPrUrl).toBeNull();
73
+ });
74
+
75
+ it('should not approve a draft PR even when all other checks pass', async () => {
76
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
77
+ createReadyPr('https://github.com/user/repo/pull/1', { isDraft: true }),
78
+ ]);
79
+
80
+ const result = await evaluator.evaluate({
81
+ url: 'https://github.com/user/repo/issues/1',
82
+ labels: [],
83
+ isPr: false,
84
+ });
85
+
86
+ expect(result.approvedPrUrl).toBeNull();
87
+ });
88
+
89
+ it('should accumulate PULL_REQUEST_IS_DRAFT alongside PULL_REQUEST_CONFLICTED when draft PR is also conflicted', async () => {
90
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
91
+ createReadyPr('https://github.com/user/repo/pull/1', {
92
+ isDraft: true,
93
+ isConflicted: true,
94
+ }),
95
+ ]);
96
+
97
+ const result = await evaluator.evaluate({
98
+ url: 'https://github.com/user/repo/issues/1',
99
+ labels: [],
100
+ isPr: false,
101
+ });
102
+
103
+ const rejectionTypes = result.rejections.map((r) => r.type);
104
+ expect(rejectionTypes).toContain('PULL_REQUEST_IS_DRAFT');
105
+ expect(rejectionTypes).toContain('PULL_REQUEST_CONFLICTED');
106
+ expect(result.approvedPrUrl).toBeNull();
107
+ });
108
+
109
+ it('should reject with PULL_REQUEST_IS_DRAFT when PR item (isPr=true) is in draft state', async () => {
110
+ mockIssueRepository.getOpenPullRequest.mockResolvedValue(
111
+ createReadyPr('https://github.com/user/repo/pull/10', {
112
+ isDraft: true,
113
+ }),
114
+ );
115
+
116
+ const result = await evaluator.evaluate({
117
+ url: 'https://github.com/user/repo/pull/10',
118
+ labels: [],
119
+ isPr: true,
120
+ });
121
+
122
+ expect(mockIssueRepository.getOpenPullRequest).toHaveBeenCalledWith(
123
+ 'https://github.com/user/repo/pull/10',
124
+ );
125
+ expect(result.rejections).toHaveLength(1);
126
+ expect(result.rejections[0].type).toBe('PULL_REQUEST_IS_DRAFT');
127
+ expect(result.approvedPrUrl).toBeNull();
128
+ });
129
+
130
+ it('should not reject for draft state when issue has llm-agent label', async () => {
131
+ const result = await evaluator.evaluate({
132
+ url: 'https://github.com/user/repo/issues/1',
133
+ labels: ['llm-agent'],
134
+ isPr: false,
135
+ });
136
+
137
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
138
+ expect(result.rejections).toHaveLength(0);
139
+ expect(result.approvedPrUrl).toBeNull();
140
+ });
141
+
142
+ it('should not reject for draft state when issue has non-e2e category label', async () => {
143
+ const result = await evaluator.evaluate({
144
+ url: 'https://github.com/user/repo/issues/1',
145
+ labels: ['category:frontend'],
146
+ isPr: false,
147
+ });
148
+
149
+ expect(mockIssueRepository.findRelatedOpenPRs).not.toHaveBeenCalled();
150
+ expect(result.rejections).toHaveLength(0);
151
+ });
152
+ });
153
+ });
@@ -6,6 +6,7 @@ import {
6
6
  export type PrRejectedReasonType =
7
7
  | 'PULL_REQUEST_NOT_FOUND'
8
8
  | 'MULTIPLE_PULL_REQUESTS_FOUND'
9
+ | 'PULL_REQUEST_IS_DRAFT'
9
10
  | 'PULL_REQUEST_CONFLICTED'
10
11
  | 'ANY_CI_JOB_FAILED_OR_IN_PROGRESS'
11
12
  | 'REQUIRED_CI_JOB_NEVER_STARTED'
@@ -59,6 +60,12 @@ export class IssueRejectionEvaluator {
59
60
  });
60
61
  } else {
61
62
  const pr = prsToCheck[0];
63
+ if (pr.isDraft) {
64
+ rejections.push({
65
+ type: 'PULL_REQUEST_IS_DRAFT',
66
+ detail: `PULL_REQUEST_IS_DRAFT: ${pr.url}`,
67
+ });
68
+ }
62
69
  if (pr.isConflicted) {
63
70
  rejections.push({
64
71
  type: 'PULL_REQUEST_CONFLICTED',
@@ -90,6 +97,7 @@ export class IssueRejectionEvaluator {
90
97
  });
91
98
  }
92
99
  if (
100
+ !pr.isDraft &&
93
101
  !pr.isConflicted &&
94
102
  pr.isPassedAllCiJob &&
95
103
  pr.isResolvedAllReviewComments
@@ -77,7 +77,7 @@ const createMockIssue = (overrides: Partial<Issue> = {}): Issue => ({
77
77
 
78
78
  const createMockComment = (overrides: Partial<Comment> = {}): Comment => ({
79
79
  author: 'test-user',
80
- content: 'From: Test comment',
80
+ content: 'From: :robot: Test comment',
81
81
  createdAt: new Date(),
82
82
  ...overrides,
83
83
  });
@@ -108,7 +108,12 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
108
108
  beforeEach(() => {
109
109
  jest.resetAllMocks();
110
110
 
111
- mockProject = createMockProject();
111
+ mockProject = createMockProject({
112
+ dependedIssueUrlSeparatedByComma: {
113
+ name: 'Depended Issue URL',
114
+ fieldId: 'depended-field-id',
115
+ },
116
+ });
112
117
 
113
118
  mockProjectRepository = {
114
119
  getByUrl: jest.fn(),
@@ -150,7 +155,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
150
155
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
151
156
  mockIssueRepository.get.mockResolvedValue(issue);
152
157
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
153
- createMockComment({ content: 'From: Test report' }),
158
+ createMockComment({ content: 'From: :robot: Test report' }),
154
159
  ]);
155
160
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
156
161
  {
@@ -205,7 +210,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
205
210
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
206
211
  mockIssueRepository.get.mockResolvedValue(issue);
207
212
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
208
- createMockComment({ content: 'From: Agent report' }),
213
+ createMockComment({ content: 'From: :robot: Agent report' }),
209
214
  ]);
210
215
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
211
216
  {
@@ -511,6 +516,53 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
511
516
  );
512
517
  });
513
518
 
519
+ it('should reject with NO_REPORT_FROM_AGENT_BOT when last comment is a cross-issue notification starting with From: :warning:', async () => {
520
+ const issue = createMockIssue({
521
+ url: 'https://github.com/user/repo/issues/1',
522
+ status: 'Preparation',
523
+ });
524
+
525
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
526
+ mockIssueRepository.get.mockResolvedValue(issue);
527
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
528
+ createMockComment({
529
+ content:
530
+ 'From: :warning: This message is from https://github.com/user/repo/tree/i999 AI HS Implement AI Agent (claude-sonnet-4-6)',
531
+ }),
532
+ ]);
533
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
534
+ {
535
+ url: 'https://github.com/user/repo/pull/1',
536
+ isConflicted: false,
537
+ isPassedAllCiJob: true,
538
+ isCiStateSuccess: true,
539
+ isResolvedAllReviewComments: true,
540
+ isBranchOutOfDate: false,
541
+ missingRequiredCheckNames: [],
542
+ },
543
+ ]);
544
+
545
+ await useCase.run({
546
+ projectUrl: 'https://github.com/users/user/projects/1',
547
+ issueUrl: 'https://github.com/user/repo/issues/1',
548
+ thresholdForAutoReject: 3,
549
+ workflowBlockerResolvedWebhookUrl: null,
550
+ });
551
+
552
+ expect(mockIssueRepository.update).toHaveBeenCalledWith(
553
+ expect.objectContaining({
554
+ status: 'Awaiting Workspace',
555
+ }),
556
+ mockProject,
557
+ );
558
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
559
+ expect.objectContaining({
560
+ url: 'https://github.com/user/repo/issues/1',
561
+ }),
562
+ expect.stringContaining('NO_REPORT_FROM_AGENT_BOT'),
563
+ );
564
+ });
565
+
514
566
  it('should reject and set status to Awaiting Workspace when no comments exist', async () => {
515
567
  const issue = createMockIssue({
516
568
  url: 'https://github.com/user/repo/issues/1',
@@ -559,7 +611,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
559
611
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
560
612
  createMockComment({
561
613
  content:
562
- 'From: Agent report\n```json\n{"nextStep": "Fix the tests"}\n```',
614
+ 'From: :robot: Agent report\n```json\n{"nextStep": "Fix the tests"}\n```',
563
615
  }),
564
616
  ]);
565
617
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -601,7 +653,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
601
653
  mockIssueRepository.get.mockResolvedValue(issue);
602
654
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
603
655
  createMockComment({
604
- content: 'From: Agent report\n```json\n{"nextStep": null}\n```',
656
+ content: 'From: :robot: Agent report\n```json\n{"nextStep": null}\n```',
605
657
  }),
606
658
  ]);
607
659
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -691,7 +743,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
691
743
  createMockComment({ content: 'Auto Status Check: REJECTED - first' }),
692
744
  createMockComment({ content: 'Auto Status Check: REJECTED - second' }),
693
745
  createMockComment({ content: 'Auto Status Check: REJECTED - third' }),
694
- createMockComment({ content: 'From: Agent final report' }),
746
+ createMockComment({ content: 'From: :robot: Agent final report' }),
695
747
  ]);
696
748
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
697
749
  {
@@ -915,7 +967,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
915
967
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
916
968
  mockIssueRepository.get.mockResolvedValue(issue);
917
969
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
918
- createMockComment({ content: 'From: Test report' }),
970
+ createMockComment({ content: 'From: :robot: Test report' }),
919
971
  ]);
920
972
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
921
973
 
@@ -949,7 +1001,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
949
1001
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
950
1002
  mockIssueRepository.get.mockResolvedValue(issue);
951
1003
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
952
- createMockComment({ content: 'From: Test report' }),
1004
+ createMockComment({ content: 'From: :robot: Test report' }),
953
1005
  ]);
954
1006
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
955
1007
  {
@@ -1002,7 +1054,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1002
1054
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1003
1055
  mockIssueRepository.get.mockResolvedValue(issue);
1004
1056
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1005
- createMockComment({ content: 'From: Test report' }),
1057
+ createMockComment({ content: 'From: :robot: Test report' }),
1006
1058
  ]);
1007
1059
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1008
1060
  {
@@ -1046,7 +1098,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1046
1098
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1047
1099
  mockIssueRepository.get.mockResolvedValue(issue);
1048
1100
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1049
- createMockComment({ content: 'From: Test report' }),
1101
+ createMockComment({ content: 'From: :robot: Test report' }),
1050
1102
  ]);
1051
1103
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1052
1104
  {
@@ -1090,7 +1142,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1090
1142
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1091
1143
  mockIssueRepository.get.mockResolvedValue(issue);
1092
1144
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1093
- createMockComment({ content: 'From: Test report' }),
1145
+ createMockComment({ content: 'From: :robot: Test report' }),
1094
1146
  ]);
1095
1147
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1096
1148
  {
@@ -1140,7 +1192,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1140
1192
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1141
1193
  mockIssueRepository.get.mockResolvedValue(issue);
1142
1194
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1143
- createMockComment({ content: 'From: Test report' }),
1195
+ createMockComment({ content: 'From: :robot: Test report' }),
1144
1196
  ]);
1145
1197
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1146
1198
  {
@@ -1190,7 +1242,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1190
1242
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1191
1243
  mockIssueRepository.get.mockResolvedValue(issue);
1192
1244
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1193
- createMockComment({ content: 'From: Test report' }),
1245
+ createMockComment({ content: 'From: :robot: Test report' }),
1194
1246
  ]);
1195
1247
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1196
1248
  {
@@ -1228,7 +1280,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1228
1280
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1229
1281
  mockIssueRepository.get.mockResolvedValue(issue);
1230
1282
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1231
- createMockComment({ content: 'From: Test report' }),
1283
+ createMockComment({ content: 'From: :robot: Test report' }),
1232
1284
  ]);
1233
1285
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1234
1286
  {
@@ -1263,6 +1315,51 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1263
1315
  );
1264
1316
  });
1265
1317
 
1318
+ it('should reject when PR is in draft state', async () => {
1319
+ const issue = createMockIssue({
1320
+ url: 'https://github.com/user/repo/issues/1',
1321
+ status: 'Preparation',
1322
+ });
1323
+
1324
+ mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1325
+ mockIssueRepository.get.mockResolvedValue(issue);
1326
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1327
+ createMockComment({ content: 'From: Test report' }),
1328
+ ]);
1329
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1330
+ {
1331
+ url: 'https://github.com/user/repo/pull/1',
1332
+ isDraft: true,
1333
+ isConflicted: false,
1334
+ isPassedAllCiJob: true,
1335
+ isCiStateSuccess: true,
1336
+ isResolvedAllReviewComments: true,
1337
+ isBranchOutOfDate: false,
1338
+ missingRequiredCheckNames: [],
1339
+ },
1340
+ ]);
1341
+
1342
+ await useCase.run({
1343
+ projectUrl: 'https://github.com/users/user/projects/1',
1344
+ issueUrl: 'https://github.com/user/repo/issues/1',
1345
+ thresholdForAutoReject: 3,
1346
+ workflowBlockerResolvedWebhookUrl: null,
1347
+ });
1348
+
1349
+ expect(mockIssueRepository.update).toHaveBeenCalledWith(
1350
+ expect.objectContaining({
1351
+ status: 'Awaiting Workspace',
1352
+ }),
1353
+ mockProject,
1354
+ );
1355
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
1356
+ expect.objectContaining({
1357
+ url: 'https://github.com/user/repo/issues/1',
1358
+ }),
1359
+ expect.stringContaining('PULL_REQUEST_IS_DRAFT'),
1360
+ );
1361
+ });
1362
+
1266
1363
  it('should skip PR checks and update to Awaiting Quality Check when issue has category label', async () => {
1267
1364
  const issue = createMockIssue({
1268
1365
  url: 'https://github.com/user/repo/issues/1',
@@ -1273,7 +1370,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1273
1370
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1274
1371
  mockIssueRepository.get.mockResolvedValue(issue);
1275
1372
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1276
- createMockComment({ content: 'From: Test report' }),
1373
+ createMockComment({ content: 'From: :robot: Test report' }),
1277
1374
  ]);
1278
1375
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
1279
1376
 
@@ -1302,7 +1399,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1302
1399
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1303
1400
  mockIssueRepository.get.mockResolvedValue(issue);
1304
1401
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1305
- createMockComment({ content: 'From: Test report' }),
1402
+ createMockComment({ content: 'From: :robot: Test report' }),
1306
1403
  ]);
1307
1404
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1308
1405
  {
@@ -1379,7 +1476,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1379
1476
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1380
1477
  mockIssueRepository.get.mockResolvedValue(issue);
1381
1478
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1382
- createMockComment({ content: 'From: Test report' }),
1479
+ createMockComment({ content: 'From: :robot: Test report' }),
1383
1480
  ]);
1384
1481
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
1385
1482
 
@@ -1406,7 +1503,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1406
1503
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1407
1504
  mockIssueRepository.get.mockResolvedValue(issue);
1408
1505
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1409
- createMockComment({ content: 'From: Test report' }),
1506
+ createMockComment({ content: 'From: :robot: Test report' }),
1410
1507
  ]);
1411
1508
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
1412
1509
 
@@ -1466,7 +1563,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1466
1563
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1467
1564
  mockIssueRepository.get.mockResolvedValue(prIssue);
1468
1565
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1469
- createMockComment({ content: 'From: Agent report' }),
1566
+ createMockComment({ content: 'From: :robot: Agent report' }),
1470
1567
  ]);
1471
1568
  mockIssueRepository.getOpenPullRequest.mockResolvedValue({
1472
1569
  url: 'https://github.com/user/repo/pull/10',
@@ -1665,6 +1762,53 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1665
1762
  'https://github.com/user/repo/issues/1',
1666
1763
  );
1667
1764
  });
1765
+
1766
+ it('should log a warning and skip setDependedIssueUrl when dependedIssueUrlSeparatedByComma is not configured in project', async () => {
1767
+ const projectWithoutDependedField = createMockProject({
1768
+ dependedIssueUrlSeparatedByComma: null,
1769
+ });
1770
+ const issue = createMockIssue({
1771
+ url: 'https://github.com/user/repo/issues/1',
1772
+ status: 'Preparation',
1773
+ });
1774
+
1775
+ mockProjectRepository.getByUrl.mockResolvedValue(
1776
+ projectWithoutDependedField,
1777
+ );
1778
+ mockIssueRepository.get.mockResolvedValue(issue);
1779
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1780
+ createMockComment({ content: 'From: Agent report' }),
1781
+ ]);
1782
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1783
+ {
1784
+ url: 'https://github.com/user/repo/pull/10',
1785
+ isConflicted: false,
1786
+ isPassedAllCiJob: true,
1787
+ isCiStateSuccess: true,
1788
+ isResolvedAllReviewComments: true,
1789
+ isBranchOutOfDate: false,
1790
+ missingRequiredCheckNames: [],
1791
+ },
1792
+ ]);
1793
+
1794
+ const consoleWarnSpy = jest
1795
+ .spyOn(console, 'warn')
1796
+ .mockImplementation(() => {});
1797
+
1798
+ await useCase.run({
1799
+ projectUrl: 'https://github.com/users/user/projects/1',
1800
+ issueUrl: 'https://github.com/user/repo/issues/1',
1801
+ thresholdForAutoReject: 3,
1802
+ workflowBlockerResolvedWebhookUrl: null,
1803
+ });
1804
+
1805
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
1806
+ expect.stringContaining('dependedIssueUrlSeparatedByComma'),
1807
+ );
1808
+ expect(mockIssueRepository.setDependedIssueUrl).not.toHaveBeenCalled();
1809
+
1810
+ consoleWarnSpy.mockRestore();
1811
+ });
1668
1812
  });
1669
1813
 
1670
1814
  describe('workflow blocker webhook notification', () => {
@@ -1713,7 +1857,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1713
1857
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1714
1858
  mockIssueRepository.get.mockResolvedValue(issue);
1715
1859
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1716
- createMockComment({ content: 'From: Test report' }),
1860
+ createMockComment({ content: 'From: :robot: Test report' }),
1717
1861
  ]);
1718
1862
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1719
1863
  {
@@ -1794,7 +1938,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1794
1938
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1795
1939
  mockIssueRepository.get.mockResolvedValue(issue);
1796
1940
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1797
- createMockComment({ content: 'From: Test report' }),
1941
+ createMockComment({ content: 'From: :robot: Test report' }),
1798
1942
  ]);
1799
1943
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1800
1944
  {
@@ -1831,7 +1975,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1831
1975
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1832
1976
  mockIssueRepository.get.mockResolvedValue(issue);
1833
1977
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1834
- createMockComment({ content: 'From: Test report' }),
1978
+ createMockComment({ content: 'From: :robot: Test report' }),
1835
1979
  ]);
1836
1980
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1837
1981
  {
@@ -1864,7 +2008,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1864
2008
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1865
2009
  mockIssueRepository.get.mockResolvedValue(issue);
1866
2010
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1867
- createMockComment({ content: 'From: Test report' }),
2011
+ createMockComment({ content: 'From: :robot: Test report' }),
1868
2012
  ]);
1869
2013
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1870
2014
  {
@@ -1919,7 +2063,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1919
2063
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
1920
2064
  mockIssueRepository.get.mockResolvedValue(issue);
1921
2065
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1922
- createMockComment({ content: 'From: Test report' }),
2066
+ createMockComment({ content: 'From: :robot: Test report' }),
1923
2067
  ]);
1924
2068
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1925
2069
  {
@@ -1974,7 +2118,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
1974
2118
  new Error('Story map unavailable'),
1975
2119
  );
1976
2120
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
1977
- createMockComment({ content: 'From: Test report' }),
2121
+ createMockComment({ content: 'From: :robot: Test report' }),
1978
2122
  ]);
1979
2123
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
1980
2124
  {
@@ -2019,7 +2163,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
2019
2163
  mockProjectRepository.getByUrl.mockResolvedValue(mockProject);
2020
2164
  mockIssueRepository.get.mockResolvedValue(prIssue);
2021
2165
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
2022
- createMockComment({ content: 'From: Agent report' }),
2166
+ createMockComment({ content: 'From: :robot: Agent report' }),
2023
2167
  ]);
2024
2168
  mockIssueRepository.getOpenPullRequest.mockResolvedValue(null);
2025
2169
 
@@ -2053,7 +2197,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
2053
2197
  mockIssueRepository.get.mockResolvedValue(issue);
2054
2198
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
2055
2199
  createMockComment({
2056
- content: 'From: Agent report\n```json\n{invalid json}\n```',
2200
+ content: 'From: :robot: Agent report\n```json\n{invalid json}\n```',
2057
2201
  }),
2058
2202
  ]);
2059
2203
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -2091,7 +2235,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
2091
2235
  mockIssueRepository.get.mockResolvedValue(issue);
2092
2236
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
2093
2237
  createMockComment({
2094
- content: 'From: Agent report\n```json\nnull\n```',
2238
+ content: 'From: :robot: Agent report\n```json\nnull\n```',
2095
2239
  }),
2096
2240
  ]);
2097
2241
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -2130,7 +2274,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
2130
2274
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
2131
2275
  createMockComment({
2132
2276
  content:
2133
- 'From: Agent report\n```json\n{"status": "done", "result": "success"}\n```',
2277
+ 'From: :robot: Agent report\n```json\n{"status": "done", "result": "success"}\n```',
2134
2278
  }),
2135
2279
  ]);
2136
2280
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -2168,7 +2312,7 @@ describe('NotifyFinishedIssuePreparationUseCase', () => {
2168
2312
  mockIssueRepository.get.mockResolvedValue(issue);
2169
2313
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
2170
2314
  createMockComment({
2171
- content: 'From: Agent report\n```json\n"just a string"\n```',
2315
+ content: 'From: :robot: Agent report\n```json\n"just a string"\n```',
2172
2316
  }),
2173
2317
  ]);
2174
2318
  mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
@@ -266,7 +266,7 @@ export class NotifyFinishedIssuePreparationUseCase {
266
266
  const rejections: { type: RejectedReasonType; detail: string }[] = [];
267
267
 
268
268
  const lastComment = comments[comments.length - 1];
269
- if (!lastComment || !lastComment.content.startsWith('From:')) {
269
+ if (!lastComment || !lastComment.content.startsWith('From: :robot:')) {
270
270
  rejections.push({
271
271
  type: 'NO_REPORT_FROM_AGENT_BOT',
272
272
  detail: 'NO_REPORT_FROM_AGENT_BOT',
@@ -313,6 +313,12 @@ export class NotifyFinishedIssuePreparationUseCase {
313
313
  issueUrl: string,
314
314
  project: Parameters<IssueRepository['get']>[1],
315
315
  ): Promise<void> => {
316
+ if (!project.dependedIssueUrlSeparatedByComma) {
317
+ console.warn(
318
+ `dependedIssueUrlSeparatedByComma field not configured in project, skipping depended issue URL update for issue ${issueUrl}`,
319
+ );
320
+ return;
321
+ }
316
322
  const openPRs = issue.isPr
317
323
  ? await this.resolveOpenPrsForPrItem(issue.url)
318
324
  : await this.issueRepository.findRelatedOpenPRs(issue.url);
@@ -304,6 +304,38 @@ describe('RevertNotReadyAwaitingQualityCheckUseCase', () => {
304
304
  );
305
305
  });
306
306
 
307
+ it('should revert issue when linked PR is in draft state', async () => {
308
+ const issue = createMockIssue({
309
+ status: 'Awaiting Quality Check',
310
+ });
311
+ mockIssueRepository.getAllIssues.mockResolvedValue({
312
+ issues: [issue],
313
+ cacheUsed: false,
314
+ });
315
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
316
+ { ...createReadyPr(), isDraft: true },
317
+ ]);
318
+
319
+ await useCase.run({
320
+ projectUrl: 'https://github.com/users/user/projects/1',
321
+ allowIssueCacheMinutes: 10,
322
+ });
323
+
324
+ expect(mockIssueRepository.updateStatus).toHaveBeenCalledWith(
325
+ mockProject,
326
+ issue,
327
+ 'awaiting-workspace-id',
328
+ );
329
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
330
+ issue,
331
+ expect.stringContaining('Auto Status Check: REJECTED'),
332
+ );
333
+ expect(mockIssueCommentRepository.createComment).toHaveBeenCalledWith(
334
+ issue,
335
+ expect.stringContaining('PULL_REQUEST_IS_DRAFT'),
336
+ );
337
+ });
338
+
307
339
  it('should revert issue when multiple linked open PRs are found', async () => {
308
340
  const issue = createMockIssue({
309
341
  status: 'Awaiting Quality Check',