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
@@ -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
- issueRepository: IssueRepository;
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.issueRepository.closePullRequest(prUrl);
217
+ await context.resolveIssueRepository(prUrl).closePullRequest(prUrl);
214
218
  if (isNonEmptyString(body.commentBody)) {
215
- await context.issueRepository.createCommentByUrl(prUrl, body.commentBody);
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.issueRepository.approvePullRequest(prUrl);
234
+ await context.resolveIssueRepository(prUrl).approvePullRequest(prUrl);
229
235
  const failure = await updateStatusByName(
230
- context.issueRepository,
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.issueRepository.requestChangesWithInlineComment(
258
- prUrl,
259
- changedFilePath,
260
- commentBody,
261
- inlineCommentLocation,
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.issueRepository,
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.issueRepository.createCommentByUrl(
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.issueRepository.closePullRequest(issueUrl);
316
+ await context.resolveIssueRepository(issueUrl).closePullRequest(issueUrl);
310
317
  } else {
311
- await context.issueRepository.closeIssueByUrl(
312
- issueUrl,
313
- action === 'close_not_planned' ? 'not_planned' : 'completed',
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.issueRepository,
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.issueRepository.updateStory(
354
- { ...project, story: project.story },
355
- projectItemReference(issueUrl, projectItemId),
356
- storyOptionId,
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.issueRepository.updateNextActionDate(
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.issueRepository.createCommentByUrl(url, commentBody);
391
- const comments =
392
- await context.issueRepository.getIssueOrPullRequestComments(url);
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.issueRepository.createPullRequestReviewComment(
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.issueRepository,
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
- 'umino',
13
- 'https://github.com/orgs/umino/projects/1',
14
- { xmile: 'https://github.com/orgs/xmile/projects/2' },
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
- umino: 'https://github.com/orgs/umino/projects/1',
18
- xmile: 'https://github.com/orgs/xmile/projects/2',
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
- 'umino',
25
- 'https://github.com/orgs/umino/projects/1',
26
- { umino: 'https://github.com/orgs/umino/projects/9' },
24
+ 'acme',
25
+ 'https://github.com/orgs/acme/projects/1',
26
+ { acme: 'https://github.com/orgs/acme/projects/9' },
27
27
  );
28
- expect(mapping.umino).toBe('https://github.com/orgs/umino/projects/9');
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
- 'umino',
34
- 'https://github.com/orgs/umino/projects/1',
33
+ 'acme',
34
+ 'https://github.com/orgs/acme/projects/1',
35
35
  null,
36
36
  );
37
37
  expect(mapping).toEqual({
38
- umino: 'https://github.com/orgs/umino/projects/1',
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
- umino: 'https://github.com/orgs/umino/projects/1',
47
- xmile: 'https://github.com/orgs/xmile/projects/2',
46
+ acme: 'https://github.com/orgs/acme/projects/1',
47
+ globex: 'https://github.com/orgs/globex/projects/2',
48
48
  });
49
- expect(isConfigured('umino')).toBe(true);
50
- expect(isConfigured('xmile')).toBe(true);
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 uminoProject: Project = { ...mock<Project>(), id: 'PVT_umino' };
57
- const xmileProject: Project = { ...mock<Project>(), id: 'PVT_xmile' };
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('umino') ? uminoProject : xmileProject,
61
+ url.includes('acme') ? acmeProject : globexProject,
62
62
  );
63
63
  const resolver = createConsoleProjectResolver(
64
64
  {
65
- umino: 'https://github.com/orgs/umino/projects/1',
66
- xmile: 'https://github.com/orgs/xmile/projects/2',
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('umino')).resolves.toEqual({
71
- pjcode: 'umino',
72
- project: uminoProject,
70
+ await expect(resolver('acme')).resolves.toEqual({
71
+ pjcode: 'acme',
72
+ project: acmeProject,
73
73
  });
74
- await expect(resolver('xmile')).resolves.toEqual({
75
- pjcode: 'xmile',
76
- project: xmileProject,
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 () => uminoProject);
81
+ const loadProject = jest.fn(async () => acmeProject);
82
82
  const resolver = createConsoleProjectResolver(
83
- { umino: 'https://github.com/orgs/umino/projects/1' },
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
- { umino: 'https://github.com/orgs/umino/projects/1' },
93
+ { acme: 'https://github.com/orgs/acme/projects/1' },
94
94
  loadProject,
95
95
  );
96
- await expect(resolver('umino')).resolves.toBeNull();
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 () => uminoProject);
100
+ const loadProject = jest.fn(async () => acmeProject);
101
101
  const resolver = createConsoleProjectResolver(
102
- { umino: 'https://github.com/orgs/umino/projects/1' },
102
+ { acme: 'https://github.com/orgs/acme/projects/1' },
103
103
  loadProject,
104
104
  );
105
- await resolver('umino');
106
- await resolver('umino');
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, 'umino', {
26
- pjcode: 'umino',
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: ['umino', 'xcare'],
39
+ projectNames: ['acme', 'initech'],
40
40
  });
41
41
  expect(input.projects).toEqual([
42
42
  {
43
- code: 'um',
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: 'xc', row: null },
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, 'umino', { unread: 1, todo: 1 });
65
+ writeProject(dataDir, 'acme', { unread: 1, todo: 1 });
66
66
  const input = buildComposeDashboardInput({
67
67
  dashboardDataDir: dataDir,
68
- projectNames: ['umino'],
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: ['umino'],
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, 'umino', {
369
- pjcode: 'umino',
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: ['umino', 'xcare'],
409
+ projectNames: ['acme', 'initech'],
413
410
  }),
414
411
  ).toBe(
415
412
  '<tt>M55%&nbsp;C62%&nbsp;D89%&nbsp;cy14</tt><br>\n' +
416
413
  '<tt>LA&nbsp;16&nbsp;23&nbsp;40</tt><br>\n' +
417
414
  '<tt>pj&nbsp;&nbsp;&nbsp;unr&nbsp;tdo&nbsp;aqc&nbsp;fal&nbsp;prp&nbsp;aws&nbsp;dep</tt><br>\n' +
418
- '<tt>🟢um&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;1</tt><br>\n' +
419
- '<tt>&nbsp;&nbsp;xc&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--</tt><br>\n' +
415
+ '<tt>🟢ac&nbsp;&nbsp;&nbsp;3&nbsp;&nbsp;&nbsp;1&nbsp;&nbsp;&nbsp;2&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;0&nbsp;&nbsp;&nbsp;4&nbsp;&nbsp;&nbsp;1</tt><br>\n' +
416
+ '<tt>&nbsp;&nbsp;in&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--&nbsp;&nbsp;--</tt><br>\n' +
420
417
  '<tt></tt><br>\n' +
421
418
  '<tt>🟢alice&nbsp;&nbsp;10%&nbsp;0d01h00&nbsp;&nbsp;12%&nbsp;5d00h00&nbsp;2&nbsp;1</tt><br>\n',
422
419
  );
@@ -446,7 +443,7 @@ describe('dashboardComposeFilesPresent', () => {
446
443
  };
447
444
 
448
445
  const minimalProject = {
449
- pjcode: 'umino',
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, 'umino', minimalProject);
466
- writeProject(dataDir, 'xcare', { ...minimalProject, pjcode: 'xcare' });
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: ['umino', 'xcare'],
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: ['umino'],
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, 'umino', minimalProject);
496
+ writeProject(dataDir, 'acme', minimalProject);
497
497
  expect(
498
498
  dashboardComposeFilesPresent({
499
499
  dashboardDataDir: dataDir,
500
- projectNames: ['umino', 'xcare'],
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, 'umino', minimalProject);
515
+ writeProject(dataDir, 'acme', minimalProject);
516
516
  expect(
517
517
  dashboardComposeFilesPresent({
518
518
  dashboardDataDir: dataDir,
519
- projectNames: ['umino'],
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, 'umino', minimalProject);
541
+ writeProject(dataDir, 'acme', minimalProject);
542
542
  expect(
543
543
  dashboardComposeFilesPresent({
544
544
  dashboardDataDir: dataDir,
545
- projectNames: ['umino'],
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(DASHBOARD_PROJECT_NAMES)(
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 DASHBOARD_PROJECT_NAMES) {
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: DEFAULT_DASHBOARD_PROJECT_NAMES,
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
- umino: [makeIssue({ status: 'Unread' }), makeIssue({ status: 'Unread' })],
118
- xmile: [makeIssue({ status: 'Awaiting Quality Check' })],
119
- xcare: [makeIssue({ status: 'Awaiting Workspace' })],
120
- utage3: [makeIssue({ status: 'Todo by human' })],
121
- cmg: [makeIssue({ status: 'Preparation' })],
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 DASHBOARD_PROJECT_NAMES) {
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: DEFAULT_DASHBOARD_PROJECT_NAMES,
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('umino')} 2 0 0 0 0 0 0`,
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('xmile')} 0 0 1 0 0 0 0`,
170
+ `🟢${toDashboardDisplayLabel('initech')} 0 0 0 0 0 1 0`,
164
171
  ),
165
172
  wrap(
166
- `🟢${toDashboardDisplayLabel('xcare')} 0 0 0 0 0 1 0`,
173
+ `🟢${toDashboardDisplayLabel('umbrella')} 0 1 0 0 0 0 0`,
167
174
  ),
168
175
  wrap(
169
- `🟢${toDashboardDisplayLabel('utage3')} 0 1 0 0 0 0 0`,
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,