github-issue-tower-defence-management 1.146.0 → 1.146.2

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 (111) hide show
  1. package/CHANGELOG.md +23 -9
  2. package/README.md +1 -1
  3. package/bin/adapter/entry-points/cli/index.js +29 -7
  4. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  5. package/bin/adapter/entry-points/cli/projectConfig.js +3 -0
  6. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  7. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +41 -0
  8. package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -0
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js +34 -16
  10. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  11. package/bin/adapter/entry-points/console/ensureConsoleRunning.js +6 -1
  12. package/bin/adapter/entry-points/console/ensureConsoleRunning.js.map +1 -1
  13. package/bin/adapter/entry-points/console/webServer.js +3 -4
  14. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  15. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  16. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  17. package/bin/adapter/repositories/GraphqlProjectRepository.js +75 -4
  18. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  19. package/bin/adapter/repositories/utils.js +2 -4
  20. package/bin/adapter/repositories/utils.js.map +1 -1
  21. package/bin/domain/entities/ProjectFieldName.js +6 -0
  22. package/bin/domain/entities/ProjectFieldName.js.map +1 -0
  23. package/bin/domain/entities/RequiredProjectField.js +47 -0
  24. package/bin/domain/entities/RequiredProjectField.js.map +1 -0
  25. package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -1
  26. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  27. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +25 -0
  28. package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -0
  29. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +6 -1
  30. package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
  31. package/bin/domain/usecases/dashboard/DashboardProjectCode.js +17 -13
  32. package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/adapter/entry-points/cli/index.test.ts +62 -2
  35. package/src/adapter/entry-points/cli/index.ts +58 -11
  36. package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
  37. package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +37 -39
  38. package/src/adapter/entry-points/console/consoleDoneStore.test.ts +33 -33
  39. package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +135 -0
  40. package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +55 -0
  41. package/src/adapter/entry-points/console/consoleItemUrlLookup.test.ts +6 -6
  42. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +92 -70
  43. package/src/adapter/entry-points/console/consoleOperationApi.ts +51 -44
  44. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +35 -35
  45. package/src/adapter/entry-points/console/dashboardComposeService.test.ts +28 -28
  46. package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +26 -20
  47. package/src/adapter/entry-points/console/ensureConsoleRunning.test.ts +19 -2
  48. package/src/adapter/entry-points/console/ensureConsoleRunning.ts +8 -0
  49. package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +1 -1
  50. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +2 -2
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +3 -3
  53. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +19 -31
  54. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +24 -32
  55. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +6 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +4 -4
  57. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +8 -8
  58. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +5 -5
  59. package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +1 -1
  60. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +13 -13
  61. package/src/adapter/entry-points/console/webServer.test.ts +55 -52
  62. package/src/adapter/entry-points/console/webServer.ts +5 -4
  63. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +2 -2
  64. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
  65. package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +5 -5
  66. package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +5 -5
  67. package/src/adapter/repositories/GraphqlProjectRepository.ts +110 -4
  68. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +1 -1
  69. package/src/adapter/repositories/utils.ts +3 -3
  70. package/src/domain/entities/ProjectFieldName.ts +2 -0
  71. package/src/domain/entities/RequiredProjectField.ts +64 -0
  72. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
  73. package/src/domain/usecases/HandleScheduledEventUseCase.ts +5 -0
  74. package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +3 -3
  75. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +159 -0
  76. package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +32 -0
  77. package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +19 -19
  78. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +24 -14
  79. package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +10 -1
  80. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
  81. package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +28 -28
  82. package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +21 -21
  83. package/src/domain/usecases/dashboard/DashboardProjectCode.ts +21 -15
  84. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  85. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  86. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  87. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +6 -0
  88. package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -0
  89. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -1
  90. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  91. package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts +1 -1
  92. package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts.map +1 -1
  93. package/types/adapter/entry-points/console/webServer.d.ts +2 -2
  94. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  95. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  96. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +4 -1
  97. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  98. package/types/adapter/repositories/utils.d.ts.map +1 -1
  99. package/types/domain/entities/ProjectFieldName.d.ts +2 -0
  100. package/types/domain/entities/ProjectFieldName.d.ts.map +1 -0
  101. package/types/domain/entities/RequiredProjectField.d.ts +13 -0
  102. package/types/domain/entities/RequiredProjectField.d.ts.map +1 -0
  103. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
  104. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  105. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +10 -0
  106. package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -0
  107. package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
  108. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
  109. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
  110. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +2 -2
  111. package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts.map +1 -1
