github-issue-tower-defence-management 1.185.1 → 1.186.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 (144) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/cli/index.js +2 -1
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/console/consoleOperationApi.js +43 -1
  6. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleProjectResolver.js +7 -1
  8. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
  9. package/bin/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  10. package/bin/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  11. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  12. package/bin/adapter/entry-points/console/webServer.js +3 -0
  13. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  14. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +5 -1
  15. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  16. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js +1 -0
  17. package/bin/adapter/entry-points/handlers/tokenStatusWriter.js.map +1 -1
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +36 -0
  19. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  20. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +12 -0
  21. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  22. package/bin/domain/entities/Project.js +11 -1
  23. package/bin/domain/entities/Project.js.map +1 -1
  24. package/bin/domain/entities/RequiredProjectField.js +3 -2
  25. package/bin/domain/entities/RequiredProjectField.js.map +1 -1
  26. package/bin/domain/usecases/HandleScheduledEventUseCase.js +12 -1
  27. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  28. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js +36 -0
  29. package/bin/domain/usecases/IssueNoStatusUpdateUseCase.js.map +1 -0
  30. package/bin/domain/usecases/IssueRejectionEvaluator.js +19 -0
  31. package/bin/domain/usecases/IssueRejectionEvaluator.js.map +1 -1
  32. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +1 -0
  33. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -1
  34. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js +36 -0
  35. package/bin/domain/usecases/ReopenedDoneIssueRevertUseCase.js.map +1 -0
  36. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js +2 -5
  37. package/bin/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.js.map +1 -1
  38. package/bin/domain/usecases/StartPreparationUseCase.js +5 -2
  39. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/adapter/entry-points/cli/index.ts +14 -10
  42. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +199 -0
  43. package/src/adapter/entry-points/console/consoleOperationApi.ts +65 -0
  44. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +26 -0
  45. package/src/adapter/entry-points/console/consoleProjectResolver.ts +8 -1
  46. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +1 -0
  47. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +40 -0
  48. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +18 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.stories.tsx +4 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.test.tsx +114 -141
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/list/ConsoleStoryList.tsx +84 -3
  52. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +23 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +73 -0
  54. package/src/adapter/entry-points/console/ui/src/index.css +35 -0
  55. package/src/adapter/entry-points/console/ui-dist/assets/index-wj6o8qvo.js +86 -0
  56. package/src/adapter/entry-points/console/ui-dist/assets/index-yZy1TUOo.css +2 -0
  57. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  58. package/src/adapter/entry-points/console/webServer.ts +7 -0
  59. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +11 -0
  60. package/src/adapter/entry-points/handlers/InTmuxByHumanSessionTokenCountHandler.test.ts +1 -0
  61. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +1 -0
  62. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +1 -0
  63. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  64. package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.test.ts +1 -0
  65. package/src/adapter/entry-points/handlers/ownerCallFileCleaner.test.ts +1 -0
  66. package/src/adapter/entry-points/handlers/situationFileWriter.test.ts +1 -0
  67. package/src/adapter/entry-points/handlers/staleTmuxSessionCleaner.test.ts +1 -0
  68. package/src/adapter/entry-points/handlers/tokenStatusWriter.test.ts +1 -0
  69. package/src/adapter/entry-points/handlers/tokenStatusWriter.ts +1 -0
  70. package/src/adapter/repositories/GitHubIssueCommentRepository.test.ts +1 -0
  71. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +90 -0
  72. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +48 -2
  73. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +107 -0
  74. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +20 -0
  75. package/src/adapter/repositories/issue/RestIssueRepository.test.ts +1 -0
  76. package/src/domain/entities/Issue.ts +1 -0
  77. package/src/domain/entities/Project.ts +11 -2
  78. package/src/domain/entities/RequiredProjectField.ts +3 -1
  79. package/src/domain/usecases/AgentDesignationLabelAdoptUseCase.test.ts +1 -0
  80. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +1 -0
  81. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  82. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +144 -0
  83. package/src/domain/usecases/HandleScheduledEventUseCase.ts +16 -0
  84. package/src/domain/usecases/InTmuxByHumanSessionTokenCountUseCase.test.ts +1 -0
  85. package/src/domain/usecases/IssueNoStatusUpdateUseCase.test.ts +165 -0
  86. package/src/domain/usecases/IssueNoStatusUpdateUseCase.ts +44 -0
  87. package/src/domain/usecases/IssueRejectionEvaluator.test.ts +120 -0
  88. package/src/domain/usecases/IssueRejectionEvaluator.ts +21 -0
  89. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +1 -0
  90. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +1 -0
  91. package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +34 -0
  92. package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +1 -0
  93. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.test.ts +188 -0
  94. package/src/domain/usecases/ReopenedDoneIssueRevertUseCase.ts +57 -0
  95. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.test.ts +44 -0
  96. package/src/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.ts +4 -12
  97. package/src/domain/usecases/RevertOrphanedPreparationUseCase.test.ts +1 -0
  98. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +1 -0
  99. package/src/domain/usecases/StaleTmuxSessionKillUseCase.test.ts +1 -0
  100. package/src/domain/usecases/StartPreparationUseCase.test.ts +111 -1
  101. package/src/domain/usecases/StartPreparationUseCase.ts +6 -2
  102. package/src/domain/usecases/TriagerApprovalDispatchUseCase.test.ts +1 -0
  103. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +5 -0
  104. package/src/domain/usecases/console/GenerateConsoleListsUseCase.test.ts +1 -0
  105. package/src/domain/usecases/dashboard/GenerateDashboardRowUseCase.test.ts +1 -0
  106. package/src/domain/usecases/intmux/GenerateInTmuxByHumanDataUseCase.test.ts +1 -0
  107. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  108. package/src/domain/usecases/issueReactivationTriggerIsPending.test.ts +1 -0
  109. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  110. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
  111. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  112. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +1 -0
  113. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
  114. package/types/adapter/entry-points/console/webServer.d.ts +2 -0
  115. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  116. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  117. package/types/adapter/entry-points/handlers/tokenStatusWriter.d.ts.map +1 -1
  118. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
  119. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  120. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  121. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  122. package/types/domain/entities/Issue.d.ts +1 -0
  123. package/types/domain/entities/Issue.d.ts.map +1 -1
  124. package/types/domain/entities/Project.d.ts +2 -1
  125. package/types/domain/entities/Project.d.ts.map +1 -1
  126. package/types/domain/entities/RequiredProjectField.d.ts +1 -0
  127. package/types/domain/entities/RequiredProjectField.d.ts.map +1 -1
  128. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +6 -1
  129. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  130. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts +12 -0
  131. package/types/domain/usecases/IssueNoStatusUpdateUseCase.d.ts.map +1 -0
  132. package/types/domain/usecases/IssueRejectionEvaluator.d.ts +1 -0
  133. package/types/domain/usecases/IssueRejectionEvaluator.d.ts.map +1 -1
  134. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -1
  135. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts +12 -0
  136. package/types/domain/usecases/ReopenedDoneIssueRevertUseCase.d.ts.map +1 -0
  137. package/types/domain/usecases/RevertNotReadyReviewQueueIssueUseCase.d.ts.map +1 -1
  138. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
  139. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +3 -0
  140. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  141. package/bin/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  142. package/bin/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
  143. package/src/adapter/entry-points/console/ui-dist/assets/index-8Ni5iu-C.css +0 -2
  144. package/src/adapter/entry-points/console/ui-dist/assets/index-DQT4HL0G.js +0 -86
