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
|
@@ -22,8 +22,12 @@ export type ConsoleProjectResolver = (
|
|
|
22
22
|
|
|
23
23
|
export type ConsolePjcodeValidator = (pjcode: string) => boolean;
|
|
24
24
|
|
|
25
|
+
export type ConsoleIssueRepositoryResolver = (
|
|
26
|
+
issueOrPullRequestUrl: string,
|
|
27
|
+
) => IssueRepository;
|
|
28
|
+
|
|
25
29
|
export type ConsoleOperationContext = {
|
|
26
|
-
|
|
30
|
+
resolveIssueRepository: ConsoleIssueRepositoryResolver;
|
|
27
31
|
resolveProject: ConsoleProjectResolver;
|
|
28
32
|
isPjcodeConfigured: ConsolePjcodeValidator;
|
|
29
33
|
consoleDataOutputDir: string | null;
|
|
@@ -210,9 +214,11 @@ export const handleReview = async (
|
|
|
210
214
|
if (typeof pjcodeResult !== 'string') {
|
|
211
215
|
return pjcodeResult;
|
|
212
216
|
}
|
|
213
|
-
await context.
|
|
217
|
+
await context.resolveIssueRepository(prUrl).closePullRequest(prUrl);
|
|
214
218
|
if (isNonEmptyString(body.commentBody)) {
|
|
215
|
-
await context
|
|
219
|
+
await context
|
|
220
|
+
.resolveIssueRepository(prUrl)
|
|
221
|
+
.createCommentByUrl(prUrl, body.commentBody);
|
|
216
222
|
}
|
|
217
223
|
recordDone(context, pjcodeResult, projectItemId);
|
|
218
224
|
return ok();
|
|
@@ -225,9 +231,9 @@ export const handleReview = async (
|
|
|
225
231
|
const { project, pjcode } = binding;
|
|
226
232
|
|
|
227
233
|
if (action === 'approve') {
|
|
228
|
-
await context.
|
|
234
|
+
await context.resolveIssueRepository(prUrl).approvePullRequest(prUrl);
|
|
229
235
|
const failure = await updateStatusByName(
|
|
230
|
-
context.
|
|
236
|
+
context.resolveIssueRepository(prUrl),
|
|
231
237
|
project,
|
|
232
238
|
prUrl,
|
|
233
239
|
projectItemId,
|
|
@@ -254,14 +260,16 @@ export const handleReview = async (
|
|
|
254
260
|
isReviewCommentSide(body.side)
|
|
255
261
|
? { line: body.line, side: body.side }
|
|
256
262
|
: null;
|
|
257
|
-
await context
|
|
258
|
-
prUrl
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
await context
|
|
264
|
+
.resolveIssueRepository(prUrl)
|
|
265
|
+
.requestChangesWithInlineComment(
|
|
266
|
+
prUrl,
|
|
267
|
+
changedFilePath,
|
|
268
|
+
commentBody,
|
|
269
|
+
inlineCommentLocation,
|
|
270
|
+
);
|
|
263
271
|
const failure = await updateStatusByName(
|
|
264
|
-
context.
|
|
272
|
+
context.resolveIssueRepository(prUrl),
|
|
265
273
|
project,
|
|
266
274
|
prUrl,
|
|
267
275
|
projectItemId,
|
|
@@ -300,18 +308,19 @@ export const handleTriage = async (
|
|
|
300
308
|
return pjcodeResult;
|
|
301
309
|
}
|
|
302
310
|
if (isNonEmptyString(body.commentBody)) {
|
|
303
|
-
await context
|
|
304
|
-
issueUrl
|
|
305
|
-
body.commentBody
|
|
306
|
-
);
|
|
311
|
+
await context
|
|
312
|
+
.resolveIssueRepository(issueUrl)
|
|
313
|
+
.createCommentByUrl(issueUrl, body.commentBody);
|
|
307
314
|
}
|
|
308
315
|
if (isPullRequestUrl(issueUrl)) {
|
|
309
|
-
await context.
|
|
316
|
+
await context.resolveIssueRepository(issueUrl).closePullRequest(issueUrl);
|
|
310
317
|
} else {
|
|
311
|
-
await context
|
|
312
|
-
issueUrl
|
|
313
|
-
|
|
314
|
-
|
|
318
|
+
await context
|
|
319
|
+
.resolveIssueRepository(issueUrl)
|
|
320
|
+
.closeIssueByUrl(
|
|
321
|
+
issueUrl,
|
|
322
|
+
action === 'close_not_planned' ? 'not_planned' : 'completed',
|
|
323
|
+
);
|
|
315
324
|
}
|
|
316
325
|
recordDone(context, pjcodeResult, projectItemId);
|
|
317
326
|
return ok();
|
|
@@ -329,7 +338,7 @@ export const handleTriage = async (
|
|
|
329
338
|
return badRequest('statusName is required for set_status');
|
|
330
339
|
}
|
|
331
340
|
const failure = await updateStatusByName(
|
|
332
|
-
context.
|
|
341
|
+
context.resolveIssueRepository(issueUrl),
|
|
333
342
|
project,
|
|
334
343
|
issueUrl,
|
|
335
344
|
projectItemId,
|
|
@@ -350,11 +359,13 @@ export const handleTriage = async (
|
|
|
350
359
|
if (project.story === null) {
|
|
351
360
|
return badRequest('project does not have a story field');
|
|
352
361
|
}
|
|
353
|
-
await context
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
362
|
+
await context
|
|
363
|
+
.resolveIssueRepository(issueUrl)
|
|
364
|
+
.updateStory(
|
|
365
|
+
{ ...project, story: project.story },
|
|
366
|
+
projectItemReference(issueUrl, projectItemId),
|
|
367
|
+
storyOptionId,
|
|
368
|
+
);
|
|
358
369
|
recordDone(context, pjcode, projectItemId);
|
|
359
370
|
return ok();
|
|
360
371
|
}
|
|
@@ -362,12 +373,9 @@ export const handleTriage = async (
|
|
|
362
373
|
if (action === 'snooze_1day' || action === 'snooze_1week') {
|
|
363
374
|
const days = action === 'snooze_1day' ? 1 : 7;
|
|
364
375
|
const target = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
365
|
-
await context
|
|
366
|
-
issueUrl
|
|
367
|
-
project,
|
|
368
|
-
target,
|
|
369
|
-
projectItemId,
|
|
370
|
-
);
|
|
376
|
+
await context
|
|
377
|
+
.resolveIssueRepository(issueUrl)
|
|
378
|
+
.updateNextActionDate(issueUrl, project, target, projectItemId);
|
|
371
379
|
recordDone(context, pjcode, projectItemId);
|
|
372
380
|
return ok();
|
|
373
381
|
}
|
|
@@ -387,9 +395,12 @@ export const handleComment = async (
|
|
|
387
395
|
if (!isNonEmptyString(commentBody)) {
|
|
388
396
|
return badRequest('body is required');
|
|
389
397
|
}
|
|
390
|
-
await context
|
|
391
|
-
|
|
392
|
-
|
|
398
|
+
await context
|
|
399
|
+
.resolveIssueRepository(url)
|
|
400
|
+
.createCommentByUrl(url, commentBody);
|
|
401
|
+
const comments = await context
|
|
402
|
+
.resolveIssueRepository(url)
|
|
403
|
+
.getIssueOrPullRequestComments(url);
|
|
393
404
|
const posted = comments[comments.length - 1] ?? null;
|
|
394
405
|
return {
|
|
395
406
|
statusCode: 200,
|
|
@@ -480,13 +491,9 @@ export const handleReviewComment = async (
|
|
|
480
491
|
return badRequest('body is required');
|
|
481
492
|
}
|
|
482
493
|
try {
|
|
483
|
-
await context
|
|
484
|
-
url
|
|
485
|
-
path,
|
|
486
|
-
line,
|
|
487
|
-
side,
|
|
488
|
-
commentBody,
|
|
489
|
-
);
|
|
494
|
+
await context
|
|
495
|
+
.resolveIssueRepository(url)
|
|
496
|
+
.createPullRequestReviewComment(url, path, line, side, commentBody);
|
|
490
497
|
} catch (error) {
|
|
491
498
|
return badGateway(error instanceof Error ? error.message : 'unknown error');
|
|
492
499
|
}
|
|
@@ -518,7 +525,7 @@ export const handleIntmux = async (
|
|
|
518
525
|
}
|
|
519
526
|
const { project, pjcode } = binding;
|
|
520
527
|
const failure = await updateStatusByName(
|
|
521
|
-
context.
|
|
528
|
+
context.resolveIssueRepository(issueUrl),
|
|
522
529
|
project,
|
|
523
530
|
issueUrl,
|
|
524
531
|
projectItemId,
|
|
@@ -9,33 +9,33 @@ import {
|
|
|
9
9
|
describe('buildPjcodeToProjectUrl', () => {
|
|
10
10
|
it('adds the default pjcode entry when it is not already present', () => {
|
|
11
11
|
const mapping = buildPjcodeToProjectUrl(
|
|
12
|
-
'
|
|
13
|
-
'https://github.com/orgs/
|
|
14
|
-
{
|
|
12
|
+
'acme',
|
|
13
|
+
'https://github.com/orgs/acme/projects/1',
|
|
14
|
+
{ globex: 'https://github.com/orgs/globex/projects/2' },
|
|
15
15
|
);
|
|
16
16
|
expect(mapping).toEqual({
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
acme: 'https://github.com/orgs/acme/projects/1',
|
|
18
|
+
globex: 'https://github.com/orgs/globex/projects/2',
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('keeps an explicit default pjcode entry from consoleProjects', () => {
|
|
23
23
|
const mapping = buildPjcodeToProjectUrl(
|
|
24
|
-
'
|
|
25
|
-
'https://github.com/orgs/
|
|
26
|
-
{
|
|
24
|
+
'acme',
|
|
25
|
+
'https://github.com/orgs/acme/projects/1',
|
|
26
|
+
{ acme: 'https://github.com/orgs/acme/projects/9' },
|
|
27
27
|
);
|
|
28
|
-
expect(mapping.
|
|
28
|
+
expect(mapping.acme).toBe('https://github.com/orgs/acme/projects/9');
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it('uses only the default entry when no consoleProjects mapping is configured', () => {
|
|
32
32
|
const mapping = buildPjcodeToProjectUrl(
|
|
33
|
-
'
|
|
34
|
-
'https://github.com/orgs/
|
|
33
|
+
'acme',
|
|
34
|
+
'https://github.com/orgs/acme/projects/1',
|
|
35
35
|
null,
|
|
36
36
|
);
|
|
37
37
|
expect(mapping).toEqual({
|
|
38
|
-
|
|
38
|
+
acme: 'https://github.com/orgs/acme/projects/1',
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
});
|
|
@@ -43,44 +43,44 @@ describe('buildPjcodeToProjectUrl', () => {
|
|
|
43
43
|
describe('createPjcodeConfigChecker', () => {
|
|
44
44
|
it('reports true only for a configured pjcode without loading any project', () => {
|
|
45
45
|
const isConfigured = createPjcodeConfigChecker({
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
acme: 'https://github.com/orgs/acme/projects/1',
|
|
47
|
+
globex: 'https://github.com/orgs/globex/projects/2',
|
|
48
48
|
});
|
|
49
|
-
expect(isConfigured('
|
|
50
|
-
expect(isConfigured('
|
|
49
|
+
expect(isConfigured('acme')).toBe(true);
|
|
50
|
+
expect(isConfigured('globex')).toBe(true);
|
|
51
51
|
expect(isConfigured('unknown')).toBe(false);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
describe('createConsoleProjectResolver', () => {
|
|
56
|
-
const
|
|
57
|
-
const
|
|
56
|
+
const acmeProject: Project = { ...mock<Project>(), id: 'PVT_acme' };
|
|
57
|
+
const globexProject: Project = { ...mock<Project>(), id: 'PVT_globex' };
|
|
58
58
|
|
|
59
59
|
it('resolves a known pjcode to its loaded project', async () => {
|
|
60
60
|
const loadProject = jest.fn(async (url: string) =>
|
|
61
|
-
url.includes('
|
|
61
|
+
url.includes('acme') ? acmeProject : globexProject,
|
|
62
62
|
);
|
|
63
63
|
const resolver = createConsoleProjectResolver(
|
|
64
64
|
{
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
acme: 'https://github.com/orgs/acme/projects/1',
|
|
66
|
+
globex: 'https://github.com/orgs/globex/projects/2',
|
|
67
67
|
},
|
|
68
68
|
loadProject,
|
|
69
69
|
);
|
|
70
|
-
await expect(resolver('
|
|
71
|
-
pjcode: '
|
|
72
|
-
project:
|
|
70
|
+
await expect(resolver('acme')).resolves.toEqual({
|
|
71
|
+
pjcode: 'acme',
|
|
72
|
+
project: acmeProject,
|
|
73
73
|
});
|
|
74
|
-
await expect(resolver('
|
|
75
|
-
pjcode: '
|
|
76
|
-
project:
|
|
74
|
+
await expect(resolver('globex')).resolves.toEqual({
|
|
75
|
+
pjcode: 'globex',
|
|
76
|
+
project: globexProject,
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
it('returns null for a pjcode that has no configured project url', async () => {
|
|
81
|
-
const loadProject = jest.fn(async () =>
|
|
81
|
+
const loadProject = jest.fn(async () => acmeProject);
|
|
82
82
|
const resolver = createConsoleProjectResolver(
|
|
83
|
-
{
|
|
83
|
+
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
84
84
|
loadProject,
|
|
85
85
|
);
|
|
86
86
|
await expect(resolver('unknown')).resolves.toBeNull();
|
|
@@ -90,20 +90,20 @@ describe('createConsoleProjectResolver', () => {
|
|
|
90
90
|
it('returns null when the project fails to load', async () => {
|
|
91
91
|
const loadProject = jest.fn(async () => null);
|
|
92
92
|
const resolver = createConsoleProjectResolver(
|
|
93
|
-
{
|
|
93
|
+
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
94
94
|
loadProject,
|
|
95
95
|
);
|
|
96
|
-
await expect(resolver('
|
|
96
|
+
await expect(resolver('acme')).resolves.toBeNull();
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
it('loads each project at most once and serves later calls from cache', async () => {
|
|
100
|
-
const loadProject = jest.fn(async () =>
|
|
100
|
+
const loadProject = jest.fn(async () => acmeProject);
|
|
101
101
|
const resolver = createConsoleProjectResolver(
|
|
102
|
-
{
|
|
102
|
+
{ acme: 'https://github.com/orgs/acme/projects/1' },
|
|
103
103
|
loadProject,
|
|
104
104
|
);
|
|
105
|
-
await resolver('
|
|
106
|
-
await resolver('
|
|
105
|
+
await resolver('acme');
|
|
106
|
+
await resolver('acme');
|
|
107
107
|
expect(loadProject).toHaveBeenCalledTimes(1);
|
|
108
108
|
});
|
|
109
109
|
});
|
|
@@ -22,8 +22,8 @@ describe('buildComposeDashboardInput', () => {
|
|
|
22
22
|
it('reads every requested project code in order and yields null for absent files', () => {
|
|
23
23
|
const dataDir = makeDataDir();
|
|
24
24
|
try {
|
|
25
|
-
writeProject(dataDir, '
|
|
26
|
-
pjcode: '
|
|
25
|
+
writeProject(dataDir, 'acme', {
|
|
26
|
+
pjcode: 'acme',
|
|
27
27
|
capturedAt: 'x',
|
|
28
28
|
unread: 3,
|
|
29
29
|
todo: 1,
|
|
@@ -36,11 +36,11 @@ describe('buildComposeDashboardInput', () => {
|
|
|
36
36
|
});
|
|
37
37
|
const input = buildComposeDashboardInput({
|
|
38
38
|
dashboardDataDir: dataDir,
|
|
39
|
-
projectNames: ['
|
|
39
|
+
projectNames: ['acme', 'initech'],
|
|
40
40
|
});
|
|
41
41
|
expect(input.projects).toEqual([
|
|
42
42
|
{
|
|
43
|
-
code: '
|
|
43
|
+
code: 'ac',
|
|
44
44
|
row: {
|
|
45
45
|
unread: 3,
|
|
46
46
|
todo: 1,
|
|
@@ -52,7 +52,7 @@ describe('buildComposeDashboardInput', () => {
|
|
|
52
52
|
blocker: 0,
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
|
-
{ code: '
|
|
55
|
+
{ code: 'in', row: null },
|
|
56
56
|
]);
|
|
57
57
|
} finally {
|
|
58
58
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
@@ -62,10 +62,10 @@ describe('buildComposeDashboardInput', () => {
|
|
|
62
62
|
it('treats a project file with a missing field as an absent row', () => {
|
|
63
63
|
const dataDir = makeDataDir();
|
|
64
64
|
try {
|
|
65
|
-
writeProject(dataDir, '
|
|
65
|
+
writeProject(dataDir, 'acme', { unread: 1, todo: 1 });
|
|
66
66
|
const input = buildComposeDashboardInput({
|
|
67
67
|
dashboardDataDir: dataDir,
|
|
68
|
-
projectNames: ['
|
|
68
|
+
projectNames: ['acme'],
|
|
69
69
|
});
|
|
70
70
|
expect(input.projects[0].row).toBeNull();
|
|
71
71
|
} finally {
|
|
@@ -77,13 +77,10 @@ describe('buildComposeDashboardInput', () => {
|
|
|
77
77
|
const dataDir = makeDataDir();
|
|
78
78
|
try {
|
|
79
79
|
fs.mkdirSync(path.join(dataDir, 'projects'), { recursive: true });
|
|
80
|
-
fs.writeFileSync(
|
|
81
|
-
path.join(dataDir, 'projects', 'umino.json'),
|
|
82
|
-
'not json',
|
|
83
|
-
);
|
|
80
|
+
fs.writeFileSync(path.join(dataDir, 'projects', 'acme.json'), 'not json');
|
|
84
81
|
const input = buildComposeDashboardInput({
|
|
85
82
|
dashboardDataDir: dataDir,
|
|
86
|
-
projectNames: ['
|
|
83
|
+
projectNames: ['acme'],
|
|
87
84
|
});
|
|
88
85
|
expect(input.projects[0].row).toBeNull();
|
|
89
86
|
} finally {
|
|
@@ -365,8 +362,8 @@ describe('composeDashboardText', () => {
|
|
|
365
362
|
it('composes the full byte-identical dashboard text from the data files', () => {
|
|
366
363
|
const dataDir = makeDataDir();
|
|
367
364
|
try {
|
|
368
|
-
writeProject(dataDir, '
|
|
369
|
-
pjcode: '
|
|
365
|
+
writeProject(dataDir, 'acme', {
|
|
366
|
+
pjcode: 'acme',
|
|
370
367
|
capturedAt: 'x',
|
|
371
368
|
unread: 3,
|
|
372
369
|
todo: 1,
|
|
@@ -409,14 +406,14 @@ describe('composeDashboardText', () => {
|
|
|
409
406
|
expect(
|
|
410
407
|
composeDashboardText({
|
|
411
408
|
dashboardDataDir: dataDir,
|
|
412
|
-
projectNames: ['
|
|
409
|
+
projectNames: ['acme', 'initech'],
|
|
413
410
|
}),
|
|
414
411
|
).toBe(
|
|
415
412
|
'<tt>M55% C62% D89% cy14</tt><br>\n' +
|
|
416
413
|
'<tt>LA 16 23 40</tt><br>\n' +
|
|
417
414
|
'<tt>pj unr tdo aqc fal prp aws dep</tt><br>\n' +
|
|
418
|
-
'<tt>🟢
|
|
419
|
-
'<tt>
|
|
415
|
+
'<tt>🟢ac 3 1 2 0 0 4 1</tt><br>\n' +
|
|
416
|
+
'<tt> in -- -- -- -- -- -- --</tt><br>\n' +
|
|
420
417
|
'<tt></tt><br>\n' +
|
|
421
418
|
'<tt>🟢alice 10% 0d01h00 12% 5d00h00 2 1</tt><br>\n',
|
|
422
419
|
);
|
|
@@ -446,7 +443,7 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
446
443
|
};
|
|
447
444
|
|
|
448
445
|
const minimalProject = {
|
|
449
|
-
pjcode: '
|
|
446
|
+
pjcode: 'acme',
|
|
450
447
|
capturedAt: 'x',
|
|
451
448
|
unread: 0,
|
|
452
449
|
todo: 0,
|
|
@@ -462,12 +459,15 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
462
459
|
const dataDir = makeDataDir();
|
|
463
460
|
try {
|
|
464
461
|
writeMachineAndToken(dataDir);
|
|
465
|
-
writeProject(dataDir, '
|
|
466
|
-
writeProject(dataDir, '
|
|
462
|
+
writeProject(dataDir, 'acme', minimalProject);
|
|
463
|
+
writeProject(dataDir, 'initech', {
|
|
464
|
+
...minimalProject,
|
|
465
|
+
pjcode: 'initech',
|
|
466
|
+
});
|
|
467
467
|
expect(
|
|
468
468
|
dashboardComposeFilesPresent({
|
|
469
469
|
dashboardDataDir: dataDir,
|
|
470
|
-
projectNames: ['
|
|
470
|
+
projectNames: ['acme', 'initech'],
|
|
471
471
|
}),
|
|
472
472
|
).toBe(true);
|
|
473
473
|
} finally {
|
|
@@ -481,7 +481,7 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
481
481
|
expect(
|
|
482
482
|
dashboardComposeFilesPresent({
|
|
483
483
|
dashboardDataDir: dataDir,
|
|
484
|
-
projectNames: ['
|
|
484
|
+
projectNames: ['acme'],
|
|
485
485
|
}),
|
|
486
486
|
).toBe(false);
|
|
487
487
|
} finally {
|
|
@@ -493,11 +493,11 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
493
493
|
const dataDir = makeDataDir();
|
|
494
494
|
try {
|
|
495
495
|
writeMachineAndToken(dataDir);
|
|
496
|
-
writeProject(dataDir, '
|
|
496
|
+
writeProject(dataDir, 'acme', minimalProject);
|
|
497
497
|
expect(
|
|
498
498
|
dashboardComposeFilesPresent({
|
|
499
499
|
dashboardDataDir: dataDir,
|
|
500
|
-
projectNames: ['
|
|
500
|
+
projectNames: ['acme', 'initech'],
|
|
501
501
|
}),
|
|
502
502
|
).toBe(false);
|
|
503
503
|
} finally {
|
|
@@ -512,11 +512,11 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
512
512
|
path.join(dataDir, 'token-status.json'),
|
|
513
513
|
JSON.stringify({ tokens: [], capturedAt: 'x' }),
|
|
514
514
|
);
|
|
515
|
-
writeProject(dataDir, '
|
|
515
|
+
writeProject(dataDir, 'acme', minimalProject);
|
|
516
516
|
expect(
|
|
517
517
|
dashboardComposeFilesPresent({
|
|
518
518
|
dashboardDataDir: dataDir,
|
|
519
|
-
projectNames: ['
|
|
519
|
+
projectNames: ['acme'],
|
|
520
520
|
}),
|
|
521
521
|
).toBe(false);
|
|
522
522
|
} finally {
|
|
@@ -538,11 +538,11 @@ describe('dashboardComposeFilesPresent', () => {
|
|
|
538
538
|
capturedAt: 'x',
|
|
539
539
|
}),
|
|
540
540
|
);
|
|
541
|
-
writeProject(dataDir, '
|
|
541
|
+
writeProject(dataDir, 'acme', minimalProject);
|
|
542
542
|
expect(
|
|
543
543
|
dashboardComposeFilesPresent({
|
|
544
544
|
dashboardDataDir: dataDir,
|
|
545
|
-
projectNames: ['
|
|
545
|
+
projectNames: ['acme'],
|
|
546
546
|
}),
|
|
547
547
|
).toBe(false);
|
|
548
548
|
} finally {
|
|
@@ -2,19 +2,23 @@ import * as fs from 'fs';
|
|
|
2
2
|
import * as os from 'os';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { Issue } from '../../../domain/entities/Issue';
|
|
5
|
-
import {
|
|
6
|
-
DASHBOARD_PROJECT_NAMES,
|
|
7
|
-
toDashboardDisplayLabel,
|
|
8
|
-
} from '../../../domain/usecases/dashboard/DashboardProjectCode';
|
|
5
|
+
import { toDashboardDisplayLabel } from '../../../domain/usecases/dashboard/DashboardProjectCode';
|
|
9
6
|
import { writeDashboardRow } from '../handlers/dashboardRowWriter';
|
|
10
7
|
import {
|
|
11
8
|
composeDashboardText,
|
|
12
9
|
dashboardComposeFilesPresent,
|
|
13
10
|
} from './dashboardComposeService';
|
|
14
|
-
import { DEFAULT_DASHBOARD_PROJECT_NAMES } from './webServer';
|
|
15
11
|
|
|
16
12
|
const ASSIGNEE = 'HiromiShikata';
|
|
17
13
|
|
|
14
|
+
const CONFIGURED_PROJECT_NAMES = [
|
|
15
|
+
'acme',
|
|
16
|
+
'globex',
|
|
17
|
+
'initech',
|
|
18
|
+
'umbrella',
|
|
19
|
+
'soylent',
|
|
20
|
+
];
|
|
21
|
+
|
|
18
22
|
let issueCounter = 0;
|
|
19
23
|
const makeIssue = (overrides: Partial<Issue>): Issue => {
|
|
20
24
|
issueCounter += 1;
|
|
@@ -59,7 +63,7 @@ describe('dashboard emitter filename matches composer lookup key', () => {
|
|
|
59
63
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
60
64
|
});
|
|
61
65
|
|
|
62
|
-
it.each(
|
|
66
|
+
it.each(CONFIGURED_PROJECT_NAMES)(
|
|
63
67
|
'emits projects/%s.json under the full project name the composer reads',
|
|
64
68
|
(projectName) => {
|
|
65
69
|
writeDashboardRow({
|
|
@@ -80,7 +84,7 @@ describe('dashboard emitter filename matches composer lookup key', () => {
|
|
|
80
84
|
);
|
|
81
85
|
|
|
82
86
|
it('detects compose files present when every project name file exists', () => {
|
|
83
|
-
for (const projectName of
|
|
87
|
+
for (const projectName of CONFIGURED_PROJECT_NAMES) {
|
|
84
88
|
writeDashboardRow({
|
|
85
89
|
dashboardDataDir: dir,
|
|
86
90
|
pjcode: projectName,
|
|
@@ -107,20 +111,20 @@ describe('dashboard emitter filename matches composer lookup key', () => {
|
|
|
107
111
|
expect(
|
|
108
112
|
dashboardComposeFilesPresent({
|
|
109
113
|
dashboardDataDir: dir,
|
|
110
|
-
projectNames:
|
|
114
|
+
projectNames: CONFIGURED_PROJECT_NAMES,
|
|
111
115
|
}),
|
|
112
116
|
).toBe(true);
|
|
113
117
|
});
|
|
114
118
|
|
|
115
119
|
it('composes the full grid with no fallback row and shows the 2-char display labels', () => {
|
|
116
120
|
const issuesByName: Record<string, Issue[]> = {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
acme: [makeIssue({ status: 'Unread' }), makeIssue({ status: 'Unread' })],
|
|
122
|
+
globex: [makeIssue({ status: 'Awaiting Quality Check' })],
|
|
123
|
+
initech: [makeIssue({ status: 'Awaiting Workspace' })],
|
|
124
|
+
umbrella: [makeIssue({ status: 'Todo by human' })],
|
|
125
|
+
soylent: [makeIssue({ status: 'Preparation' })],
|
|
122
126
|
};
|
|
123
|
-
for (const projectName of
|
|
127
|
+
for (const projectName of CONFIGURED_PROJECT_NAMES) {
|
|
124
128
|
writeDashboardRow({
|
|
125
129
|
dashboardDataDir: dir,
|
|
126
130
|
pjcode: projectName,
|
|
@@ -146,7 +150,7 @@ describe('dashboard emitter filename matches composer lookup key', () => {
|
|
|
146
150
|
|
|
147
151
|
const composed = composeDashboardText({
|
|
148
152
|
dashboardDataDir: dir,
|
|
149
|
-
projectNames:
|
|
153
|
+
projectNames: CONFIGURED_PROJECT_NAMES,
|
|
150
154
|
});
|
|
151
155
|
|
|
152
156
|
const wrap = (line: string): string =>
|
|
@@ -157,18 +161,20 @@ describe('dashboard emitter filename matches composer lookup key', () => {
|
|
|
157
161
|
wrap('LA 16 23 40'),
|
|
158
162
|
wrap('pj unr tdo aqc fal prp aws dep'),
|
|
159
163
|
wrap(
|
|
160
|
-
`🟢${toDashboardDisplayLabel('
|
|
164
|
+
`🟢${toDashboardDisplayLabel('acme')} 2 0 0 0 0 0 0`,
|
|
165
|
+
),
|
|
166
|
+
wrap(
|
|
167
|
+
`🟢${toDashboardDisplayLabel('globex')} 0 0 1 0 0 0 0`,
|
|
161
168
|
),
|
|
162
169
|
wrap(
|
|
163
|
-
`🟢${toDashboardDisplayLabel('
|
|
170
|
+
`🟢${toDashboardDisplayLabel('initech')} 0 0 0 0 0 1 0`,
|
|
164
171
|
),
|
|
165
172
|
wrap(
|
|
166
|
-
`🟢${toDashboardDisplayLabel('
|
|
173
|
+
`🟢${toDashboardDisplayLabel('umbrella')} 0 1 0 0 0 0 0`,
|
|
167
174
|
),
|
|
168
175
|
wrap(
|
|
169
|
-
`🟢${toDashboardDisplayLabel('
|
|
176
|
+
`🟢${toDashboardDisplayLabel('soylent')} 0 0 0 0 1 0 0`,
|
|
170
177
|
),
|
|
171
|
-
wrap(`🟢${toDashboardDisplayLabel('cmg')} 0 0 0 0 1 0 0`),
|
|
172
178
|
wrap(''),
|
|
173
179
|
].join('\n') + '\n';
|
|
174
180
|
expect(composed).toBe(expected);
|
|
@@ -54,14 +54,20 @@ describe('ensureConsoleRunning', () => {
|
|
|
54
54
|
|
|
55
55
|
it('should short-circuit and return null when the port already responds', async () => {
|
|
56
56
|
const port = await startProbeServer();
|
|
57
|
-
const result = await ensureConsoleRunning('/path/to/config.yaml', port
|
|
57
|
+
const result = await ensureConsoleRunning('/path/to/config.yaml', port, [
|
|
58
|
+
'acme',
|
|
59
|
+
'globex',
|
|
60
|
+
]);
|
|
58
61
|
expect(spawnCalls).toHaveLength(0);
|
|
59
62
|
expect(result).toBeNull();
|
|
60
63
|
});
|
|
61
64
|
|
|
62
65
|
it('should spawn serveWeb when the port is unresponsive', async () => {
|
|
63
66
|
const unusedPort = await findFreePort();
|
|
64
|
-
await ensureConsoleRunning('/path/to/config.yaml', unusedPort
|
|
67
|
+
await ensureConsoleRunning('/path/to/config.yaml', unusedPort, [
|
|
68
|
+
'acme',
|
|
69
|
+
'globex',
|
|
70
|
+
]);
|
|
65
71
|
|
|
66
72
|
expect(spawnCalls).toHaveLength(1);
|
|
67
73
|
const [program, args, options] = spawnCalls[0];
|
|
@@ -80,16 +86,27 @@ describe('ensureConsoleRunning', () => {
|
|
|
80
86
|
if (!isObject(options)) {
|
|
81
87
|
throw new Error('Expected spawn options to be an object');
|
|
82
88
|
}
|
|
89
|
+
expect(args[6]).toBe('--dashboardProjectNames');
|
|
90
|
+
expect(args[7]).toBe('acme,globex');
|
|
83
91
|
expect(options.detached).toBe(true);
|
|
84
92
|
expect(options.stdio).toBe('ignore');
|
|
85
93
|
expect(unrefMock).toHaveBeenCalledTimes(1);
|
|
86
94
|
});
|
|
87
95
|
|
|
96
|
+
it('should refuse to start the console when no project name is configured', async () => {
|
|
97
|
+
const unusedPort = await findFreePort();
|
|
98
|
+
await expect(
|
|
99
|
+
ensureConsoleRunning('/path/to/config.yaml', unusedPort, []),
|
|
100
|
+
).rejects.toThrow('consoleProjects is empty');
|
|
101
|
+
expect(spawnCalls).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
88
104
|
it('should return an object with a kill method that delegates to the child process', async () => {
|
|
89
105
|
const unusedPort = await findFreePort();
|
|
90
106
|
const result = await ensureConsoleRunning(
|
|
91
107
|
'/path/to/config.yaml',
|
|
92
108
|
unusedPort,
|
|
109
|
+
['acme', 'globex'],
|
|
93
110
|
);
|
|
94
111
|
expect(result).not.toBeNull();
|
|
95
112
|
result?.kill();
|
|
@@ -32,7 +32,13 @@ const sleep = (ms: number): Promise<void> =>
|
|
|
32
32
|
export const ensureConsoleRunning = async (
|
|
33
33
|
configFilePath: string,
|
|
34
34
|
port: number,
|
|
35
|
+
dashboardProjectNames: string[],
|
|
35
36
|
): Promise<ConsoleProcess | null> => {
|
|
37
|
+
if (dashboardProjectNames.length === 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Cannot start the console from ${configFilePath}: consoleProjects is empty, so the dashboard has no project to render`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
36
42
|
if (await isPortResponding(port)) return null;
|
|
37
43
|
const cliEntry = path.resolve(__dirname, '../cli/index.js');
|
|
38
44
|
const child = spawn(
|
|
@@ -44,6 +50,8 @@ export const ensureConsoleRunning = async (
|
|
|
44
50
|
configFilePath,
|
|
45
51
|
'--port',
|
|
46
52
|
String(port),
|
|
53
|
+
'--dashboardProjectNames',
|
|
54
|
+
dashboardProjectNames.join(','),
|
|
47
55
|
],
|
|
48
56
|
{
|
|
49
57
|
detached: true,
|