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
@@ -9,31 +9,31 @@ import { recordDoneProjectItemId } from './consoleDoneStore';
9
9
 
10
10
  describe('parseConsoleDataRoute', () => {
11
11
  it('parses a list route', () => {
12
- expect(parseConsoleDataRoute('/projects/umino/prs/list.json')).toEqual({
12
+ expect(parseConsoleDataRoute('/projects/acme/prs/list.json')).toEqual({
13
13
  kind: 'list',
14
- pjcode: 'umino',
14
+ pjcode: 'acme',
15
15
  tab: 'prs',
16
16
  });
17
17
  });
18
18
 
19
19
  it('parses a list route for todo-by-human', () => {
20
20
  expect(
21
- parseConsoleDataRoute('/projects/umino/todo-by-human/list.json'),
22
- ).toEqual({ kind: 'list', pjcode: 'umino', tab: 'todo-by-human' });
21
+ parseConsoleDataRoute('/projects/acme/todo-by-human/list.json'),
22
+ ).toEqual({ kind: 'list', pjcode: 'acme', tab: 'todo-by-human' });
23
23
  });
24
24
 
25
25
  it('parses a list route for todo-by-agent', () => {
26
26
  expect(
27
- parseConsoleDataRoute('/projects/umino/todo-by-agent/list.json'),
28
- ).toEqual({ kind: 'list', pjcode: 'umino', tab: 'todo-by-agent' });
27
+ parseConsoleDataRoute('/projects/acme/todo-by-agent/list.json'),
28
+ ).toEqual({ kind: 'list', pjcode: 'acme', tab: 'todo-by-agent' });
29
29
  });
30
30
 
31
31
  it('parses a detail route', () => {
32
32
  expect(
33
- parseConsoleDataRoute('/projects/umino/triage/detail/123.json'),
33
+ parseConsoleDataRoute('/projects/acme/triage/detail/123.json'),
34
34
  ).toEqual({
35
35
  kind: 'detail',
36
- pjcode: 'umino',
36
+ pjcode: 'acme',
37
37
  tab: 'triage',
38
38
  key: '123.json',
39
39
  });
@@ -41,49 +41,47 @@ describe('parseConsoleDataRoute', () => {
41
41
 
42
42
  it('parses an in-tmux route', () => {
43
43
  expect(
44
- parseConsoleDataRoute('/projects/umino/in-tmux-by-human/list.json'),
44
+ parseConsoleDataRoute('/projects/acme/in-tmux-by-human/list.json'),
45
45
  ).toEqual({
46
46
  kind: 'in-tmux',
47
- pjcode: 'umino',
47
+ pjcode: 'acme',
48
48
  relativePath: 'list.json',
49
49
  });
50
50
  });
51
51
 
52
52
  it('parses a nested in-tmux route', () => {
53
53
  expect(
54
- parseConsoleDataRoute('/projects/umino/in-tmux-by-human/sub/data.json'),
54
+ parseConsoleDataRoute('/projects/acme/in-tmux-by-human/sub/data.json'),
55
55
  ).toEqual({
56
56
  kind: 'in-tmux',
57
- pjcode: 'umino',
57
+ pjcode: 'acme',
58
58
  relativePath: 'sub/data.json',
59
59
  });
60
60
  });
61
61
 
62
62
  it('rejects unknown tabs', () => {
63
63
  expect(
64
- parseConsoleDataRoute('/projects/umino/unknown/list.json'),
64
+ parseConsoleDataRoute('/projects/acme/unknown/list.json'),
65
65
  ).toBeNull();
66
66
  });
67
67
 
68
68
  it('rejects a non-projects prefix', () => {
69
- expect(parseConsoleDataRoute('/other/umino/prs/list.json')).toBeNull();
69
+ expect(parseConsoleDataRoute('/other/acme/prs/list.json')).toBeNull();
70
70
  });
71
71
 
72
72
  it('rejects dot segments in pjcode or tab', () => {
73
73
  expect(parseConsoleDataRoute('/projects/../prs/list.json')).toBeNull();
74
- expect(parseConsoleDataRoute('/projects/umino/../list.json')).toBeNull();
74
+ expect(parseConsoleDataRoute('/projects/acme/../list.json')).toBeNull();
75
75
  });
76
76
 
77
77
  it('rejects a non-json detail key', () => {
78
78
  expect(
79
- parseConsoleDataRoute('/projects/umino/prs/detail/123.txt'),
79
+ parseConsoleDataRoute('/projects/acme/prs/detail/123.txt'),
80
80
  ).toBeNull();
81
81
  });
82
82
 
83
83
  it('rejects an empty in-tmux relative path', () => {
84
- expect(
85
- parseConsoleDataRoute('/projects/umino/in-tmux-by-human'),
86
- ).toBeNull();
84
+ expect(parseConsoleDataRoute('/projects/acme/in-tmux-by-human')).toBeNull();
87
85
  });
88
86
  });
89
87
 
@@ -107,72 +105,72 @@ describe('buildConsoleDataResponse', () => {
107
105
  it('returns 404 when the list file is absent', () => {
108
106
  const response = buildConsoleDataResponse(baseDir, {
109
107
  kind: 'list',
110
- pjcode: 'umino',
108
+ pjcode: 'acme',
111
109
  tab: 'prs',
112
110
  });
113
111
  expect(response.statusCode).toBe(404);
114
112
  });
115
113
 
116
114
  it('serves a list file and applies the done exclusion', () => {
117
- writeJson('umino/prs/list.json', {
118
- pjcode: 'umino',
115
+ writeJson('acme/prs/list.json', {
116
+ pjcode: 'acme',
119
117
  items: [
120
118
  { projectItemId: 'PVTI_1', title: 'keep' },
121
119
  { projectItemId: 'PVTI_2', title: 'drop' },
122
120
  ],
123
121
  });
124
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_2');
122
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
125
123
  const response = buildConsoleDataResponse(baseDir, {
126
124
  kind: 'list',
127
- pjcode: 'umino',
125
+ pjcode: 'acme',
128
126
  tab: 'prs',
129
127
  });
130
128
  expect(response.statusCode).toBe(200);
131
129
  const parsed: unknown = JSON.parse(response.body);
132
130
  expect(parsed).toEqual({
133
- pjcode: 'umino',
131
+ pjcode: 'acme',
134
132
  items: [{ projectItemId: 'PVTI_1', title: 'keep' }],
135
133
  });
136
134
  });
137
135
 
138
136
  it('applies the done exclusion to the workflow-blocker list', () => {
139
- writeJson('umino/workflow-blocker/list.json', {
140
- pjcode: 'umino',
137
+ writeJson('acme/workflow-blocker/list.json', {
138
+ pjcode: 'acme',
141
139
  items: [
142
140
  { projectItemId: 'PVTI_1', title: 'keep' },
143
141
  { projectItemId: 'PVTI_2', title: 'processed blocker' },
144
142
  ],
145
143
  });
146
- recordDoneProjectItemId(baseDir, 'umino', 'workflow-blocker', 'PVTI_2');
144
+ recordDoneProjectItemId(baseDir, 'acme', 'workflow-blocker', 'PVTI_2');
147
145
  const response = buildConsoleDataResponse(baseDir, {
148
146
  kind: 'list',
149
- pjcode: 'umino',
147
+ pjcode: 'acme',
150
148
  tab: 'workflow-blocker',
151
149
  });
152
150
  expect(response.statusCode).toBe(200);
153
151
  const parsed: unknown = JSON.parse(response.body);
154
152
  expect(parsed).toEqual({
155
- pjcode: 'umino',
153
+ pjcode: 'acme',
156
154
  items: [{ projectItemId: 'PVTI_1', title: 'keep' }],
157
155
  });
158
156
  });
159
157
 
160
158
  it('serves a list file without an items array unchanged', () => {
161
- writeJson('umino/prs/list.json', { pjcode: 'umino' });
159
+ writeJson('acme/prs/list.json', { pjcode: 'acme' });
162
160
  const response = buildConsoleDataResponse(baseDir, {
163
161
  kind: 'list',
164
- pjcode: 'umino',
162
+ pjcode: 'acme',
165
163
  tab: 'prs',
166
164
  });
167
165
  expect(response.statusCode).toBe(200);
168
- expect(JSON.parse(response.body)).toEqual({ pjcode: 'umino' });
166
+ expect(JSON.parse(response.body)).toEqual({ pjcode: 'acme' });
169
167
  });
170
168
 
171
169
  it('serves a detail file without exclusion', () => {
172
- writeJson('umino/triage/detail/123.json', { number: 123 });
170
+ writeJson('acme/triage/detail/123.json', { number: 123 });
173
171
  const response = buildConsoleDataResponse(baseDir, {
174
172
  kind: 'detail',
175
- pjcode: 'umino',
173
+ pjcode: 'acme',
176
174
  tab: 'triage',
177
175
  key: '123.json',
178
176
  });
@@ -183,7 +181,7 @@ describe('buildConsoleDataResponse', () => {
183
181
  it('returns 404 when the detail file is absent', () => {
184
182
  const response = buildConsoleDataResponse(baseDir, {
185
183
  kind: 'detail',
186
- pjcode: 'umino',
184
+ pjcode: 'acme',
187
185
  tab: 'triage',
188
186
  key: '404.json',
189
187
  });
@@ -191,10 +189,10 @@ describe('buildConsoleDataResponse', () => {
191
189
  });
192
190
 
193
191
  it('serves an in-tmux file', () => {
194
- writeJson('umino/in-tmux-by-human/list.json', { items: [] });
192
+ writeJson('acme/in-tmux-by-human/list.json', { items: [] });
195
193
  const response = buildConsoleDataResponse(baseDir, {
196
194
  kind: 'in-tmux',
197
- pjcode: 'umino',
195
+ pjcode: 'acme',
198
196
  relativePath: 'list.json',
199
197
  });
200
198
  expect(response.statusCode).toBe(200);
@@ -204,7 +202,7 @@ describe('buildConsoleDataResponse', () => {
204
202
  it('returns 404 when the in-tmux file is absent', () => {
205
203
  const response = buildConsoleDataResponse(baseDir, {
206
204
  kind: 'in-tmux',
207
- pjcode: 'umino',
205
+ pjcode: 'acme',
208
206
  relativePath: 'missing.json',
209
207
  });
210
208
  expect(response.statusCode).toBe(404);
@@ -25,48 +25,48 @@ describe('consoleDoneStore', () => {
25
25
 
26
26
  describe('doneFilePathForTab', () => {
27
27
  it('places the done file under pjcode/tab', () => {
28
- expect(doneFilePathForTab(baseDir, 'umino', 'prs')).toBe(
29
- path.join(baseDir, 'umino', 'prs', CONSOLE_DONE_FILE_NAME),
28
+ expect(doneFilePathForTab(baseDir, 'acme', 'prs')).toBe(
29
+ path.join(baseDir, 'acme', 'prs', CONSOLE_DONE_FILE_NAME),
30
30
  );
31
31
  });
32
32
  });
33
33
 
34
34
  describe('readDoneProjectItemIds', () => {
35
35
  it('returns an empty array when the file is absent', () => {
36
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([]);
36
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
37
37
  });
38
38
 
39
39
  it('returns the persisted project item ids', () => {
40
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_1');
41
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_2');
42
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([
40
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_1');
41
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
42
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([
43
43
  'PVTI_1',
44
44
  'PVTI_2',
45
45
  ]);
46
46
  });
47
47
 
48
48
  it('returns an empty array for malformed json', () => {
49
- const filePath = doneFilePathForTab(baseDir, 'umino', 'prs');
49
+ const filePath = doneFilePathForTab(baseDir, 'acme', 'prs');
50
50
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
51
  fs.writeFileSync(filePath, '{ not json');
52
- expect(() => readDoneProjectItemIds(baseDir, 'umino', 'prs')).toThrow();
52
+ expect(() => readDoneProjectItemIds(baseDir, 'acme', 'prs')).toThrow();
53
53
  });
54
54
 
55
55
  it('returns an empty array when projectItemIds is not an array', () => {
56
- const filePath = doneFilePathForTab(baseDir, 'umino', 'prs');
56
+ const filePath = doneFilePathForTab(baseDir, 'acme', 'prs');
57
57
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
58
58
  fs.writeFileSync(filePath, JSON.stringify({ projectItemIds: 'x' }));
59
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([]);
59
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
60
60
  });
61
61
 
62
62
  it('filters out non-string entries', () => {
63
- const filePath = doneFilePathForTab(baseDir, 'umino', 'prs');
63
+ const filePath = doneFilePathForTab(baseDir, 'acme', 'prs');
64
64
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
65
65
  fs.writeFileSync(
66
66
  filePath,
67
67
  JSON.stringify({ projectItemIds: ['PVTI_1', 1, null, ''] }),
68
68
  );
69
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([
69
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([
70
70
  'PVTI_1',
71
71
  ]);
72
72
  });
@@ -74,14 +74,14 @@ describe('consoleDoneStore', () => {
74
74
 
75
75
  describe('recordDoneProjectItemId', () => {
76
76
  it('does not record an empty id', () => {
77
- recordDoneProjectItemId(baseDir, 'umino', 'prs', '');
78
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([]);
77
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', '');
78
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
79
79
  });
80
80
 
81
81
  it('does not duplicate an already recorded id', () => {
82
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_1');
83
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_1');
84
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([
82
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_1');
83
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_1');
84
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([
85
85
  'PVTI_1',
86
86
  ]);
87
87
  });
@@ -89,9 +89,9 @@ describe('consoleDoneStore', () => {
89
89
 
90
90
  describe('recordDoneProjectItemIdAcrossTabs', () => {
91
91
  it('records the id into every tab done file', () => {
92
- recordDoneProjectItemIdAcrossTabs(baseDir, 'umino', 'PVTI_99');
92
+ recordDoneProjectItemIdAcrossTabs(baseDir, 'acme', 'PVTI_99');
93
93
  for (const tab of CONSOLE_DONE_TAB_NAMES) {
94
- expect(readDoneProjectItemIds(baseDir, 'umino', tab)).toEqual([
94
+ expect(readDoneProjectItemIds(baseDir, 'acme', tab)).toEqual([
95
95
  'PVTI_99',
96
96
  ]);
97
97
  }
@@ -103,32 +103,32 @@ describe('consoleDoneStore', () => {
103
103
 
104
104
  it('includes the workflow-blocker tab so processed blockers are excluded', () => {
105
105
  expect(CONSOLE_DONE_TAB_NAMES).toContain('workflow-blocker');
106
- recordDoneProjectItemIdAcrossTabs(baseDir, 'umino', 'PVTI_7');
106
+ recordDoneProjectItemIdAcrossTabs(baseDir, 'acme', 'PVTI_7');
107
107
  expect(
108
- readDoneProjectItemIds(baseDir, 'umino', 'workflow-blocker'),
108
+ readDoneProjectItemIds(baseDir, 'acme', 'workflow-blocker'),
109
109
  ).toEqual(['PVTI_7']);
110
110
  });
111
111
  });
112
112
 
113
113
  describe('resetDoneProjectItemIds', () => {
114
114
  it('clears an accumulated done file to an empty record', () => {
115
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_1');
116
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_2');
117
- recordDoneProjectItemId(baseDir, 'umino', 'prs', 'PVTI_3');
118
- resetDoneProjectItemIds(baseDir, 'umino', 'prs');
119
- expect(readDoneProjectItemIds(baseDir, 'umino', 'prs')).toEqual([]);
115
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_1');
116
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
117
+ recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_3');
118
+ resetDoneProjectItemIds(baseDir, 'acme', 'prs');
119
+ expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
120
120
  });
121
121
 
122
122
  it('writes an empty record when no done file exists yet', () => {
123
- resetDoneProjectItemIds(baseDir, 'umino', 'prs');
124
- const filePath = doneFilePathForTab(baseDir, 'umino', 'prs');
123
+ resetDoneProjectItemIds(baseDir, 'acme', 'prs');
124
+ const filePath = doneFilePathForTab(baseDir, 'acme', 'prs');
125
125
  const raw: unknown = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
126
126
  expect(raw).toEqual({ projectItemIds: [] });
127
127
  });
128
128
 
129
129
  it('does not leave a temp file behind', () => {
130
- resetDoneProjectItemIds(baseDir, 'umino', 'prs');
131
- const filePath = doneFilePathForTab(baseDir, 'umino', 'prs');
130
+ resetDoneProjectItemIds(baseDir, 'acme', 'prs');
131
+ const filePath = doneFilePathForTab(baseDir, 'acme', 'prs');
132
132
  expect(fs.existsSync(`${filePath}.tmp`)).toBe(false);
133
133
  });
134
134
  });
@@ -137,11 +137,11 @@ describe('consoleDoneStore', () => {
137
137
  it('clears the done file of every tab including in-tmux-by-human', () => {
138
138
  expect(CONSOLE_DONE_TAB_NAMES).toContain('in-tmux-by-human');
139
139
  for (const tab of CONSOLE_DONE_TAB_NAMES) {
140
- recordDoneProjectItemId(baseDir, 'umino', tab, 'PVTI_1');
140
+ recordDoneProjectItemId(baseDir, 'acme', tab, 'PVTI_1');
141
141
  }
142
- resetDoneProjectItemIdsAcrossTabs(baseDir, 'umino');
142
+ resetDoneProjectItemIdsAcrossTabs(baseDir, 'acme');
143
143
  for (const tab of CONSOLE_DONE_TAB_NAMES) {
144
- expect(readDoneProjectItemIds(baseDir, 'umino', tab)).toEqual([]);
144
+ expect(readDoneProjectItemIds(baseDir, 'acme', tab)).toEqual([]);
145
145
  }
146
146
  });
147
147
  });
@@ -0,0 +1,135 @@
1
+ import {
2
+ createConsoleGithubTokenResolver,
3
+ createConsoleIssueRepositoryResolver,
4
+ extractRepositoryOwner,
5
+ } from './consoleGithubTokenResolver';
6
+
7
+ describe('createConsoleIssueRepositoryResolver', () => {
8
+ it('builds the issue repository from the token of the owner in the url', () => {
9
+ const resolve = createConsoleIssueRepositoryResolver<string>(
10
+ (repositoryOwner) => `token-of-${repositoryOwner}`,
11
+ (githubToken) => `repository-with-${githubToken}`,
12
+ );
13
+
14
+ expect(
15
+ resolve('https://github.com/meta-site/hr-audit-mock/issues/178'),
16
+ ).toBe('repository-with-token-of-meta-site');
17
+ });
18
+
19
+ it('throws when the repository owner cannot be read from the url', () => {
20
+ const resolve = createConsoleIssueRepositoryResolver<string>(
21
+ (repositoryOwner) => `token-of-${repositoryOwner}`,
22
+ (githubToken) => `repository-with-${githubToken}`,
23
+ );
24
+
25
+ expect(() => resolve('https://github.com/meta-site/hr-audit-mock')).toThrow(
26
+ 'The repository owner cannot be read from the operated url: https://github.com/meta-site/hr-audit-mock',
27
+ );
28
+ });
29
+ });
30
+
31
+ describe('extractRepositoryOwner', () => {
32
+ it('should return the owner of an issue url', () => {
33
+ expect(
34
+ extractRepositoryOwner(
35
+ 'https://github.com/meta-site/hr-audit-mock/issues/178',
36
+ ),
37
+ ).toBe('meta-site');
38
+ });
39
+
40
+ it('should return the owner of a pull request url', () => {
41
+ expect(
42
+ extractRepositoryOwner(
43
+ 'https://github.com/HiromiShikata/npm-cli-github-issue-tower-defence-management/pull/1399',
44
+ ),
45
+ ).toBe('HiromiShikata');
46
+ });
47
+
48
+ it('should return null for a url that is not an issue or a pull request', () => {
49
+ expect(
50
+ extractRepositoryOwner('https://github.com/meta-site/hr-audit-mock'),
51
+ ).toBeNull();
52
+ });
53
+ });
54
+
55
+ describe('createConsoleGithubTokenResolver', () => {
56
+ it('should return the default token for an owner that has no token file configured', () => {
57
+ const resolve = createConsoleGithubTokenResolver(
58
+ 'default-token',
59
+ null,
60
+ () => {
61
+ throw new Error('must not read any file');
62
+ },
63
+ );
64
+
65
+ expect(resolve('HiromiShikata')).toBe('default-token');
66
+ });
67
+
68
+ it('should return the token read from the file configured for that owner', () => {
69
+ const resolve = createConsoleGithubTokenResolver(
70
+ 'default-token',
71
+ { 'meta-site': '/creds/meta-site-token.txt' },
72
+ (filePath) =>
73
+ filePath === '/creds/meta-site-token.txt'
74
+ ? 'fine-grained-token\n'
75
+ : (() => {
76
+ throw new Error(`unexpected file path: ${filePath}`);
77
+ })(),
78
+ );
79
+
80
+ expect(resolve('meta-site')).toBe('fine-grained-token');
81
+ });
82
+
83
+ it('should keep using the default token for the other owners when one owner has a token file', () => {
84
+ const resolve = createConsoleGithubTokenResolver(
85
+ 'default-token',
86
+ { 'meta-site': '/creds/meta-site-token.txt' },
87
+ () => 'fine-grained-token',
88
+ );
89
+
90
+ expect(resolve('X-Mile')).toBe('default-token');
91
+ });
92
+
93
+ it('should read the token file only once per owner', () => {
94
+ let readCount = 0;
95
+ const resolve = createConsoleGithubTokenResolver(
96
+ 'default-token',
97
+ { 'meta-site': '/creds/meta-site-token.txt' },
98
+ () => {
99
+ readCount += 1;
100
+ return 'fine-grained-token';
101
+ },
102
+ );
103
+
104
+ resolve('meta-site');
105
+ resolve('meta-site');
106
+
107
+ expect(readCount).toBe(1);
108
+ });
109
+
110
+ it('should throw when the configured token file contains no token', () => {
111
+ const resolve = createConsoleGithubTokenResolver(
112
+ 'default-token',
113
+ { 'meta-site': '/creds/meta-site-token.txt' },
114
+ () => ' \n',
115
+ );
116
+
117
+ expect(() => resolve('meta-site')).toThrow(
118
+ 'The GitHub token file configured for repository owner "meta-site" contains no token: /creds/meta-site-token.txt',
119
+ );
120
+ });
121
+
122
+ it('should surface the read failure when the configured token file cannot be read', () => {
123
+ const resolve = createConsoleGithubTokenResolver(
124
+ 'default-token',
125
+ { 'meta-site': '/creds/meta-site-token.txt' },
126
+ () => {
127
+ throw new Error('ENOENT: no such file or directory');
128
+ },
129
+ );
130
+
131
+ expect(() => resolve('meta-site')).toThrow(
132
+ 'ENOENT: no such file or directory',
133
+ );
134
+ });
135
+ });
@@ -0,0 +1,55 @@
1
+ export type ConsoleGithubTokenResolver = (repositoryOwner: string) => string;
2
+
3
+ export type GithubTokenFileReader = (filePath: string) => string;
4
+
5
+ export const extractRepositoryOwner = (
6
+ issueOrPullRequestUrl: string,
7
+ ): string | null => {
8
+ const match = issueOrPullRequestUrl.match(
9
+ /^https:\/\/github\.com\/([A-Za-z0-9._-]+)\/[A-Za-z0-9._-]+\/(?:issues|pull)\/\d+/,
10
+ );
11
+ return match ? match[1] : null;
12
+ };
13
+
14
+ export const createConsoleIssueRepositoryResolver = <IssueRepositoryType>(
15
+ resolveGithubToken: ConsoleGithubTokenResolver,
16
+ buildIssueRepositoryForToken: (githubToken: string) => IssueRepositoryType,
17
+ ): ((issueOrPullRequestUrl: string) => IssueRepositoryType) => {
18
+ return (issueOrPullRequestUrl: string): IssueRepositoryType => {
19
+ const repositoryOwner = extractRepositoryOwner(issueOrPullRequestUrl);
20
+ if (repositoryOwner === null) {
21
+ throw new Error(
22
+ `The repository owner cannot be read from the operated url: ${issueOrPullRequestUrl}`,
23
+ );
24
+ }
25
+ return buildIssueRepositoryForToken(resolveGithubToken(repositoryOwner));
26
+ };
27
+ };
28
+
29
+ export const createConsoleGithubTokenResolver = (
30
+ defaultToken: string,
31
+ githubTokenFilePathByRepositoryOwner: Record<string, string> | null,
32
+ readTokenFile: GithubTokenFileReader,
33
+ ): ConsoleGithubTokenResolver => {
34
+ const resolvedTokenByRepositoryOwner = new Map<string, string>();
35
+ return (repositoryOwner: string): string => {
36
+ const alreadyResolved = resolvedTokenByRepositoryOwner.get(repositoryOwner);
37
+ if (alreadyResolved !== undefined) {
38
+ return alreadyResolved;
39
+ }
40
+ const filePath = githubTokenFilePathByRepositoryOwner
41
+ ? githubTokenFilePathByRepositoryOwner[repositoryOwner]
42
+ : undefined;
43
+ if (filePath === undefined) {
44
+ return defaultToken;
45
+ }
46
+ const token = readTokenFile(filePath).trim();
47
+ if (token.length === 0) {
48
+ throw new Error(
49
+ `The GitHub token file configured for repository owner "${repositoryOwner}" contains no token: ${filePath}`,
50
+ );
51
+ }
52
+ resolvedTokenByRepositoryOwner.set(repositoryOwner, token);
53
+ return token;
54
+ };
55
+ };
@@ -15,7 +15,7 @@ describe('findConsoleItemUrl', () => {
15
15
  });
16
16
 
17
17
  const writeList = (tab: string, items: unknown): void => {
18
- const dir = path.join(baseDir, 'umino', tab);
18
+ const dir = path.join(baseDir, 'acme', tab);
19
19
  fs.mkdirSync(dir, { recursive: true });
20
20
  fs.writeFileSync(path.join(dir, 'list.json'), JSON.stringify({ items }));
21
21
  };
@@ -23,14 +23,14 @@ describe('findConsoleItemUrl', () => {
23
23
  it('returns the stored url when the requested url is listed in a tab', () => {
24
24
  writeList('prs', [{ url: 'https://github.com/o/r/issues/1' }]);
25
25
  expect(
26
- findConsoleItemUrl(baseDir, 'umino', 'https://github.com/o/r/issues/1'),
26
+ findConsoleItemUrl(baseDir, 'acme', 'https://github.com/o/r/issues/1'),
27
27
  ).toBe('https://github.com/o/r/issues/1');
28
28
  });
29
29
 
30
30
  it('returns null when no tab lists the requested url', () => {
31
31
  writeList('prs', [{ url: 'https://github.com/o/r/issues/1' }]);
32
32
  expect(
33
- findConsoleItemUrl(baseDir, 'umino', 'https://github.com/o/r/issues/2'),
33
+ findConsoleItemUrl(baseDir, 'acme', 'https://github.com/o/r/issues/2'),
34
34
  ).toBeNull();
35
35
  });
36
36
 
@@ -41,12 +41,12 @@ describe('findConsoleItemUrl', () => {
41
41
  });
42
42
 
43
43
  it('ignores a tab file that is not valid json', () => {
44
- const dir = path.join(baseDir, 'umino', 'triage');
44
+ const dir = path.join(baseDir, 'acme', 'triage');
45
45
  fs.mkdirSync(dir, { recursive: true });
46
46
  fs.writeFileSync(path.join(dir, 'list.json'), 'not json');
47
47
  writeList('prs', [{ url: 'https://github.com/o/r/issues/1' }]);
48
48
  expect(
49
- findConsoleItemUrl(baseDir, 'umino', 'https://github.com/o/r/issues/1'),
49
+ findConsoleItemUrl(baseDir, 'acme', 'https://github.com/o/r/issues/1'),
50
50
  ).toBe('https://github.com/o/r/issues/1');
51
51
  });
52
52
 
@@ -57,7 +57,7 @@ describe('findConsoleItemUrl', () => {
57
57
  { url: 'https://github.com/o/r/pull/9' },
58
58
  ]);
59
59
  expect(
60
- findConsoleItemUrl(baseDir, 'umino', 'https://github.com/o/r/pull/9'),
60
+ findConsoleItemUrl(baseDir, 'acme', 'https://github.com/o/r/pull/9'),
61
61
  ).toBe('https://github.com/o/r/pull/9');
62
62
  });
63
63
  });