@@ -22,6 +22,7 @@ import {
22
22
  handleReview,
23
23
  handleReviewComment,
24
24
  handleStoryAdd,
25
+ handleStoryColor,
25
26
  handleTriage,
26
27
  } from './consoleOperationApi';
27
28
 
@@ -115,6 +116,7 @@ describe('consoleOperationApi', () => {
115
116
  updateStoryList: null,
116
117
  resolveProjectRepository: null,
117
118
  invalidateProject: null,
119
+ updateProjectCacheEntry: null,
118
120
  };
119
121
  });
120
122
 
@@ -130,6 +132,7 @@ describe('consoleOperationApi', () => {
130
132
  updateStoryList: null,
131
133
  resolveProjectRepository: null,
132
134
  invalidateProject: null,
135
+ updateProjectCacheEntry: null,
133
136
  });
134
137
 
135
138
  afterEach(() => {
@@ -875,6 +878,7 @@ describe('consoleOperationApi', () => {
875
878
  updateStoryList: null,
876
879
  resolveProjectRepository: null,
877
880
  invalidateProject: null,
881
+ updateProjectCacheEntry: null,
878
882
  };
879
883
  });
880
884
 
@@ -1123,6 +1127,7 @@ describe('consoleOperationApi', () => {
1123
1127
  updateStoryList: null,
1124
1128
  resolveProjectRepository: null,
1125
1129
  invalidateProject: null,
1130
+ updateProjectCacheEntry: null,
1126
1131
  };
1127
1132
  const response = await handleTriage(multiContext, {
1128
1133
  pjcode: 'globex',
@@ -2114,6 +2119,7 @@ describe('consoleOperationApi', () => {
2114
2119
  updateStoryList: consecutiveUpdateStoryList,
2115
2120
  })),
2116
2121
  invalidateProject,
2122
+ updateProjectCacheEntry: null,
2117
2123
  };
2118
2124
 
2119
2125
  await handleStoryAdd(consecutiveContext, {
@@ -2137,4 +2143,197 @@ describe('consoleOperationApi', () => {
2137
2143
  );
2138
2144
  });
2139
2145
  });
2146
+
2147
+ describe('handleStoryColor', () => {
2148
+ const projectWithStory = (): Project => ({
2149
+ ...project,
2150
+ story: {
2151
+ name: 'Story',
2152
+ fieldId: 'storyField',
2153
+ databaseId: 1,
2154
+ stories: [
2155
+ {
2156
+ id: 'opt_blue',
2157
+ name: 'Portal redesign',
2158
+ color: 'BLUE',
2159
+ description: '',
2160
+ },
2161
+ {
2162
+ id: 'opt_green',
2163
+ name: 'Move to Okinawa',
2164
+ color: 'GREEN',
2165
+ description: '',
2166
+ },
2167
+ ],
2168
+ workflowManagementStory: { id: 'wms', name: 'workflow' },
2169
+ },
2170
+ });
2171
+
2172
+ beforeEach(() => {
2173
+ issueRepository.updateStoryOptionColor.mockResolvedValue(undefined);
2174
+ });
2175
+
2176
+ it('calls updateStoryOptionColor with the correct project, storyOptionId, and newColor', async () => {
2177
+ const storyProject = projectWithStory();
2178
+ const response = await handleStoryColor(contextForProject(storyProject), {
2179
+ pjcode: 'acme',
2180
+ storyOptionId: 'opt_blue',
2181
+ newColor: 'RED',
2182
+ nameWithOwner: 'acme-labs/portal',
2183
+ });
2184
+
2185
+ expect(response.statusCode).toBe(200);
2186
+ expect(issueRepository.updateStoryOptionColor).toHaveBeenCalledWith(
2187
+ expect.objectContaining({ story: storyProject.story }),
2188
+ 'opt_blue',
2189
+ 'RED',
2190
+ );
2191
+ });
2192
+
2193
+ it('resolves the issue repository from a proxy url of the target repo', async () => {
2194
+ const resolvedUrls: string[] = [];
2195
+ const recordingContext: ConsoleOperationContext = {
2196
+ ...contextForProject(projectWithStory()),
2197
+ resolveIssueRepository: (url: string) => {
2198
+ resolvedUrls.push(url);
2199
+ return issueRepository;
2200
+ },
2201
+ };
2202
+
2203
+ await handleStoryColor(recordingContext, {
2204
+ pjcode: 'acme',
2205
+ storyOptionId: 'opt_blue',
2206
+ newColor: 'GREEN',
2207
+ nameWithOwner: 'acme-labs/portal',
2208
+ });
2209
+
2210
+ expect(resolvedUrls).toContain(
2211
+ 'https://github.com/acme-labs/portal/issues/0',
2212
+ );
2213
+ });
2214
+
2215
+ it('rejects when storyOptionId is missing', async () => {
2216
+ const response = await handleStoryColor(
2217
+ contextForProject(projectWithStory()),
2218
+ {
2219
+ pjcode: 'acme',
2220
+ newColor: 'RED',
2221
+ nameWithOwner: 'acme-labs/portal',
2222
+ },
2223
+ );
2224
+ expect(response).toEqual({
2225
+ statusCode: 400,
2226
+ body: { error: 'storyOptionId is required' },
2227
+ });
2228
+ });
2229
+
2230
+ it('rejects when newColor is not a valid ConsoleColor', async () => {
2231
+ const response = await handleStoryColor(
2232
+ contextForProject(projectWithStory()),
2233
+ {
2234
+ pjcode: 'acme',
2235
+ storyOptionId: 'opt_blue',
2236
+ newColor: 'MAGENTA',
2237
+ nameWithOwner: 'acme-labs/portal',
2238
+ },
2239
+ );
2240
+ expect(response).toEqual({
2241
+ statusCode: 400,
2242
+ body: {
2243
+ error:
2244
+ 'newColor must be one of GRAY, BLUE, GREEN, YELLOW, ORANGE, RED, PINK, PURPLE',
2245
+ },
2246
+ });
2247
+ });
2248
+
2249
+ it('rejects when nameWithOwner is missing', async () => {
2250
+ const response = await handleStoryColor(
2251
+ contextForProject(projectWithStory()),
2252
+ {
2253
+ pjcode: 'acme',
2254
+ storyOptionId: 'opt_blue',
2255
+ newColor: 'RED',
2256
+ },
2257
+ );
2258
+ expect(response).toEqual({
2259
+ statusCode: 400,
2260
+ body: { error: 'nameWithOwner is required' },
2261
+ });
2262
+ });
2263
+
2264
+ it('rejects when the project has no story field', async () => {
2265
+ const projectWithoutStory: Project = { ...project, story: null };
2266
+
2267
+ const response = await handleStoryColor(
2268
+ contextForProject(projectWithoutStory),
2269
+ {
2270
+ pjcode: 'acme',
2271
+ storyOptionId: 'opt_blue',
2272
+ newColor: 'RED',
2273
+ nameWithOwner: 'acme-labs/portal',
2274
+ },
2275
+ );
2276
+ expect(response).toEqual({
2277
+ statusCode: 400,
2278
+ body: { error: 'project does not have a story field' },
2279
+ });
2280
+ });
2281
+
2282
+ it('rejects when storyOptionId is not found in project.story.stories', async () => {
2283
+ const response = await handleStoryColor(
2284
+ contextForProject(projectWithStory()),
2285
+ {
2286
+ pjcode: 'acme',
2287
+ storyOptionId: 'opt_nonexistent',
2288
+ newColor: 'RED',
2289
+ nameWithOwner: 'acme-labs/portal',
2290
+ },
2291
+ );
2292
+ expect(response).toEqual({
2293
+ statusCode: 400,
2294
+ body: { error: 'story option "opt_nonexistent" not found in project' },
2295
+ });
2296
+ expect(issueRepository.updateStoryOptionColor).not.toHaveBeenCalled();
2297
+ });
2298
+
2299
+ it('calls updateProjectCacheEntry with the updated project after a successful color change', async () => {
2300
+ const storyProject = projectWithStory();
2301
+ const updatedEntries: { pjcode: string; project: Project }[] = [];
2302
+ const contextWithCache: ConsoleOperationContext = {
2303
+ ...contextForProject(storyProject),
2304
+ updateProjectCacheEntry: (pjcode, updatedProject) => {
2305
+ updatedEntries.push({ pjcode, project: updatedProject });
2306
+ },
2307
+ };
2308
+
2309
+ await handleStoryColor(contextWithCache, {
2310
+ pjcode: 'acme',
2311
+ storyOptionId: 'opt_blue',
2312
+ newColor: 'RED',
2313
+ nameWithOwner: 'acme-labs/portal',
2314
+ });
2315
+
2316
+ expect(updatedEntries).toHaveLength(1);
2317
+ expect(updatedEntries[0].pjcode).toBe('acme');
2318
+ expect(updatedEntries[0].project.story?.stories).toEqual(
2319
+ expect.arrayContaining([
2320
+ expect.objectContaining({ id: 'opt_blue', color: 'RED' }),
2321
+ expect.objectContaining({ id: 'opt_green', color: 'GREEN' }),
2322
+ ]),
2323
+ );
2324
+ });
2325
+
2326
+ it('does not call updateProjectCacheEntry when updateProjectCacheEntry is null', async () => {
2327
+ const response = await handleStoryColor(
2328
+ contextForProject(projectWithStory()),
2329
+ {
2330
+ pjcode: 'acme',
2331
+ storyOptionId: 'opt_blue',
2332
+ newColor: 'RED',
2333
+ nameWithOwner: 'acme-labs/portal',
2334
+ },
2335
+ );
2336
+ expect(response.statusCode).toBe(200);
2337
+ });
2338
+ });
2140
2339
  });