@@ -0,0 +1,64 @@
1
+ import { FieldOption } from './Project';
2
+
3
+ export type RequiredProjectFieldDataType = 'DATE' | 'TEXT' | 'SINGLE_SELECT';
4
+
5
+ export type RequiredProjectFieldDefinition = {
6
+ name: string;
7
+ dataType: RequiredProjectFieldDataType;
8
+ options: Omit<FieldOption, 'id'>[];
9
+ };
10
+
11
+ const storyOption = (
12
+ name: string,
13
+ color: FieldOption['color'],
14
+ ): Omit<FieldOption, 'id'> => ({ name, color, description: '' });
15
+
16
+ const REQUIRED_STORY_OPTIONS: Omit<FieldOption, 'id'>[] = [
17
+ storyOption('regular / NO STORY', 'RED'),
18
+ storyOption('regular / WORKFLOW BLOCKER', 'RED'),
19
+ storyOption('regular / high priority', 'RED'),
20
+ storyOption('regular / workflow management', 'YELLOW'),
21
+ storyOption('regular / routine management', 'YELLOW'),
22
+ storyOption('regular / middle bug', 'GRAY'),
23
+ storyOption('regular / minor bug', 'GRAY'),
24
+ storyOption('regular / refactor', 'GRAY'),
25
+ storyOption('regular / backlog', 'GRAY'),
26
+ ];
27
+
28
+ const REQUIRED_NEXT_ACTION_HOUR_OPTIONS: Omit<FieldOption, 'id'>[] = Array.from(
29
+ { length: 23 },
30
+ (_, index) => ({
31
+ name: `${index + 1}`,
32
+ color: 'GRAY' as const,
33
+ description: '',
34
+ }),
35
+ );
36
+
37
+ export const STORY_FIELD_NAME = 'Story';
38
+ export const NEXT_ACTION_DATE_FIELD_NAME = 'Next Action Date';
39
+ export const NEXT_ACTION_HOUR_FIELD_NAME = 'Next Action Hour';
40
+ export const DEPENDED_ISSUE_URL_FIELD_NAME =
41
+ 'Depended Issue URL separated by comma';
42
+
43
+ export const REQUIRED_PROJECT_FIELDS: RequiredProjectFieldDefinition[] = [
44
+ {
45
+ name: STORY_FIELD_NAME,
46
+ dataType: 'SINGLE_SELECT',
47
+ options: REQUIRED_STORY_OPTIONS,
48
+ },
49
+ {
50
+ name: NEXT_ACTION_DATE_FIELD_NAME,
51
+ dataType: 'DATE',
52
+ options: [],
53
+ },
54
+ {
55
+ name: NEXT_ACTION_HOUR_FIELD_NAME,
56
+ dataType: 'SINGLE_SELECT',
57
+ options: REQUIRED_NEXT_ACTION_HOUR_OPTIONS,
58
+ },
59
+ {
60
+ name: DEPENDED_ISSUE_URL_FIELD_NAME,
61
+ dataType: 'TEXT',
62
+ options: [],
63
+ },
64
+ ];
@@ -24,6 +24,7 @@ import { UpdateIssueStatusByLabelUseCase } from './UpdateIssueStatusByLabelUseCa
24
24
  import { StartPreparationUseCase } from './StartPreparationUseCase';
25
25
  import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUseCase';
26
26
  import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
27
+ import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
27
28
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
28
29
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
29
30
  import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
@@ -85,6 +86,8 @@ describe('HandleScheduledEventUseCase', () => {
85
86
  });
86
87
 
