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.
- package/CHANGELOG.md +23 -9
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +29 -7
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +3 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js +41 -0
- package/bin/adapter/entry-points/console/consoleGithubTokenResolver.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleOperationApi.js +34 -16
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/ensureConsoleRunning.js +6 -1
- package/bin/adapter/entry-points/console/ensureConsoleRunning.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +3 -4
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/GraphqlProjectRepository.js +75 -4
- package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
- package/bin/adapter/repositories/utils.js +2 -4
- package/bin/adapter/repositories/utils.js.map +1 -1
- package/bin/domain/entities/ProjectFieldName.js +6 -0
- package/bin/domain/entities/ProjectFieldName.js.map +1 -0
- package/bin/domain/entities/RequiredProjectField.js +47 -0
- package/bin/domain/entities/RequiredProjectField.js.map +1 -0
- package/bin/domain/usecases/HandleScheduledEventUseCase.js +5 -1
- package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js +25 -0
- package/bin/domain/usecases/ProjectRequiredFieldCreateUseCase.js.map +1 -0
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js +6 -1
- package/bin/domain/usecases/SetupTowerDefenceProjectUseCase.js.map +1 -1
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js +17 -13
- package/bin/domain/usecases/dashboard/DashboardProjectCode.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +62 -2
- package/src/adapter/entry-points/cli/index.ts +58 -11
- package/src/adapter/entry-points/cli/projectConfig.ts +8 -0
- package/src/adapter/entry-points/console/consoleDataDelivery.test.ts +37 -39
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +33 -33
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.test.ts +135 -0
- package/src/adapter/entry-points/console/consoleGithubTokenResolver.ts +55 -0
- package/src/adapter/entry-points/console/consoleItemUrlLookup.test.ts +6 -6
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +92 -70
- package/src/adapter/entry-points/console/consoleOperationApi.ts +51 -44
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +35 -35
- package/src/adapter/entry-points/console/dashboardComposeService.test.ts +28 -28
- package/src/adapter/entry-points/console/dashboardEmitterComposerKey.test.ts +26 -20
- package/src/adapter/entry-points/console/ensureConsoleRunning.test.ts +19 -2
- package/src/adapter/entry-points/console/ensureConsoleRunning.ts +8 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleScenario.spec.ts +1 -1
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.stories.tsx +2 -2
- package/src/adapter/entry-points/console/ui/src/features/console/components/layout/ConsoleTabList.test.tsx +3 -3
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleNavigation.test.ts +19 -31
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +24 -32
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +6 -6
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +4 -4
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +8 -8
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +5 -5
- package/src/adapter/entry-points/console/ui/src/features/console/logic/overlay.test.ts +1 -1
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +13 -13
- package/src/adapter/entry-points/console/webServer.test.ts +55 -52
- package/src/adapter/entry-points/console/webServer.ts +5 -4
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.test.ts +2 -2
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +4 -0
- package/src/adapter/entry-points/handlers/dashboardRowWriter.test.ts +5 -5
- package/src/adapter/entry-points/handlers/inTmuxByHumanDataWriter.test.ts +5 -5
- package/src/adapter/repositories/GraphqlProjectRepository.ts +110 -4
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +1 -1
- package/src/adapter/repositories/utils.ts +3 -3
- package/src/domain/entities/ProjectFieldName.ts +2 -0
- package/src/domain/entities/RequiredProjectField.ts +64 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +4 -0
- package/src/domain/usecases/HandleScheduledEventUseCase.ts +5 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +3 -3
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.test.ts +159 -0
- package/src/domain/usecases/ProjectRequiredFieldCreateUseCase.ts +32 -0
- package/src/domain/usecases/SetWorkflowManagementIssueToStoryUseCase.test.ts +19 -19
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.test.ts +24 -14
- package/src/domain/usecases/SetupTowerDefenceProjectUseCase.ts +10 -1
- package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +6 -0
- package/src/domain/usecases/dashboard/ComposeDashboardUseCase.test.ts +28 -28
- package/src/domain/usecases/dashboard/DashboardProjectCode.test.ts +21 -21
- package/src/domain/usecases/dashboard/DashboardProjectCode.ts +21 -15
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts +6 -0
- package/types/adapter/entry-points/console/consoleGithubTokenResolver.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts +1 -1
- package/types/adapter/entry-points/console/ensureConsoleRunning.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -2
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts +4 -1
- package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
- package/types/adapter/repositories/utils.d.ts.map +1 -1
- package/types/domain/entities/ProjectFieldName.d.ts +2 -0
- package/types/domain/entities/ProjectFieldName.d.ts.map +1 -0
- package/types/domain/entities/RequiredProjectField.d.ts +13 -0
- package/types/domain/entities/RequiredProjectField.d.ts.map +1 -0
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +3 -1
- package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts +10 -0
- package/types/domain/usecases/ProjectRequiredFieldCreateUseCase.d.ts.map +1 -0
- package/types/domain/usecases/SetupTowerDefenceProjectUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +3 -0
- package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
- package/types/domain/usecases/dashboard/DashboardProjectCode.d.ts +2 -2
- 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/
|
|
12
|
+
expect(parseConsoleDataRoute('/projects/acme/prs/list.json')).toEqual({
|
|
13
13
|
kind: 'list',
|
|
14
|
-
pjcode: '
|
|
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/
|
|
22
|
-
).toEqual({ kind: 'list', pjcode: '
|
|
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/
|
|
28
|
-
).toEqual({ kind: 'list', pjcode: '
|
|
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/
|
|
33
|
+
parseConsoleDataRoute('/projects/acme/triage/detail/123.json'),
|
|
34
34
|
).toEqual({
|
|
35
35
|
kind: 'detail',
|
|
36
|
-
pjcode: '
|
|
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/
|
|
44
|
+
parseConsoleDataRoute('/projects/acme/in-tmux-by-human/list.json'),
|
|
45
45
|
).toEqual({
|
|
46
46
|
kind: 'in-tmux',
|
|
47
|
-
pjcode: '
|
|
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/
|
|
54
|
+
parseConsoleDataRoute('/projects/acme/in-tmux-by-human/sub/data.json'),
|
|
55
55
|
).toEqual({
|
|
56
56
|
kind: 'in-tmux',
|
|
57
|
-
pjcode: '
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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: '
|
|
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('
|
|
118
|
-
pjcode: '
|
|
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, '
|
|
122
|
+
recordDoneProjectItemId(baseDir, 'acme', 'prs', 'PVTI_2');
|
|
125
123
|
const response = buildConsoleDataResponse(baseDir, {
|
|
126
124
|
kind: 'list',
|
|
127
|
-
pjcode: '
|
|
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: '
|
|
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('
|
|
140
|
-
pjcode: '
|
|
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, '
|
|
144
|
+
recordDoneProjectItemId(baseDir, 'acme', 'workflow-blocker', 'PVTI_2');
|
|
147
145
|
const response = buildConsoleDataResponse(baseDir, {
|
|
148
146
|
kind: 'list',
|
|
149
|
-
pjcode: '
|
|
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: '
|
|
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('
|
|
159
|
+
writeJson('acme/prs/list.json', { pjcode: 'acme' });
|
|
162
160
|
const response = buildConsoleDataResponse(baseDir, {
|
|
163
161
|
kind: 'list',
|
|
164
|
-
pjcode: '
|
|
162
|
+
pjcode: 'acme',
|
|
165
163
|
tab: 'prs',
|
|
166
164
|
});
|
|
167
165
|
expect(response.statusCode).toBe(200);
|
|
168
|
-
expect(JSON.parse(response.body)).toEqual({ pjcode: '
|
|
166
|
+
expect(JSON.parse(response.body)).toEqual({ pjcode: 'acme' });
|
|
169
167
|
});
|
|
170
168
|
|
|
171
169
|
it('serves a detail file without exclusion', () => {
|
|
172
|
-
writeJson('
|
|
170
|
+
writeJson('acme/triage/detail/123.json', { number: 123 });
|
|
173
171
|
const response = buildConsoleDataResponse(baseDir, {
|
|
174
172
|
kind: 'detail',
|
|
175
|
-
pjcode: '
|
|
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: '
|
|
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('
|
|
192
|
+
writeJson('acme/in-tmux-by-human/list.json', { items: [] });
|
|
195
193
|
const response = buildConsoleDataResponse(baseDir, {
|
|
196
194
|
kind: 'in-tmux',
|
|
197
|
-
pjcode: '
|
|
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: '
|
|
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, '
|
|
29
|
-
path.join(baseDir, '
|
|
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, '
|
|
36
|
+
expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it('returns the persisted project item ids', () => {
|
|
40
|
-
recordDoneProjectItemId(baseDir, '
|
|
41
|
-
recordDoneProjectItemId(baseDir, '
|
|
42
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
59
|
+
expect(readDoneProjectItemIds(baseDir, 'acme', 'prs')).toEqual([]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('filters out non-string entries', () => {
|
|
63
|
-
const filePath = doneFilePathForTab(baseDir, '
|
|
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, '
|
|
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, '
|
|
78
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
83
|
-
recordDoneProjectItemId(baseDir, '
|
|
84
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
92
|
+
recordDoneProjectItemIdAcrossTabs(baseDir, 'acme', 'PVTI_99');
|
|
93
93
|
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
94
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
106
|
+
recordDoneProjectItemIdAcrossTabs(baseDir, 'acme', 'PVTI_7');
|
|
107
107
|
expect(
|
|
108
|
-
readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
116
|
-
recordDoneProjectItemId(baseDir, '
|
|
117
|
-
recordDoneProjectItemId(baseDir, '
|
|
118
|
-
resetDoneProjectItemIds(baseDir, '
|
|
119
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
124
|
-
const filePath = doneFilePathForTab(baseDir, '
|
|
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, '
|
|
131
|
-
const filePath = doneFilePathForTab(baseDir, '
|
|
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, '
|
|
140
|
+
recordDoneProjectItemId(baseDir, 'acme', tab, 'PVTI_1');
|
|
141
141
|
}
|
|
142
|
-
resetDoneProjectItemIdsAcrossTabs(baseDir, '
|
|
142
|
+
resetDoneProjectItemIdsAcrossTabs(baseDir, 'acme');
|
|
143
143
|
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
144
|
-
expect(readDoneProjectItemIds(baseDir, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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, '
|
|
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
|
});
|