@@ -1,6 +1,7 @@
1
1
  import type { Issue } from '../../../domain/entities/Issue';
2
2
  import {
3
3
  buildStoryListWithNew,
4
+ FIELD_OPTION_COLORS,
4
5
  type FieldOption,
5
6
  type Project,
6
7
  } from '../../../domain/entities/Project';
@@ -52,6 +53,8 @@ export type ConsoleOperationContext = {
52
53
  | null;
53
54
  resolveProjectRepository: ConsoleProjectRepositoryResolver | null;
54
55
  invalidateProject: ((pjcode: string) => void) | null;
56
+ updateProjectCacheEntry:
57
+ ((pjcode: string, updatedProject: Project) => void) | null;
55
58
  };
56
59
 
57
60
  export type ConsoleOperationResponse = {
@@ -138,6 +141,7 @@ const projectItemReference = (
138
141
  author: '',
139
142
  closingIssueReferenceUrls: [],
140
143
  agent: null,
144
+ stateReason: null,
141
145
  });
142
146
 
143
147
  const recordDone = (
@@ -698,6 +702,67 @@ export const handleReviewComment = async (
698
702
  return ok();
699
703
  };
700
704
 
705
+ const isStoryColor = (value: unknown): value is FieldOption['color'] =>
706
+ FIELD_OPTION_COLORS.some((c) => c === value);
707
+
708
+ export const handleStoryColor = async (
709
+ context: ConsoleOperationContext,
710
+ body: Record<string, unknown>,
711
+ ): Promise<ConsoleOperationResponse> => {
712
+ const storyOptionId = body.storyOptionId;
713
+ const newColor = body.newColor;
714
+ const nameWithOwner = body.nameWithOwner;
715
+
716
+ if (!isNonEmptyString(storyOptionId)) {
717
+ return badRequest('storyOptionId is required');
718
+ }
719
+ if (!isStoryColor(newColor)) {
720
+ return badRequest(
721
+ 'newColor must be one of GRAY, BLUE, GREEN, YELLOW, ORANGE, RED, PINK, PURPLE',
722
+ );
723
+ }
724
+ if (!isNonEmptyString(nameWithOwner)) {
725
+ return badRequest('nameWithOwner is required');
726
+ }
727
+
728
+ const binding = await resolveBinding(context, body);
729
+ if (isOperationResponse(binding)) {
730
+ return binding;
731
+ }
732
+ const { project, pjcode } = binding;
733
+
734
+ if (project.story === null) {
735
+ return badRequest('project does not have a story field');
736
+ }
737
+
738
+ const storyOption = project.story.stories.find((s) => s.id === storyOptionId);
739
+ if (storyOption === undefined) {
740
+ return badRequest(`story option "${storyOptionId}" not found in project`);
741
+ }
742
+
743
+ const proxyUrl = `https://github.com/${nameWithOwner}/issues/0`;
744
+ await context
745
+ .resolveIssueRepository(proxyUrl)
746
+ .updateStoryOptionColor(
747
+ { ...project, story: project.story },
748
+ storyOptionId,
749
+ newColor,
750
+ );
751
+
752
+ if (context.updateProjectCacheEntry !== null) {
753
+ const updatedStories = project.story.stories.map((s) =>
754
+ s.id === storyOptionId ? { ...s, color: newColor } : s,
755
+ );
756
+ const updatedProject: Project = {
757
+ ...project,
758
+ story: { ...project.story, stories: updatedStories },
759
+ };
760
+ context.updateProjectCacheEntry(pjcode, updatedProject);
761
+ }
762
+
763
+ return ok();
764
+ };
765
+
701
766
  export const handleIntmux = async (
702
767
  context: ConsoleOperationContext,
703
768
  body: Record<string, unknown>,
@@ -127,6 +127,32 @@ describe('createConsoleProjectResolver', () => {
127
127
  });
128
128
  expect(loadProject).toHaveBeenCalledTimes(2);
129
129
  });