87
88
  describe('run', () => {
89
+ const mockProjectRequiredFieldCreateUseCase =
90
+ mock<ProjectRequiredFieldCreateUseCase>();
88
91
  const mockSetupTowerDefenceProjectUseCase =
89
92
  mock<SetupTowerDefenceProjectUseCase>();
90
93
  const mockActionAnnouncementUseCase = mock<ActionAnnouncementUseCase>();
@@ -128,6 +131,7 @@ describe('HandleScheduledEventUseCase', () => {
128
131
  const mockIssueRepository = mock<IssueRepository>();
129
132
 
130
133
  const useCase = new HandleScheduledEventUseCase(
134
+ mockProjectRequiredFieldCreateUseCase,
131
135
  mockSetupTowerDefenceProjectUseCase,
132
136
  mockActionAnnouncementUseCase,
133
137
  mockSetWorkflowManagementIssueToStoryUseCase,
@@ -29,6 +29,7 @@ import { RevertOrphanedPreparationUseCase } from './RevertOrphanedPreparationUse
29
29
  import { RevertNotReadyReviewQueueIssueUseCase } from './RevertNotReadyReviewQueueIssueUseCase';
30
30
  import { resolveLabelsAsLlmAgentName } from './resolveLabelsAsLlmAgentName';
31
31
  import { resolveAllowedIssueAuthors } from './resolveAllowedIssueAuthors';
32
+ import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
32
33
  import { SetupTowerDefenceProjectUseCase } from './SetupTowerDefenceProjectUseCase';
33
34
  import { UpdateRateLimitCacheUseCase } from './UpdateRateLimitCacheUseCase';
34
35
  import {
@@ -114,6 +115,7 @@ const isTransientSpreadsheetApiError = (error: Error): boolean => {
114
115
 
115
116
  export class HandleScheduledEventUseCase {
116
117
  constructor(
118
+ readonly projectRequiredFieldCreateUseCase: ProjectRequiredFieldCreateUseCase,
117
119
  readonly setupTowerDefenceProjectUseCase: SetupTowerDefenceProjectUseCase,
118
120
  readonly actionAnnouncementUseCase: ActionAnnouncementUseCase,
119
121
  readonly setWorkflowManagementIssueToStoryUseCase: SetWorkflowManagementIssueToStoryUseCase,
@@ -187,6 +189,9 @@ export class HandleScheduledEventUseCase {
187
189
  if (input.disabled) {
188
190
  return null;
189
191
  }
192
+ await this.projectRequiredFieldCreateUseCase.run({
193
+ projectUrl: input.projectUrl,
194
+ });
190
195
  await this.setupTowerDefenceProjectUseCase.run({
191
196
  projectUrl: input.projectUrl,
192
197
  });
@@ -755,18 +755,18 @@ describe('NotifySilentLiveSessionsUseCase', () => {
755
755
  });
756
756
 
757
757
  it('parks a role-named leader session while its latest owner call is unanswered', async () => {
758
- setupLiveInteractiveSession('uminopm');
758
+ setupLiveInteractiveSession('acmepm');
759
759
  mockSessionOutputActivityRepository.listSessionOutputActivities.mockResolvedValue(
760
760
  [
761
761
  {
762
- sessionName: 'uminopm',
762
+ sessionName: 'acmepm',
763
763
  lastOutputEpochSeconds:
764
764
  nowEpochSeconds - DEFAULT_MAIN_SILENT_THRESHOLD_SECONDS,
765
765
  },
766
766
  ],
767
767
  );
768
768
  mockOwnerCallStatusProvider.listUnansweredOwnerCallEpochSecondsBySessionName.mockResolvedValue(
769
- new Map([['uminopm', nowEpochSeconds - 60]]),
769
+ new Map([['acmepm', nowEpochSeconds - 60]]),
770
770
  );
771
771
 
772
772
  await useCase.run(runParams());
@@ -0,0 +1,159 @@
1
+ import { mock } from 'jest-mock-extended';
2
+ import { Project } from '../entities/Project';
3
+ import {
4
+ DEPENDED_ISSUE_URL_FIELD_NAME,
5
+ NEXT_ACTION_DATE_FIELD_NAME,
6
+ NEXT_ACTION_HOUR_FIELD_NAME,
7
+ STORY_FIELD_NAME,
8
+ } from '../entities/RequiredProjectField';
9
+ import { ProjectRequiredFieldCreateUseCase } from './ProjectRequiredFieldCreateUseCase';
10
+ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
11
+
12
+ describe('ProjectRequiredFieldCreateUseCase', () => {
13
+ const projectUrl = 'https://github.com/orgs/meta-site/projects/18';
14
+ const project: Project = {
15
+ id: 'PVT_kwDOBjp5zs4BfQGn',
16
+ url: projectUrl,
17
+ databaseId: 18,
18
+ name: 'cmg',
19
+ status: {
20
+ name: 'Status',
21
+ fieldId: 'PVTSSF_status',
22
+ statuses: [],
23
+ },
24
+ nextActionDate: null,
25
+ nextActionHour: null,
26
+ story: null,
27
+ remainingEstimationMinutes: null,
28
+ dependedIssueUrlSeparatedByComma: null,
29
+ completionDate50PercentConfidence: null,
30
+ };
31
+
32
+ const createUseCase = (existingFieldNames: string[]) => {
33
+ const projectRepository =
34
+ mock<
35
+ Pick<ProjectRepository, 'getByUrl' | 'listFieldNames' | 'createField'>
36
+ >();
37
+ projectRepository.getByUrl.mockResolvedValue(project);
38
+ projectRepository.listFieldNames.mockResolvedValue(existingFieldNames);
39
+ projectRepository.createField.mockResolvedValue(undefined);
40
+ return {
41
+ projectRepository,
42
+ useCase: new ProjectRequiredFieldCreateUseCase(projectRepository),
43
+ };
44
+ };
45
+
46
+ it('should create every required field when the project has none of them', async () => {
47
+ const { projectRepository, useCase } = createUseCase([
48
+ 'Title',
49
+ 'Assignees',
50
+ 'Status',
51
+ ]);
52
+
53
+ await useCase.run({ projectUrl });
54
+
55
+ const createdFieldNames = projectRepository.createField.mock.calls.map(
56
+ (call) => call[1].name,
57
+ );
58
+ expect(createdFieldNames).toEqual([
59
+ STORY_FIELD_NAME,
60
+ NEXT_ACTION_DATE_FIELD_NAME,
61
+ NEXT_ACTION_HOUR_FIELD_NAME,
62
+ DEPENDED_ISSUE_URL_FIELD_NAME,
63
+ ]);
64
+ });
65
+
66
+ it('should create nothing when the project already has every required field', async () => {
67
+ const { projectRepository, useCase } = createUseCase([
68
+ 'Title',
69
+ 'Status',
70
+ 'story',
71
+ 'nextactiondate',
72
+ 'nextactionhour',
73
+ 'Depended Issue URL separated by comma',
74
+ ]);
75
+
76
+ await useCase.run({ projectUrl });
77
+
78
+ expect(projectRepository.createField).not.toHaveBeenCalled();
79
+ });
80
+
81
+ it('should create only the missing fields', async () => {
82
+ const { projectRepository, useCase } = createUseCase([
83
+ 'Title',
84
+ 'Status',
85
+ 'Story',
86
+ 'Next Action Date',
87
+ ]);
88
+
89
+ await useCase.run({ projectUrl });
90
+
91
+ const createdFieldNames = projectRepository.createField.mock.calls.map(
92
+ (call) => call[1].name,
93
+ );
94
+ expect(createdFieldNames).toEqual([
95
+ NEXT_ACTION_HOUR_FIELD_NAME,
96
+ DEPENDED_ISSUE_URL_FIELD_NAME,
97
+ ]);
98
+ });
99
+
100
+ it('should create the story field with the standard story options', async () => {
101
+ const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
102
+
103
+ await useCase.run({ projectUrl });
104
+
105
+ const storyCall = projectRepository.createField.mock.calls.find(
106
+ (call) => call[1].name === STORY_FIELD_NAME,
107
+ );
108
+ expect(storyCall?.[1].dataType).toBe('SINGLE_SELECT');
109
+ expect(storyCall?.[1].options.map((option) => option.name)).toEqual([
110
+ 'regular / NO STORY',
111
+ 'regular / WORKFLOW BLOCKER',
112
+ 'regular / high priority',
113
+ 'regular / workflow management',
114
+ 'regular / routine management',
115
+ 'regular / middle bug',
116
+ 'regular / minor bug',
117
+ 'regular / refactor',
118
+ 'regular / backlog',
119
+ ]);
120
+ });
121
+
122
+ it('should create the next action hour field with the hour options from 1 to 23', async () => {
123
+ const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
124
+
125
+ await useCase.run({ projectUrl });
126
+
127
+ const hourCall = projectRepository.createField.mock.calls.find(
128
+ (call) => call[1].name === NEXT_ACTION_HOUR_FIELD_NAME,
129
+ );
130
+ expect(hourCall?.[1].dataType).toBe('SINGLE_SELECT');
131
+ expect(hourCall?.[1].options.map((option) => option.name)).toEqual(
132
+ Array.from({ length: 23 }, (_, index) => `${index + 1}`),
133
+ );
134
+ });
135
+
136
+ it('should create the depended issue url field as a text field', async () => {
137
+ const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
138
+
139
+ await useCase.run({ projectUrl });
140
+
141
+ const dependedCall = projectRepository.createField.mock.calls.find(
142
+ (call) => call[1].name === DEPENDED_ISSUE_URL_FIELD_NAME,
143
+ );
144
+ expect(dependedCall?.[1].dataType).toBe('TEXT');
145
+ expect(dependedCall?.[1].options).toEqual([]);
146
+ });
147
+
148
+ it('should create the next action date field as a date field', async () => {
149
+ const { projectRepository, useCase } = createUseCase(['Title', 'Status']);
150
+
151
+ await useCase.run({ projectUrl });
152
+
153
+ const dateCall = projectRepository.createField.mock.calls.find(
154
+ (call) => call[1].name === NEXT_ACTION_DATE_FIELD_NAME,
155
+ );
156
+ expect(dateCall?.[1].dataType).toBe('DATE');
157
+ expect(dateCall?.[1].options).toEqual([]);
158
+ });
159
+ });
@@ -0,0 +1,32 @@
1
+ import { Project } from '../entities/Project';
2
+ import { normalizeProjectFieldName } from '../entities/ProjectFieldName';
3
+ import { REQUIRED_PROJECT_FIELDS } from '../entities/RequiredProjectField';
4
+ import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
5
+
6
+ export class ProjectRequiredFieldCreateUseCase {
7
+ constructor(
8
+ private readonly projectRepository: Pick<
9
+ ProjectRepository,
10
+ 'getByUrl' | 'listFieldNames' | 'createField'
11
+ >,
12
+ ) {}
13
+
14
+ run = async (params: { projectUrl: string }): Promise<void> => {
15
+ const project = await this.projectRepository.getByUrl(params.projectUrl);
16
+ await this.createMissingFields(project);
17
+ };
18
+
19
+ private createMissingFields = async (project: Project): Promise<void> => {
20
+ const existingFieldNames = (
21
+ await this.projectRepository.listFieldNames(project)
22
+ ).map(normalizeProjectFieldName);
23
+ for (const required of REQUIRED_PROJECT_FIELDS) {
24
+ if (
25
+ existingFieldNames.includes(normalizeProjectFieldName(required.name))
26
+ ) {
27
+ continue;
28
+ }
29
+ await this.projectRepository.createField(project, required);
30
+ }
31
+ };
32
+ }
@@ -381,9 +381,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
381
381
  nextActionDate: null,
382
382
  nextActionHour: null,
383
383
  isPr: false,
384
- org: 'xcare-medical',
385
- repo: 'xcare-platform',
386
- url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
384
+ org: 'initech-medical',
385
+ repo: 'initech-platform',
386
+ url: 'https://github.com/initech-medical/initech-platform/issues/1445',
387
387
  };
388
388
  mockIssueRepository.searchIssue.mockResolvedValue([]);
389
389
 
@@ -401,8 +401,8 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
401
401
  expect(mockIssueRepository.searchIssue.mock.calls).toEqual([
402
402
  [
403
403
  {
404
- owner: 'xcare-medical',
405
- repositoryName: 'xcare-platform',
404
+ owner: 'initech-medical',
405
+ repositoryName: 'initech-platform',
406
406
  type: 'issue',
407
407
  state: 'open',
408
408
  title:
@@ -412,16 +412,16 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
412
412
  ]);
413
413
  expect(mockIssueRepository.createNewIssue).toHaveBeenCalledTimes(1);
414
414
  const createCall = mockIssueRepository.createNewIssue.mock.calls[0];
415
- expect(createCall[0]).toEqual('xcare-medical');
416
- expect(createCall[1]).toEqual('xcare-platform');
415
+ expect(createCall[0]).toEqual('initech-medical');
416
+ expect(createCall[1]).toEqual('initech-platform');
417
417
  expect(createCall[2]).toEqual(
418
418
  'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
419
419
  );
420
420
  expect(createCall[3]).toContain(
421
- 'https://github.com/xcare-medical/xcare-platform/issues/1445',
421
+ 'https://github.com/initech-medical/initech-platform/issues/1445',
422
422
  );
423
423
  expect(createCall[3]).toContain('story:routine-management');
424
- expect(createCall[4]).toEqual(['xcare-medical']);
424
+ expect(createCall[4]).toEqual(['initech-medical']);
425
425
  expect(createCall[5]).toEqual([]);
426
426
  });
427
427
 
@@ -434,13 +434,13 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
434
434
  nextActionDate: null,
435
435
  nextActionHour: null,
436
436
  isPr: false,
437
- org: 'xcare-medical',
438
- repo: 'xcare-platform',
439
- url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
437
+ org: 'initech-medical',
438
+ repo: 'initech-platform',
439
+ url: 'https://github.com/initech-medical/initech-platform/issues/1445',
440
440
  };
441
441
  mockIssueRepository.searchIssue.mockResolvedValue([
442
442
  {
443
- url: 'https://github.com/xcare-medical/xcare-platform/issues/9999',
443
+ url: 'https://github.com/initech-medical/initech-platform/issues/9999',
444
444
  title:
445
445
  'TDPM: story label "story:routine-management" has no matching "regular / routine-management" Story option',
446
446
  number: '9999',
@@ -471,9 +471,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
471
471
  nextActionDate: null,
472
472
  nextActionHour: null,
473
473
  isPr: false,
474
- org: 'xcare-medical',
475
- repo: 'xcare-platform',
476
- url: 'https://github.com/xcare-medical/xcare-platform/issues/1500',
474
+ org: 'initech-medical',
475
+ repo: 'initech-platform',
476
+ url: 'https://github.com/initech-medical/initech-platform/issues/1500',
477
477
  };
478
478
  mockIssueRepository.searchIssue.mockResolvedValue([]);
479
479
 
@@ -503,9 +503,9 @@ describe('SetWorkflowManagementIssueToStoryUseCase', () => {
503
503
  nextActionDate: null,
504
504
  nextActionHour: null,
505
505
  isPr: false,
506
- org: 'xcare-medical',
507
- repo: 'xcare-platform',
508
- url: 'https://github.com/xcare-medical/xcare-platform/issues/1445',
506
+ org: 'initech-medical',
507
+ repo: 'initech-platform',
508
+ url: 'https://github.com/initech-medical/initech-platform/issues/1445',
509
509
  };
510
510
  const matchedIssue: Issue = {
511
511
  ...mock<Issue>(),
@@ -367,22 +367,31 @@ describe('SetupTowerDefenceProjectUseCase', () => {
367
367
  expect(payload[1].color).toBe(REQUIRED_WORKFLOW_STATUSES[1].color);
368
368
  });
369
369
 
370
- it('should rename legacy "Todo" to "Todo by human" by reusing the existing option ID', async () => {
370
+ it('should reuse the default template "Todo" option ID for Unread so a newly added item lands in Unread', async () => {
371
371
  const mockProjectRepository =
372
372
  mock<Pick<ProjectRepository, 'getByUrl' | 'updateStatusList'>>();
373
373
  const mockIssueRepository =
374
374
  mock<Pick<IssueRepository, 'getAllIssues' | 'updateStatus'>>();
375
- const statuses: FieldOption[] = REQUIRED_WORKFLOW_STATUSES.map(
376
- (required, index) => ({
377
- id: `id-${index}`,
378
- name:
379
- required.name === TODO_STATUS_NAME
380
- ? LEGACY_TODO_STATUS_NAME
381
- : required.name,
382
- color: required.color,
375
+ const statuses: FieldOption[] = [
376
+ {
377
+ id: 'template-todo',
378
+ name: LEGACY_TODO_STATUS_NAME,
379
+ color: 'PINK',
383
380
  description: '',
384
- }),
385
- );
381
+ },
382
+ {
383
+ id: 'template-in-progress',
384
+ name: 'In progress',
385
+ color: 'YELLOW',
386
+ description: 'This is actively being worked on',
387
+ },
388
+ {
389
+ id: 'template-done',
390
+ name: DONE_STATUS_NAME,
391
+ color: 'PURPLE',
392
+ description: '',
393
+ },
394
+ ];
386
395
  const project = buildProject(statuses);
387
396
  mockProjectRepository.getByUrl.mockResolvedValue(project);
388
397
  mockProjectRepository.updateStatusList.mockResolvedValue([]);
@@ -400,9 +409,10 @@ describe('SetupTowerDefenceProjectUseCase', () => {
400
409
 
401
410
  expect(mockProjectRepository.updateStatusList).toHaveBeenCalledTimes(1);
402
411
  const [, payload] = mockProjectRepository.updateStatusList.mock.calls[0];
403
- const todoEntry = payload.find((s) => s.name === TODO_STATUS_NAME);
404
- expect(todoEntry).toBeDefined();
405
- expect(todoEntry?.id).toBe('id-5');
412
+ const unreadEntry = payload.find((s) => s.name === DEFAULT_STATUS_NAME);
413
+ expect(unreadEntry?.id).toBe('template-todo');
414
+ const todoByHumanEntry = payload.find((s) => s.name === TODO_STATUS_NAME);
415
+ expect(todoByHumanEntry?.id).toBeNull();
406
416
  expect(payload.some((s) => s.name === LEGACY_TODO_STATUS_NAME)).toBe(false);
407
417
  });
408
418
 
@@ -2,6 +2,7 @@ import { FieldOption } from '../entities/Project';
2
2
  import { ProjectRepository } from './adapter-interfaces/ProjectRepository';
3
3
  import { IssueRepository } from './adapter-interfaces/IssueRepository';
4
4
  import {
5
+ DEFAULT_STATUS_NAME,
5
6
  IN_TMUX_STATUS_NAME,
6
7
  LEGACY_AWAITING_TASK_BREAKDOWN_STATUS_NAME,
7
8
  LEGACY_IN_TMUX_STATUS_NAME,
@@ -27,6 +28,7 @@ export class SetupTowerDefenceProjectUseCase {
27
28
  private static readonly LEGACY_STATUS_NAMES: Readonly<
28
29
  Record<string, string>
29
30
  > = {
31
+ [DEFAULT_STATUS_NAME]: LEGACY_TODO_STATUS_NAME,
30
32
  [TODO_STATUS_NAME]: LEGACY_TODO_STATUS_NAME,
31
33
  [IN_TMUX_STATUS_NAME]: LEGACY_IN_TMUX_STATUS_NAME,
32
34
  };
@@ -84,6 +86,7 @@ export class SetupTowerDefenceProjectUseCase {
84
86
  !SetupTowerDefenceProjectUseCase.MIGRATED_FROM_NAMES.has(status.name),
85
87
  );
86
88
 
89
+ const reusedOptionIds = new Set<FieldOption['id']>();
87
90
  const newStatusList: (Omit<FieldOption, 'id'> & {
88
91
  id: FieldOption['id'] | null;
89
92
  })[] = [
@@ -93,8 +96,14 @@ export class SetupTowerDefenceProjectUseCase {
93
96
  const found =
94
97
  existing.find((status) => status.name === required.name) ??
95
98
  (legacyName !== undefined
96
- ? existing.find((status) => status.name === legacyName)
99
+ ? existing.find(
100
+ (status) =>
101
+ status.name === legacyName && !reusedOptionIds.has(status.id),
102
+ )
97
103
  : undefined);
104
+ if (found) {
105
+ reusedOptionIds.add(found.id);
106
+ }
98
107
  return {
99
108
  id: found ? found.id : null,
100
109
  name: required.name,
@@ -1,6 +1,12 @@
1
1
  import { FieldOption, Project } from '../../entities/Project';
2
+ import { RequiredProjectFieldDefinition } from '../../entities/RequiredProjectField';
2
3
 
3
4
  export interface ProjectRepository {
5
+ listFieldNames: (project: Project) => Promise<string[]>;
6
+ createField: (
7
+ project: Project,
8
+ field: RequiredProjectFieldDefinition,
9
+ ) => Promise<void>;
4
10
  findProjectIdByUrl: (projectUrl: string) => Promise<Project['id'] | null>;
5
11
  getProject: (projectId: Project['id']) => Promise<Project | null>;
6
12
  updateStoryList: (