github-issue-tower-defence-management 1.89.0 → 1.91.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 (160) hide show
  1. package/.github/CODEOWNERS +1 -2
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/adapter/entry-points/cli/index.js +16 -12
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  11. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  12. package/bin/adapter/entry-points/console/consoleServer.js +43 -17
  13. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  15. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  16. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/jest.config.js +57 -9
  20. package/package.json +17 -13
  21. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  22. package/src/adapter/entry-points/cli/index.ts +30 -12
  23. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  25. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  26. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  27. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  28. package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
  29. package/src/adapter/entry-points/console/consoleServer.ts +53 -23
  30. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  94. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  97. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
  132. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  135. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  136. package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
  137. package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
  138. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  140. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  141. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  143. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  144. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  145. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  146. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  147. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  148. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  149. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  150. package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
  151. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  152. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  153. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  154. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  155. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  156. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  157. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
  158. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
  159. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  160. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
@@ -69,12 +69,21 @@ describe('consoleOperationApi', () => {
69
69
  };
70
70
  context = {
71
71
  issueRepository,
72
- project,
72
+ resolveProject: async (pjcode: string) =>
73
+ pjcode === 'umino' ? { pjcode, project } : null,
73
74
  consoleDataOutputDir: baseDir,
74
- pjcode: 'umino',
75
75
  };
76
76
  });
77
77
 
78
+ const contextForProject = (
79
+ nextProject: Project,
80
+ ): ConsoleOperationContext => ({
81
+ issueRepository,
82
+ resolveProject: async (pjcode: string) =>
83
+ pjcode === 'umino' ? { pjcode, project: nextProject } : null,
84
+ consoleDataOutputDir: baseDir,
85
+ });
86
+
78
87
  afterEach(() => {
79
88
  fs.rmSync(baseDir, { recursive: true, force: true });
80
89
  });