130
+
131
+ it('updateEntry replaces the cached project so the next resolve returns the updated project', async () => {
132
+ const updatedProject: Project = { ...acmeProject, id: 'PVT_acme_updated' };
133
+ const loadProject = jest.fn(async () => acmeProject);
134
+ const { resolve, updateEntry } = createConsoleProjectResolver(
135
+ { acme: 'https://github.com/orgs/acme/projects/1' },
136
+ loadProject,
137
+ );
138
+ await resolve('acme');
139
+ updateEntry('acme', updatedProject);
140
+ const result = await resolve('acme');
141
+ expect(result?.project).toBe(updatedProject);
142
+ expect(loadProject).toHaveBeenCalledTimes(1);
143
+ });
144
+
145
+ it('updateEntry is a no-op for a pjcode that was never resolved', async () => {
146
+ const updatedProject: Project = { ...acmeProject, id: 'PVT_acme_updated' };
147
+ const loadProject = jest.fn(async () => acmeProject);
148
+ const { resolve, updateEntry } = createConsoleProjectResolver(
149
+ { acme: 'https://github.com/orgs/acme/projects/1' },
150
+ loadProject,
151
+ );
152
+ updateEntry('acme', updatedProject);
153
+ const result = await resolve('acme');
154
+ expect(result?.project).toBe(acmeProject);
155
+ });
130
156
  });
131
157
 
132
158
  describe('createConsoleProjectLoader', () => {
@@ -71,6 +71,7 @@ export const createPjcodeConfigChecker = (
71
71
  export type ConsoleProjectResolverBundle = {
72
72
  resolve: ConsoleProjectResolver;
73
73
  invalidate: (pjcode: string) => void;
74
+ updateEntry: (pjcode: string, updatedProject: Project) => void;
74
75
  };
75
76
 
76
77
  export const createConsoleProjectResolver = (
@@ -100,5 +101,11 @@ export const createConsoleProjectResolver = (
100
101
  const invalidate = (pjcode: string): void => {
101
102
  cache.delete(pjcode);
102
103
  };
103
- return { resolve, invalidate };
104
+ const updateEntry = (pjcode: string, updatedProject: Project): void => {
105
+ const existing = cache.get(pjcode);
106
+ if (existing !== undefined) {
107
+ cache.set(pjcode, { ...existing, project: updatedProject });
108
+ }
109
+ };
110
+ return { resolve, invalidate, updateEntry };
104
111
  };
@@ -48,6 +48,7 @@ const makeIssue = (overrides: Partial<Issue>): Issue => {
48
48
  author: 'someone',
49
49
  closingIssueReferenceUrls: [],
50
50
  agent: null,
51
+ stateReason: null,
51
52
  ...overrides,
52
53
  };
53
54
  };
@@ -514,6 +514,46 @@ test('creates a new story when the add-story button and form are used', async ({
514
514
  ).toHaveCount(0);
515
515
  });
516
516
 
517
+ test('changes the color of a story row via the color palette in the stories tab', async ({
518
+ page,
519
+ }) => {
520
+ await page.goto(harness.appRootUrl);
521
+
522
+ await tabByLabel(page, 'Stories').click();
523
+
524
+ const tdpmRow = page.locator('.console-story-list-row', {
525
+ hasText: 'TDPM Console port',
526
+ });
527
+ await expect(tdpmRow).toBeVisible();
528
+
529
+ const changeColorButton = tdpmRow.locator('.console-op-button', {
530
+ hasText: 'Change color',
531
+ });
532
+ await expect(changeColorButton).toBeVisible();
533
+
534
+ await changeColorButton.click();
535
+
536
+ const palette = tdpmRow.locator('.console-story-color-palette');
537
+ await expect(palette).toBeVisible();
538
+
539
+ const swatches = palette.locator('.console-story-color-swatch');
540
+ await expect(swatches).toHaveCount(8);
541
+
542
+ const graySwatch = swatches.filter({ hasText: 'disable' });
543
+ await expect(graySwatch).toHaveCount(1);
544
+
545
+ const greenSwatch = palette.locator('[aria-label="GREEN"]');
546
+ await greenSwatch.click();
547
+
548
+ await expect
549
+ .poll(() => harness.storyColorCalls.length, { timeout: 10000 })
550
+ .toBe(1);
551
+ expect(harness.storyColorCalls[0].storyOptionId).toBe('1491051e');
552
+ expect(harness.storyColorCalls[0].newColor).toBe('GREEN');
553
+
554
+ await expect(palette).toHaveCount(0);
555
+ });
556
+
517
557
  test('reorders stories on the triage tab with up/down buttons', async ({
518
558
  page,
519
559
  }) => {
@@ -47,6 +47,11 @@ export type ConsoleE2eCreateIssueCall = {
47
47
  title: string;
48
48
  };
49
49
 
50
+ export type ConsoleE2eStoryColorCall = {
51
+ storyOptionId: string;
52
+ newColor: string;
53
+ };
54
+
50
55
  type ConsoleFixtureListItem = {
51
56
  number: number;
52
57
  title: string;
@@ -391,6 +396,7 @@ const buildIssueForUrl = (url: string): Issue => ({
391
396
  author: 'HiromiShikata',
392
397
  closingIssueReferenceUrls: [],
393
398
  agent: null,
399
+ stateReason: null,
394
400
  });
395
401
 
396
402
  const notImplemented = (method: string): never => {
@@ -453,6 +459,7 @@ const createStubIssueRepository = (
453
459
  reviewCommentCalls: ConsoleE2eReviewCommentCall[],
454
460
  requestChangesCalls: ConsoleE2eRequestChangesCall[],
455
461
  createIssueCalls: ConsoleE2eCreateIssueCall[],
462
+ storyColorCalls: ConsoleE2eStoryColorCall[],
456
463
  ): IssueRepository => ({
457
464
  getAllIssues: () => notImplemented('getAllIssues'),
458
465
  getIssueByUrl: async (url: string): Promise<Issue | null> =>
@@ -615,6 +622,13 @@ const createStubIssueRepository = (
615
622
  }
616
623
  : null,
617
624
  setIssueAgentField: async (): Promise<void> => undefined,
625
+ updateStoryOptionColor: async (
626
+ _project,
627
+ storyOptionId: string,
628
+ newColor: string,
629
+ ): Promise<void> => {
630
+ storyColorCalls.push({ storyOptionId, newColor });
631
+ },
618
632
  });
619
633
 
620
634
  export type ConsoleE2eReorderStoryCall = {
@@ -635,6 +649,7 @@ export type ConsoleE2eHarness = {
635
649
  createIssueCalls: ConsoleE2eCreateIssueCall[];
636
650
  reorderStoryCalls: ConsoleE2eReorderStoryCall[];
637
651
  addStoryCalls: ConsoleE2eAddStoryCall[];
652
+ storyColorCalls: ConsoleE2eStoryColorCall[];
638
653
  stop: () => Promise<void>;
639
654
  };
640
655
 
@@ -658,6 +673,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
658
673
  const createIssueCalls: ConsoleE2eCreateIssueCall[] = [];
659
674
  const reorderStoryCalls: ConsoleE2eReorderStoryCall[] = [];
660
675
  const addStoryCalls: ConsoleE2eAddStoryCall[] = [];
676
+ const storyColorCalls: ConsoleE2eStoryColorCall[] = [];
661
677
 
662
678
  const server = await startWebServer({
663
679
  accessToken: CONSOLE_E2E_TOKEN,
@@ -667,6 +683,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
667
683
  reviewCommentCalls,
668
684
  requestChangesCalls,
669
685
  createIssueCalls,
686
+ storyColorCalls,
670
687
  ),
671
688
  projectRepository: {
672
689
  updateStoryList: async (_updatedProject, stories) => {
@@ -726,6 +743,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
726
743
  createIssueCalls,
727
744
  reorderStoryCalls,
728
745
  addStoryCalls,
746
+ storyColorCalls,
729
747
  stop: async (): Promise<void> => {
730
748
  await closeServer(server);
731
749
  fs.rmSync(tmpRoot, { recursive: true, force: true });
@@ -53,6 +53,10 @@ const meta: Meta<typeof ConsoleStoryList> = {
53
53
  args: {
54
54
  onCreateIssue: () => Promise.resolve(),
55
55
  onAddStory: () => Promise.resolve(),
56
+ onSelectColor: () => undefined,
57
+ optimisticColors: {},
58
+ colorChangeInFlight: null,
59
+ colorErrors: {},
56
60
  },
57
61
  };
58
62