@@ -90,6 +99,7 @@ describe('consoleOperationApi', () => {
90
99
  describe('handleReview', () => {
91
100
  it('approves and sets Awaiting workspace then records done', async () => {
92
101
  const response = await handleReview(context, {
102
+ pjcode: 'umino',
93
103
  action: 'approve',
94
104
  prUrl: 'https://github.com/o/r/pull/1',
95
105
  projectItemId: 'PVTI_a',
@@ -108,6 +118,7 @@ describe('consoleOperationApi', () => {
108
118
 
109
119
  it('requests changes with the inline comment', async () => {
110
120
  const response = await handleReview(context, {
121
+ pjcode: 'umino',
111
122
  action: 'request_changes',
112
123
  prUrl: 'https://github.com/o/r/pull/1',
113
124
  projectItemId: 'PVTI_b',
@@ -127,6 +138,7 @@ describe('consoleOperationApi', () => {
127
138
 
128
139
  it('rejects request_changes without a comment body', async () => {
129
140
  const response = await handleReview(context, {
141
+ pjcode: 'umino',
130
142
  action: 'request_changes',
131
143
  prUrl: 'https://github.com/o/r/pull/1',
132
144
  projectItemId: 'PVTI_b',
@@ -139,6 +151,7 @@ describe('consoleOperationApi', () => {
139
151
 
140
152
  it('closes a pull request and posts a comment', async () => {
141
153
  const response = await handleReview(context, {
154
+ pjcode: 'umino',
142
155
  action: 'close',
143
156
  prUrl: 'https://github.com/o/r/pull/1',
144
157
  projectItemId: 'PVTI_c',
@@ -157,6 +170,7 @@ describe('consoleOperationApi', () => {
157
170
 
158
171
  it('rejects an unknown review action', async () => {
159
172
  const response = await handleReview(context, {
173
+ pjcode: 'umino',
160
174
  action: 'frobnicate',
161
175
  prUrl: 'https://github.com/o/r/pull/1',
162
176
  projectItemId: 'PVTI_c',
@@ -166,6 +180,7 @@ describe('consoleOperationApi', () => {
166
180
 
167
181
  it('rejects a missing prUrl', async () => {
168
182
  const response = await handleReview(context, {
183
+ pjcode: 'umino',
169
184
  action: 'approve',
170
185
  projectItemId: 'PVTI_c',
171
186
  });
@@ -174,6 +189,7 @@ describe('consoleOperationApi', () => {
174
189
 
175
190
  it('rejects a missing action', async () => {
176
191
  const response = await handleReview(context, {
192
+ pjcode: 'umino',
177
193
  prUrl: 'https://github.com/o/r/pull/1',
178
194
  projectItemId: 'PVTI_c',
179
195
  });
@@ -182,6 +198,7 @@ describe('consoleOperationApi', () => {
182
198
 
183
199
  it('rejects a missing projectItemId', async () => {
184
200
  const response = await handleReview(context, {
201
+ pjcode: 'umino',
185
202
  action: 'approve',
186
203
  prUrl: 'https://github.com/o/r/pull/1',
187
204
  });
@@ -191,6 +208,7 @@ describe('consoleOperationApi', () => {
191
208
  it('returns 400 when the issue cannot be loaded during approve', async () => {
192
209
  issueRepository.get.mockResolvedValue(null);
193
210
  const response = await handleReview(context, {
211
+ pjcode: 'umino',
194
212
  action: 'approve',
195
213
  prUrl: 'https://github.com/o/r/pull/1',
196
214
  projectItemId: 'PVTI_c',
@@ -200,14 +218,12 @@ describe('consoleOperationApi', () => {
200
218
  });
201
219
 
202
220
  it('returns 400 when the Awaiting workspace status is absent', async () => {
203
- const contextWithoutStatus: ConsoleOperationContext = {
204
- ...context,
205
- project: {
206
- ...project,
207
- status: { name: 'Status', fieldId: 'f', statuses: [] },
208
- },
209
- };
221
+ const contextWithoutStatus = contextForProject({
222
+ ...project,
223
+ status: { name: 'Status', fieldId: 'f', statuses: [] },
224
+ });
210
225
  const response = await handleReview(contextWithoutStatus, {
226
+ pjcode: 'umino',
211
227
  action: 'approve',
212
228
  prUrl: 'https://github.com/o/r/pull/1',
213
229
  projectItemId: 'PVTI_c',
@@ -219,6 +235,7 @@ describe('consoleOperationApi', () => {
219
235
  describe('handleTriage', () => {
220
236
  it('sets the status by name', async () => {
221
237
  const response = await handleTriage(context, {
238
+ pjcode: 'umino',
222
239
  action: 'set_status',
223
240
  issueUrl: 'https://github.com/o/r/issues/1',
224
241
  projectItemId: 'PVTI_d',
@@ -235,6 +252,7 @@ describe('consoleOperationApi', () => {
235
252
 
236
253
  it('rejects an unknown status name', async () => {
237
254
  const response = await handleTriage(context, {
255
+ pjcode: 'umino',
238
256
  action: 'set_status',
239
257
  issueUrl: 'https://github.com/o/r/issues/1',
240
258
  projectItemId: 'PVTI_d',
@@ -246,6 +264,7 @@ describe('consoleOperationApi', () => {
246
264
 
247
265
  it('sets the story option', async () => {
248
266
  const response = await handleTriage(context, {
267
+ pjcode: 'umino',
249
268
  action: 'set_story',
250
269
  issueUrl: 'https://github.com/o/r/issues/1',
251
270
  projectItemId: 'PVTI_e',
@@ -262,6 +281,7 @@ describe('consoleOperationApi', () => {
262
281
 
263
282
  it('snoozes for one day via updateNextActionDate', async () => {
264
283
  const response = await handleTriage(context, {
284
+ pjcode: 'umino',
265
285
  action: 'snooze_1day',
266
286
  issueUrl: 'https://github.com/o/r/issues/1',
267
287
  projectItemId: 'PVTI_f',
@@ -277,6 +297,7 @@ describe('consoleOperationApi', () => {
277
297
 
278
298
  it('snoozes for one week via updateNextActionDate', async () => {
279
299
  const response = await handleTriage(context, {
300
+ pjcode: 'umino',
280
301
  action: 'snooze_1week',
281
302
  issueUrl: 'https://github.com/o/r/issues/1',
282
303
  projectItemId: 'PVTI_g',
@@ -286,16 +307,18 @@ describe('consoleOperationApi', () => {
286
307
  expectRecordedAcrossTabs('PVTI_g');
287
308
  });
288
309
 
289
- it('closes via the triage close action', async () => {
310
+ it('closes an issue as completed via the triage close action', async () => {
290
311
  const response = await handleTriage(context, {
312
+ pjcode: 'umino',
291
313
  action: 'close',
292
314
  issueUrl: 'https://github.com/o/r/issues/1',
293
315
  projectItemId: 'PVTI_h',
294
316
  commentBody: 'duplicate',
295
317
  });
296
318
  expect(response.statusCode).toBe(200);
297
- expect(issueRepository.closePullRequest).toHaveBeenCalledWith(
319
+ expect(issueRepository.closeIssueByUrl).toHaveBeenCalledWith(
298
320
  'https://github.com/o/r/issues/1',
321
+ 'completed',
299
322
  );
300
323
  expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
301
324
  'https://github.com/o/r/issues/1',
@@ -304,8 +327,24 @@ describe('consoleOperationApi', () => {
304
327
  expectRecordedAcrossTabs('PVTI_h');
305
328
  });
306
329
 
330
+ it('closes an issue as not planned via the triage close_not_planned action', async () => {
331
+ const response = await handleTriage(context, {
332
+ pjcode: 'umino',
333
+ action: 'close_not_planned',
334
+ issueUrl: 'https://github.com/o/r/issues/2',
335
+ projectItemId: 'PVTI_np',
336
+ });
337
+ expect(response.statusCode).toBe(200);
338
+ expect(issueRepository.closeIssueByUrl).toHaveBeenCalledWith(
339
+ 'https://github.com/o/r/issues/2',
340
+ 'not_planned',
341
+ );
342
+ expectRecordedAcrossTabs('PVTI_np');
343
+ });
344
+
307
345
  it('rejects an unknown triage action', async () => {
308
346
  const response = await handleTriage(context, {
347
+ pjcode: 'umino',
309
348
  action: 'unknown',
310
349
  issueUrl: 'https://github.com/o/r/issues/1',
311
350
  projectItemId: 'PVTI_h',
@@ -315,6 +354,7 @@ describe('consoleOperationApi', () => {
315
354
 
316
355
  it('rejects set_status without a status name', async () => {
317
356
  const response = await handleTriage(context, {
357
+ pjcode: 'umino',
318
358
  action: 'set_status',
319
359
  issueUrl: 'https://github.com/o/r/issues/1',
320
360
  projectItemId: 'PVTI_h',
@@ -324,6 +364,7 @@ describe('consoleOperationApi', () => {
324
364
 
325
365
  it('rejects set_story without a story option id', async () => {
326
366
  const response = await handleTriage(context, {
367
+ pjcode: 'umino',
327
368
  action: 'set_story',
328
369
  issueUrl: 'https://github.com/o/r/issues/1',
329
370
  projectItemId: 'PVTI_h',
@@ -332,11 +373,12 @@ describe('consoleOperationApi', () => {
332
373
  });
333
374
 
334
375
  it('rejects set_story when the project has no story field', async () => {
335
- const contextWithoutStory: ConsoleOperationContext = {
336
- ...context,
337
- project: { ...project, story: null },
338
- };
376
+ const contextWithoutStory = contextForProject({
377
+ ...project,
378
+ story: null,
379
+ });
339
380
  const response = await handleTriage(contextWithoutStory, {
381
+ pjcode: 'umino',
340
382
  action: 'set_story',
341
383
  issueUrl: 'https://github.com/o/r/issues/1',
342
384
  projectItemId: 'PVTI_h',
@@ -348,6 +390,7 @@ describe('consoleOperationApi', () => {
348
390
 
349
391
  it('rejects a missing issueUrl', async () => {
350
392
  const response = await handleTriage(context, {
393
+ pjcode: 'umino',
351
394
  action: 'set_status',
352
395
  projectItemId: 'PVTI_h',
353
396
  statusName: 'Todo',
@@ -358,6 +401,7 @@ describe('consoleOperationApi', () => {
358
401
  it('returns 400 when the issue cannot be loaded for set_story', async () => {
359
402
  issueRepository.get.mockResolvedValue(null);
360
403
  const response = await handleTriage(context, {
404
+ pjcode: 'umino',
361
405
  action: 'set_story',
362
406
  issueUrl: 'https://github.com/o/r/issues/1',
363
407
  projectItemId: 'PVTI_h',
@@ -370,6 +414,7 @@ describe('consoleOperationApi', () => {
370
414
  describe('handleIntmux', () => {
371
415
  it('sets the In Tmux by human status and records done', async () => {
372
416
  const response = await handleIntmux(context, {
417
+ pjcode: 'umino',
373
418
  action: 'set_intmux',
374
419
  issueUrl: 'https://github.com/o/r/issues/1',
375
420
  projectItemId: 'PVTI_i',
@@ -385,6 +430,7 @@ describe('consoleOperationApi', () => {
385
430
 
386
431
  it('rejects an unknown intmux action', async () => {
387
432
  const response = await handleIntmux(context, {
433
+ pjcode: 'umino',
388
434
  action: 'unset_intmux',
389
435
  issueUrl: 'https://github.com/o/r/issues/1',
390
436
  projectItemId: 'PVTI_i',
@@ -395,6 +441,7 @@ describe('consoleOperationApi', () => {
395
441
  it('returns 400 when the issue cannot be loaded', async () => {
396
442
  issueRepository.get.mockResolvedValue(null);
397
443
  const response = await handleIntmux(context, {
444
+ pjcode: 'umino',
398
445
  action: 'set_intmux',
399
446
  issueUrl: 'https://github.com/o/r/issues/1',
400
447
  projectItemId: 'PVTI_i',
@@ -404,6 +451,7 @@ describe('consoleOperationApi', () => {
404
451
 
405
452
  it('rejects a missing issueUrl', async () => {
406
453
  const response = await handleIntmux(context, {
454
+ pjcode: 'umino',
407
455
  action: 'set_intmux',
408
456
  projectItemId: 'PVTI_i',
409
457
  });
@@ -412,6 +460,7 @@ describe('consoleOperationApi', () => {
412
460
 
413
461
  it('rejects a missing projectItemId', async () => {
414
462
  const response = await handleIntmux(context, {
463
+ pjcode: 'umino',
415
464
  action: 'set_intmux',
416
465
  issueUrl: 'https://github.com/o/r/issues/1',
417
466
  });
@@ -420,6 +469,7 @@ describe('consoleOperationApi', () => {
420
469
 
421
470
  it('rejects a missing action', async () => {
422
471
  const response = await handleIntmux(context, {
472
+ pjcode: 'umino',
423
473
  issueUrl: 'https://github.com/o/r/issues/1',
424
474
  projectItemId: 'PVTI_i',
425
475
  });
@@ -427,6 +477,69 @@ describe('consoleOperationApi', () => {
427
477
  });
428
478
  });
429
479
 
480
+ describe('per-project resolution', () => {
481
+ it('rejects an operation whose body has no pjcode', async () => {
482
+ const response = await handleTriage(context, {
483
+ action: 'set_status',
484
+ issueUrl: 'https://github.com/o/r/issues/1',
485
+ projectItemId: 'PVTI_k',
486
+ statusName: 'Todo',
487
+ });
488
+ expect(response.statusCode).toBe(400);
489
+ expect(issueRepository.updateStatus).not.toHaveBeenCalled();
490
+ });
491
+
492
+ it('rejects an operation whose pjcode has no configured project', async () => {
493
+ const response = await handleTriage(context, {
494
+ pjcode: 'unknown-project',
495
+ action: 'set_status',
496
+ issueUrl: 'https://github.com/o/r/issues/1',
497
+ projectItemId: 'PVTI_k',
498
+ statusName: 'Todo',
499
+ });
500
+ expect(response.statusCode).toBe(400);
501
+ expect(issueRepository.updateStatus).not.toHaveBeenCalled();
502
+ });
503
+
504
+ it('records the .done exclusion under the resolved pjcode', async () => {
505
+ const otherProject: Project = { ...project, id: 'PVT_other' };
506
+ const multiContext: ConsoleOperationContext = {
507
+ issueRepository,
508
+ resolveProject: async (pjcode: string) => {
509
+ if (pjcode === 'umino') {
510
+ return { pjcode, project };
511
+ }
512
+ if (pjcode === 'xmile') {
513
+ return { pjcode, project: otherProject };
514
+ }
515
+ return null;
516
+ },
517
+ consoleDataOutputDir: baseDir,
518
+ };
519
+ const response = await handleTriage(multiContext, {
520
+ pjcode: 'xmile',
521
+ action: 'set_status',
522
+ issueUrl: 'https://github.com/o/r/issues/1',
523
+ projectItemId: 'PVTI_x',
524
+ statusName: 'Todo',
525
+ });
526
+ expect(response.statusCode).toBe(200);
527
+ expect(issueRepository.updateStatus).toHaveBeenCalledWith(
528
+ otherProject,
529
+ { ...issue, itemId: 'PVTI_x' },
530
+ 'status_todo',
531
+ );
532
+ for (const tab of CONSOLE_DONE_TAB_NAMES) {
533
+ expect(readDoneProjectItemIds(baseDir, 'xmile', tab)).toContain(
534
+ 'PVTI_x',
535
+ );
536
+ expect(readDoneProjectItemIds(baseDir, 'umino', tab)).not.toContain(
537
+ 'PVTI_x',
538
+ );
539
+ }
540
+ });
541
+ });
542
+
430
543
  describe('done recording skips when storage is not configured', () => {
431
544
  it('does not throw when consoleDataOutputDir is null', async () => {
432
545
  const noStorageContext: ConsoleOperationContext = {
@@ -434,6 +547,7 @@ describe('consoleOperationApi', () => {
434
547
  consoleDataOutputDir: null,
435
548
  };
436
549
  const response = await handleIntmux(noStorageContext, {
550
+ pjcode: 'umino',
437
551
  action: 'set_intmux',
438
552
  issueUrl: 'https://github.com/o/r/issues/1',
439
553
  projectItemId: 'PVTI_j',
@@ -6,11 +6,19 @@ import { recordDoneProjectItemIdAcrossTabs } from './consoleDoneStore';
6
6
  export const AWAITING_WORKSPACE_STATUS_NAME = 'awaiting workspace';
7
7
  export const IN_TMUX_BY_HUMAN_STATUS_NAME = 'in tmux by human';
8
8
 
9
+ export type ConsoleProjectBinding = {
10
+ pjcode: string;
11
+ project: Project;
12
+ };
13
+
14
+ export type ConsoleProjectResolver = (
15
+ pjcode: string,
16
+ ) => Promise<ConsoleProjectBinding | null>;
17
+
9
18
  export type ConsoleOperationContext = {
10
19
  issueRepository: IssueRepository;
11
- project: Project;
20
+ resolveProject: ConsoleProjectResolver;
12
21
  consoleDataOutputDir: string | null;
13
- pjcode: string | null;
14
22
  };
15
23
 
16
24
  export type ConsoleOperationResponse = {
@@ -43,11 +51,12 @@ const resolveStatusId = (
43
51
  };
44
52
 
45
53
  const loadIssueWithProjectItemId = async (
46
- context: ConsoleOperationContext,
54
+ issueRepository: IssueRepository,
55
+ project: Project,
47
56
  issueUrl: string,
48
57
  projectItemId: string,
49
58
  ): Promise<Issue | null> => {
50
- const issue = await context.issueRepository.get(issueUrl, context.project);
59
+ const issue = await issueRepository.get(issueUrl, project);
51
60
  if (issue === null) {
52
61
  return null;
53
62
  }
@@ -56,37 +65,60 @@ const loadIssueWithProjectItemId = async (
56
65
 
57
66
  const recordDone = (
58
67
  context: ConsoleOperationContext,
68
+ pjcode: string,
59
69
  projectItemId: string,
60
70
  ): void => {
61
- if (context.consoleDataOutputDir === null || context.pjcode === null) {
71
+ if (context.consoleDataOutputDir === null) {
62
72
  return;
63
73
  }
64
74
  recordDoneProjectItemIdAcrossTabs(
65
75
  context.consoleDataOutputDir,
66
- context.pjcode,
76
+ pjcode,
67
77
  projectItemId,
68
78
  );
69
79
  };
70
80
 
71
- const updateStatusByName = async (
81
+ const resolveBinding = async (
72
82
  context: ConsoleOperationContext,
83
+ body: Record<string, unknown>,
84
+ ): Promise<ConsoleProjectBinding | ConsoleOperationResponse> => {
85
+ const pjcode = body.pjcode;
86
+ if (!isNonEmptyString(pjcode)) {
87
+ return badRequest('pjcode is required');
88
+ }
89
+ const binding = await context.resolveProject(pjcode);
90
+ if (binding === null) {
91
+ return badRequest(`no project configured for pjcode "${pjcode}"`);
92
+ }
93
+ return binding;
94
+ };
95
+
96
+ const isOperationResponse = (
97
+ value: ConsoleProjectBinding | ConsoleOperationResponse,
98
+ ): value is ConsoleOperationResponse =>
99
+ Object.prototype.hasOwnProperty.call(value, 'statusCode');
100
+
101
+ const updateStatusByName = async (
102
+ issueRepository: IssueRepository,
103
+ project: Project,
73
104
  issueUrl: string,
74
105
  projectItemId: string,
75
106
  statusName: string,
76
107
  ): Promise<ConsoleOperationResponse | null> => {
77
- const statusId = resolveStatusId(context.project, statusName);
108
+ const statusId = resolveStatusId(project, statusName);
78
109
  if (statusId === null) {
79
110
  return badRequest(`status option "${statusName}" not found in project`);
80
111
  }
81
112
  const issue = await loadIssueWithProjectItemId(
82
- context,
113
+ issueRepository,
114
+ project,
83
115
  issueUrl,
84
116
  projectItemId,
85
117
  );
86
118
  if (issue === null) {
87
119
  return badRequest('issue not found');
88
120
  }
89
- await context.issueRepository.updateStatus(context.project, issue, statusId);
121
+ await issueRepository.updateStatus(project, issue, statusId);
90
122
  return null;
91
123
  };
92
124
 
@@ -106,11 +138,17 @@ export const handleReview = async (
106
138
  if (!isNonEmptyString(projectItemId)) {
107
139
  return badRequest('projectItemId is required');
108
140
  }
141
+ const binding = await resolveBinding(context, body);
142
+ if (isOperationResponse(binding)) {
143
+ return binding;
144
+ }
145
+ const { project, pjcode } = binding;
109
146
 
110
147
  if (action === 'approve') {
111
148
  await context.issueRepository.approvePullRequest(prUrl);
112
149
  const failure = await updateStatusByName(
113
- context,
150
+ context.issueRepository,
151
+ project,
114
152
  prUrl,
115
153
  projectItemId,
116
154
  AWAITING_WORKSPACE_STATUS_NAME,
@@ -118,7 +156,7 @@ export const handleReview = async (
118
156
  if (failure !== null) {
119
157
  return failure;
120
158
  }
121
- recordDone(context, projectItemId);
159
+ recordDone(context, pjcode, projectItemId);
122
160
  return ok();
123
161
  }
124
162
 
@@ -136,7 +174,8 @@ export const handleReview = async (
136
174
  commentBody,
137
175
  );
138
176
  const failure = await updateStatusByName(
139
- context,
177
+ context.issueRepository,
178
+ project,
140
179
  prUrl,
141
180
  projectItemId,
142
181
  AWAITING_WORKSPACE_STATUS_NAME,
@@ -144,7 +183,7 @@ export const handleReview = async (
144
183
  if (failure !== null) {
145
184
  return failure;
146
185
  }
147
- recordDone(context, projectItemId);
186
+ recordDone(context, pjcode, projectItemId);
148
187
  return ok();
149
188
  }
150
189
 
@@ -153,7 +192,7 @@ export const handleReview = async (
153
192
  if (isNonEmptyString(body.commentBody)) {
154
193
  await context.issueRepository.createCommentByUrl(prUrl, body.commentBody);
155
194
  }
156
- recordDone(context, projectItemId);
195
+ recordDone(context, pjcode, projectItemId);
157
196
  return ok();
158
197
  }
159
198
 
@@ -176,6 +215,11 @@ export const handleTriage = async (
176
215
  if (!isNonEmptyString(projectItemId)) {
177
216
  return badRequest('projectItemId is required');
178
217
  }
218
+ const binding = await resolveBinding(context, body);
219
+ if (isOperationResponse(binding)) {
220
+ return binding;
221
+ }
222
+ const { project, pjcode } = binding;
179
223
 
180
224
  if (action === 'set_status') {
181
225
  const statusName = body.statusName;
@@ -183,7 +227,8 @@ export const handleTriage = async (
183
227
  return badRequest('statusName is required for set_status');
184
228
  }
185
229
  const failure = await updateStatusByName(
186
- context,
230
+ context.issueRepository,
231
+ project,
187
232
  issueUrl,
188
233
  projectItemId,
189
234
  statusName,
@@ -191,7 +236,7 @@ export const handleTriage = async (
191
236
  if (failure !== null) {
192
237
  return failure;
193
238
  }
194
- recordDone(context, projectItemId);
239
+ recordDone(context, pjcode, projectItemId);
195
240
  return ok();
196
241
  }
197
242
 
@@ -200,11 +245,12 @@ export const handleTriage = async (
200
245
  if (!isNonEmptyString(storyOptionId)) {
201
246
  return badRequest('storyOptionId is required for set_story');
202
247
  }
203
- if (context.project.story === null) {
248
+ if (project.story === null) {
204
249
  return badRequest('project does not have a story field');
205
250
  }
206
251
  const issue = await loadIssueWithProjectItemId(
207
- context,
252
+ context.issueRepository,
253
+ project,
208
254
  issueUrl,
209
255
  projectItemId,
210
256
  );
@@ -212,23 +258,26 @@ export const handleTriage = async (
212
258
  return badRequest('issue not found');
213
259
  }
214
260
  await context.issueRepository.updateStory(
215
- { ...context.project, story: context.project.story },
261
+ { ...project, story: project.story },
216
262
  issue,
217
263
  storyOptionId,
218
264
  );
219
- recordDone(context, projectItemId);
265
+ recordDone(context, pjcode, projectItemId);
220
266
  return ok();
221
267
  }
222
268
 
223
- if (action === 'close') {
224
- await context.issueRepository.closePullRequest(issueUrl);
269
+ if (action === 'close' || action === 'close_not_planned') {
225
270
  if (isNonEmptyString(body.commentBody)) {
226
271
  await context.issueRepository.createCommentByUrl(
227
272
  issueUrl,
228
273
  body.commentBody,
229
274
  );
230
275
  }
231
- recordDone(context, projectItemId);
276
+ await context.issueRepository.closeIssueByUrl(
277
+ issueUrl,
278
+ action === 'close_not_planned' ? 'not_planned' : 'completed',
279
+ );
280
+ recordDone(context, pjcode, projectItemId);
232
281
  return ok();
233
282
  }
234
283
 
@@ -237,10 +286,10 @@ export const handleTriage = async (
237
286
  const target = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
238
287
  await context.issueRepository.updateNextActionDate(
239
288
  issueUrl,
240
- context.project,
289
+ project,
241
290
  target,
242
291
  );
243
- recordDone(context, projectItemId);
292
+ recordDone(context, pjcode, projectItemId);
244
293
  return ok();
245
294
  }
246
295
 
@@ -266,8 +315,14 @@ export const handleIntmux = async (
266
315
  if (!isNonEmptyString(projectItemId)) {
267
316
  return badRequest('projectItemId is required');
268
317
  }
318
+ const binding = await resolveBinding(context, body);
319
+ if (isOperationResponse(binding)) {
320
+ return binding;
321
+ }
322
+ const { project, pjcode } = binding;
269
323
  const failure = await updateStatusByName(
270
- context,
324
+ context.issueRepository,
325
+ project,
271
326
  issueUrl,
272
327
  projectItemId,
273
328
  IN_TMUX_BY_HUMAN_STATUS_NAME,
@@ -275,6 +330,6 @@ export const handleIntmux = async (
275
330
  if (failure !== null) {
276
331
  return failure;
277
332
  }
278
- recordDone(context, projectItemId);
333
+ recordDone(context, pjcode, projectItemId);
279
334
  return ok();
280
335
  };