github-issue-tower-defence-management 1.145.0 → 1.146.1
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 +22 -8
- package/README.md +1 -1
- package/bin/adapter/entry-points/cli/index.js +9 -7
- package/bin/adapter/entry-points/cli/index.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 +1 -3
- 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 -12
- 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 +12 -11
- 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/consoleItemUrlLookup.test.ts +6 -6
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +66 -66
- 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 -18
- 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 +0 -3
- 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 -19
- package/src/domain/usecases/dashboard/DashboardProjectCode.ts +21 -14
- 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 +0 -1
- 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
|
@@ -69,25 +69,25 @@ describe('webServer pure helpers', () => {
|
|
|
69
69
|
|
|
70
70
|
describe('isConsoleAppRoute', () => {
|
|
71
71
|
it('matches a per-project root route', () => {
|
|
72
|
-
expect(isConsoleAppRoute('/projects/
|
|
73
|
-
expect(isConsoleAppRoute('/projects/
|
|
72
|
+
expect(isConsoleAppRoute('/projects/acme')).toBe(true);
|
|
73
|
+
expect(isConsoleAppRoute('/projects/acme/')).toBe(true);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
it('matches a per-project tab route for every list tab', () => {
|
|
77
|
-
expect(isConsoleAppRoute('/projects/
|
|
78
|
-
expect(isConsoleAppRoute('/projects/
|
|
79
|
-
expect(isConsoleAppRoute('/projects/
|
|
80
|
-
expect(isConsoleAppRoute('/projects/
|
|
81
|
-
expect(isConsoleAppRoute('/projects/
|
|
77
|
+
expect(isConsoleAppRoute('/projects/acme/workflow-blocker')).toBe(true);
|
|
78
|
+
expect(isConsoleAppRoute('/projects/acme/prs')).toBe(true);
|
|
79
|
+
expect(isConsoleAppRoute('/projects/globex/triage')).toBe(true);
|
|
80
|
+
expect(isConsoleAppRoute('/projects/initech/unread')).toBe(true);
|
|
81
|
+
expect(isConsoleAppRoute('/projects/umbrella/failed-preparation')).toBe(
|
|
82
82
|
true,
|
|
83
83
|
);
|
|
84
|
-
expect(isConsoleAppRoute('/projects/
|
|
85
|
-
expect(isConsoleAppRoute('/projects/
|
|
84
|
+
expect(isConsoleAppRoute('/projects/umbrella/todo-by-human')).toBe(true);
|
|
85
|
+
expect(isConsoleAppRoute('/projects/umbrella/todo-by-agent')).toBe(true);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('does not match data, api, or unknown tab routes', () => {
|
|
89
|
-
expect(isConsoleAppRoute('/projects/
|
|
90
|
-
expect(isConsoleAppRoute('/projects/
|
|
89
|
+
expect(isConsoleAppRoute('/projects/acme/prs/list.json')).toBe(false);
|
|
90
|
+
expect(isConsoleAppRoute('/projects/acme/unknown')).toBe(false);
|
|
91
91
|
expect(isConsoleAppRoute('/projects')).toBe(false);
|
|
92
92
|
expect(isConsoleAppRoute('/api/review')).toBe(false);
|
|
93
93
|
expect(isConsoleAppRoute('/')).toBe(false);
|
|
@@ -339,17 +339,20 @@ describe('webServer integration', () => {
|
|
|
339
339
|
port: 0,
|
|
340
340
|
});
|
|
341
341
|
try {
|
|
342
|
-
const projectRoot = await requestServer(server, '/projects/
|
|
342
|
+
const projectRoot = await requestServer(server, '/projects/acme');
|
|
343
343
|
expect(projectRoot.statusCode).toBe(200);
|
|
344
344
|
expect(projectRoot.body).toContain('spa');
|
|
345
345
|
expect(projectRoot.contentType).toContain('text/html');
|
|
346
346
|
expect(projectRoot.cacheControl).toBe('no-store');
|
|
347
347
|
|
|
348
|
-
const projectTab = await requestServer(server, '/projects/
|
|
348
|
+
const projectTab = await requestServer(server, '/projects/globex/prs');
|
|
349
349
|
expect(projectTab.statusCode).toBe(200);
|
|
350
350
|
expect(projectTab.body).toContain('spa');
|
|
351
351
|
|
|
352
|
-
const unknownTab = await requestServer(
|
|
352
|
+
const unknownTab = await requestServer(
|
|
353
|
+
server,
|
|
354
|
+
'/projects/globex/unknown',
|
|
355
|
+
);
|
|
353
356
|
expect(unknownTab.statusCode).toBe(404);
|
|
354
357
|
} finally {
|
|
355
358
|
await closeServer(server);
|
|
@@ -370,7 +373,7 @@ describe('webServer integration', () => {
|
|
|
370
373
|
port: 0,
|
|
371
374
|
});
|
|
372
375
|
try {
|
|
373
|
-
const projectRoot = await requestServer(server, '/projects/
|
|
376
|
+
const projectRoot = await requestServer(server, '/projects/acme/triage');
|
|
374
377
|
expect(projectRoot.statusCode).toBe(200);
|
|
375
378
|
expect(projectRoot.body).toContain('TDPM Console');
|
|
376
379
|
} finally {
|
|
@@ -539,12 +542,12 @@ describe('webServer new routes integration', () => {
|
|
|
539
542
|
it('serves a data list file with the done exclusion through the token gate', async () => {
|
|
540
543
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
541
544
|
const dataDir = path.join(tmpDir, 'data');
|
|
542
|
-
const listDir = path.join(dataDir, '
|
|
545
|
+
const listDir = path.join(dataDir, 'acme', 'prs');
|
|
543
546
|
fs.mkdirSync(listDir, { recursive: true });
|
|
544
547
|
fs.writeFileSync(
|
|
545
548
|
path.join(listDir, 'list.json'),
|
|
546
549
|
JSON.stringify({
|
|
547
|
-
pjcode: '
|
|
550
|
+
pjcode: 'acme',
|
|
548
551
|
items: [{ projectItemId: 'PVTI_keep' }, { projectItemId: 'PVTI_drop' }],
|
|
549
552
|
}),
|
|
550
553
|
);
|
|
@@ -566,19 +569,19 @@ describe('webServer new routes integration', () => {
|
|
|
566
569
|
const unauthorized = await request(
|
|
567
570
|
server,
|
|
568
571
|
'GET',
|
|
569
|
-
'/projects/
|
|
572
|
+
'/projects/acme/prs/list.json',
|
|
570
573
|
);
|
|
571
574
|
expect(unauthorized.statusCode).toBe(401);
|
|
572
575
|
|
|
573
576
|
const authorized = await request(
|
|
574
577
|
server,
|
|
575
578
|
'GET',
|
|
576
|
-
`/projects/
|
|
579
|
+
`/projects/acme/prs/list.json?k=${testToken}`,
|
|
577
580
|
);
|
|
578
581
|
expect(authorized.statusCode).toBe(200);
|
|
579
582
|
const parsed: unknown = JSON.parse(authorized.body);
|
|
580
583
|
expect(parsed).toEqual({
|
|
581
|
-
pjcode: '
|
|
584
|
+
pjcode: 'acme',
|
|
582
585
|
items: [{ projectItemId: 'PVTI_keep' }],
|
|
583
586
|
});
|
|
584
587
|
} finally {
|
|
@@ -690,8 +693,8 @@ describe('webServer new routes integration', () => {
|
|
|
690
693
|
dashboardProjectNames: [],
|
|
691
694
|
issueRepository,
|
|
692
695
|
resolveProject: async (pjcode) =>
|
|
693
|
-
pjcode === '
|
|
694
|
-
isPjcodeConfigured: (pjcode) => pjcode === '
|
|
696
|
+
pjcode === 'acme' ? { pjcode, project: buildProject() } : null,
|
|
697
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'acme',
|
|
695
698
|
port: 0,
|
|
696
699
|
});
|
|
697
700
|
try {
|
|
@@ -700,7 +703,7 @@ describe('webServer new routes integration', () => {
|
|
|
700
703
|
'POST',
|
|
701
704
|
`/api/comment?k=${testToken}`,
|
|
702
705
|
{
|
|
703
|
-
pjcode: '
|
|
706
|
+
pjcode: 'acme',
|
|
704
707
|
url: 'https://github.com/o/r/issues/1',
|
|
705
708
|
body: 'Thanks, this resolves the parity gap.',
|
|
706
709
|
},
|
|
@@ -742,8 +745,8 @@ describe('webServer new routes integration', () => {
|
|
|
742
745
|
dashboardProjectNames: [],
|
|
743
746
|
issueRepository,
|
|
744
747
|
resolveProject: async (pjcode) =>
|
|
745
|
-
pjcode === '
|
|
746
|
-
isPjcodeConfigured: (pjcode) => pjcode === '
|
|
748
|
+
pjcode === 'acme' ? { pjcode, project: buildProject() } : null,
|
|
749
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'acme',
|
|
747
750
|
port: 0,
|
|
748
751
|
});
|
|
749
752
|
try {
|
|
@@ -752,7 +755,7 @@ describe('webServer new routes integration', () => {
|
|
|
752
755
|
'POST',
|
|
753
756
|
`/api/review?k=${testToken}`,
|
|
754
757
|
{
|
|
755
|
-
pjcode: '
|
|
758
|
+
pjcode: 'acme',
|
|
756
759
|
action: 'approve',
|
|
757
760
|
prUrl: 'https://github.com/o/r/pull/1',
|
|
758
761
|
projectItemId: 'PVTI_op',
|
|
@@ -762,7 +765,7 @@ describe('webServer new routes integration', () => {
|
|
|
762
765
|
expect(issueRepository.approvePullRequest).toHaveBeenCalledWith(
|
|
763
766
|
'https://github.com/o/r/pull/1',
|
|
764
767
|
);
|
|
765
|
-
expect(readDoneProjectItemIds(dataDir, '
|
|
768
|
+
expect(readDoneProjectItemIds(dataDir, 'acme', 'prs')).toContain(
|
|
766
769
|
'PVTI_op',
|
|
767
770
|
);
|
|
768
771
|
} finally {
|
|
@@ -791,8 +794,8 @@ describe('webServer new routes integration', () => {
|
|
|
791
794
|
dashboardProjectNames: [],
|
|
792
795
|
issueRepository,
|
|
793
796
|
resolveProject: async (pjcode) =>
|
|
794
|
-
pjcode === '
|
|
795
|
-
isPjcodeConfigured: (pjcode) => pjcode === '
|
|
797
|
+
pjcode === 'acme' ? { pjcode, project: buildProject() } : null,
|
|
798
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'acme',
|
|
796
799
|
port: 0,
|
|
797
800
|
});
|
|
798
801
|
try {
|
|
@@ -801,7 +804,7 @@ describe('webServer new routes integration', () => {
|
|
|
801
804
|
'POST',
|
|
802
805
|
`/api/review?k=${testToken}`,
|
|
803
806
|
{
|
|
804
|
-
pjcode: '
|
|
807
|
+
pjcode: 'acme',
|
|
805
808
|
action: 'approve',
|
|
806
809
|
prUrl: 'https://github.com/o/r/pull/1',
|
|
807
810
|
projectItemId: 'PVTI_op',
|
|
@@ -830,8 +833,8 @@ describe('webServer new routes integration', () => {
|
|
|
830
833
|
dashboardProjectNames: [],
|
|
831
834
|
issueRepository,
|
|
832
835
|
resolveProject: async (pjcode) =>
|
|
833
|
-
pjcode === '
|
|
834
|
-
isPjcodeConfigured: (pjcode) => pjcode === '
|
|
836
|
+
pjcode === 'acme' ? { pjcode, project: buildProject() } : null,
|
|
837
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'acme',
|
|
835
838
|
port: 0,
|
|
836
839
|
});
|
|
837
840
|
try {
|
|
@@ -912,7 +915,7 @@ describe('resolveFlatInTmuxFilePath', () => {
|
|
|
912
915
|
it('returns null for paths outside the flat in-tmux prefix', () => {
|
|
913
916
|
expect(resolveFlatInTmuxFilePath(baseDir, '/index.v4.json')).toBeNull();
|
|
914
917
|
expect(
|
|
915
|
-
resolveFlatInTmuxFilePath(baseDir, '/projects/
|
|
918
|
+
resolveFlatInTmuxFilePath(baseDir, '/projects/acme/in-tmux-by-human/x'),
|
|
916
919
|
).toBeNull();
|
|
917
920
|
});
|
|
918
921
|
|
|
@@ -1008,8 +1011,8 @@ describe('webServer flat in-tmux-by-human route integration', () => {
|
|
|
1008
1011
|
});
|
|
1009
1012
|
|
|
1010
1013
|
const indexV4Raw =
|
|
1011
|
-
'{"version":4,"projects":[{"name":"
|
|
1012
|
-
const indexV3Raw = '{"version":3,"projects":["
|
|
1014
|
+
'{"version":4,"projects":[{"name":"acme","path":"/in-tmux-by-human/acme.v4.json?k=secret"}]}\n';
|
|
1015
|
+
const indexV3Raw = '{"version":3,"projects":["acme"]}\n';
|
|
1013
1016
|
|
|
1014
1017
|
const startWithFixture = async (): Promise<{
|
|
1015
1018
|
server: http.Server;
|
|
@@ -1218,9 +1221,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1218
1221
|
const writeDataFiles = (dataDir: string): void => {
|
|
1219
1222
|
fs.mkdirSync(path.join(dataDir, 'projects'), { recursive: true });
|
|
1220
1223
|
fs.writeFileSync(
|
|
1221
|
-
path.join(dataDir, 'projects', '
|
|
1224
|
+
path.join(dataDir, 'projects', 'acme.json'),
|
|
1222
1225
|
JSON.stringify({
|
|
1223
|
-
pjcode: '
|
|
1226
|
+
pjcode: 'acme',
|
|
1224
1227
|
capturedAt: '2026-06-26T00:00:00.000Z',
|
|
1225
1228
|
unread: 3,
|
|
1226
1229
|
todo: 1,
|
|
@@ -1233,9 +1236,9 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1233
1236
|
}),
|
|
1234
1237
|
);
|
|
1235
1238
|
fs.writeFileSync(
|
|
1236
|
-
path.join(dataDir, 'projects', '
|
|
1239
|
+
path.join(dataDir, 'projects', 'initech.json'),
|
|
1237
1240
|
JSON.stringify({
|
|
1238
|
-
pjcode: '
|
|
1241
|
+
pjcode: 'initech',
|
|
1239
1242
|
capturedAt: '2026-06-26T00:00:00.000Z',
|
|
1240
1243
|
unread: 0,
|
|
1241
1244
|
todo: 0,
|
|
@@ -1282,8 +1285,8 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1282
1285
|
'<tt>M55% C62% D89% cy14</tt><br>\n' +
|
|
1283
1286
|
'<tt>LA 16 23 40</tt><br>\n' +
|
|
1284
1287
|
'<tt>pj unr tdo aqc fal prp aws dep</tt><br>\n' +
|
|
1285
|
-
'<tt>🟢
|
|
1286
|
-
'<tt>🟢
|
|
1288
|
+
'<tt>🟢ac 3 1 2 0 0 4 1</tt><br>\n' +
|
|
1289
|
+
'<tt>🟢in 0 0 0 0 0 2 0</tt><br>\n' +
|
|
1287
1290
|
'<tt></tt><br>\n' +
|
|
1288
1291
|
'<tt>🟢alice 10% 0d01h00 12% 5d00h00 2 1</tt><br>\n';
|
|
1289
1292
|
|
|
@@ -1306,7 +1309,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
|
|
|
1306
1309
|
inTmuxDataDir: null,
|
|
1307
1310
|
dashboardDir: overrides.dashboardDir,
|
|
1308
1311
|
dashboardDataDir: overrides.dashboardDataDir,
|
|
1309
|
-
dashboardProjectNames: ['
|
|
1312
|
+
dashboardProjectNames: ['acme', 'initech'],
|
|
1310
1313
|
port: 0,
|
|
1311
1314
|
});
|
|
1312
1315
|
return { server, tmpDir };
|
|
@@ -1748,10 +1751,10 @@ describe('webServer token cookie redirect', () => {
|
|
|
1748
1751
|
try {
|
|
1749
1752
|
const response = await requestServer(
|
|
1750
1753
|
server,
|
|
1751
|
-
`/projects/
|
|
1754
|
+
`/projects/acme?k=${testToken}`,
|
|
1752
1755
|
);
|
|
1753
1756
|
expect(response.statusCode).toBe(302);
|
|
1754
|
-
expect(response.location).toBe('/projects/
|
|
1757
|
+
expect(response.location).toBe('/projects/acme');
|
|
1755
1758
|
expect(response.body).not.toContain(testToken);
|
|
1756
1759
|
const cookie = firstSetCookie(response.setCookie);
|
|
1757
1760
|
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
@@ -1770,10 +1773,10 @@ describe('webServer token cookie redirect', () => {
|
|
|
1770
1773
|
try {
|
|
1771
1774
|
const response = await requestServer(
|
|
1772
1775
|
server,
|
|
1773
|
-
`/projects/
|
|
1776
|
+
`/projects/globex/prs?k=${testToken}&foo=bar`,
|
|
1774
1777
|
);
|
|
1775
1778
|
expect(response.statusCode).toBe(302);
|
|
1776
|
-
expect(response.location).toBe('/projects/
|
|
1779
|
+
expect(response.location).toBe('/projects/globex/prs?foo=bar');
|
|
1777
1780
|
const cookie = firstSetCookie(response.setCookie);
|
|
1778
1781
|
expect(cookie).toContain(`${CONSOLE_TOKEN_COOKIE}=${testToken}`);
|
|
1779
1782
|
} finally {
|
|
@@ -1807,7 +1810,7 @@ describe('webServer token cookie redirect', () => {
|
|
|
1807
1810
|
it('serves the app route without a token and never leaks a token in the URL', async () => {
|
|
1808
1811
|
const { server, tmpDir } = await startWithUiDist();
|
|
1809
1812
|
try {
|
|
1810
|
-
const response = await requestServer(server, '/projects/
|
|
1813
|
+
const response = await requestServer(server, '/projects/acme');
|
|
1811
1814
|
expect(response.statusCode).toBe(200);
|
|
1812
1815
|
expect(response.body).toContain('spa');
|
|
1813
1816
|
expect(response.setCookie).toBeUndefined();
|
|
@@ -1825,7 +1828,7 @@ describe('webServer token cookie redirect', () => {
|
|
|
1825
1828
|
expect(root.statusCode).toBe(200);
|
|
1826
1829
|
expect(root.referrerPolicy).toBe('no-referrer');
|
|
1827
1830
|
|
|
1828
|
-
const projectTab = await requestServer(server, '/projects/
|
|
1831
|
+
const projectTab = await requestServer(server, '/projects/globex/prs');
|
|
1829
1832
|
expect(projectTab.statusCode).toBe(200);
|
|
1830
1833
|
expect(projectTab.referrerPolicy).toBe('no-referrer');
|
|
1831
1834
|
} finally {
|
|
@@ -1837,11 +1840,11 @@ describe('webServer token cookie redirect', () => {
|
|
|
1837
1840
|
it('authenticates a data list request through the cookie without a token in the URL', async () => {
|
|
1838
1841
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'console-server-'));
|
|
1839
1842
|
const dataDir = path.join(tmpDir, 'data');
|
|
1840
|
-
const listDir = path.join(dataDir, '
|
|
1843
|
+
const listDir = path.join(dataDir, 'acme', 'prs');
|
|
1841
1844
|
fs.mkdirSync(listDir, { recursive: true });
|
|
1842
1845
|
fs.writeFileSync(
|
|
1843
1846
|
path.join(listDir, 'list.json'),
|
|
1844
|
-
JSON.stringify({ pjcode: '
|
|
1847
|
+
JSON.stringify({ pjcode: 'acme', items: [] }),
|
|
1845
1848
|
);
|
|
1846
1849
|
const server = await startWebServer({
|
|
1847
1850
|
accessToken: testToken,
|
|
@@ -1856,12 +1859,12 @@ describe('webServer token cookie redirect', () => {
|
|
|
1856
1859
|
try {
|
|
1857
1860
|
const withCookie = await requestServer(
|
|
1858
1861
|
server,
|
|
1859
|
-
'/projects/
|
|
1862
|
+
'/projects/acme/prs/list.json',
|
|
1860
1863
|
{ Cookie: `${CONSOLE_TOKEN_COOKIE}=${testToken}` },
|
|
1861
1864
|
);
|
|
1862
1865
|
expect(withCookie.statusCode).toBe(200);
|
|
1863
1866
|
expect(JSON.parse(withCookie.body)).toEqual({
|
|
1864
|
-
pjcode: '
|
|
1867
|
+
pjcode: 'acme',
|
|
1865
1868
|
items: [],
|
|
1866
1869
|
});
|
|
1867
1870
|
} finally {
|
|
@@ -35,12 +35,9 @@ import {
|
|
|
35
35
|
composeDashboardText,
|
|
36
36
|
dashboardComposeFilesPresent,
|
|
37
37
|
} from './dashboardComposeService';
|
|
38
|
-
import { DASHBOARD_PROJECT_NAMES } from '../../../domain/usecases/dashboard/DashboardProjectCode';
|
|
39
38
|
|
|
40
39
|
export const DEFAULT_WEB_PORT = 9980;
|
|
41
40
|
|
|
42
|
-
export const DEFAULT_DASHBOARD_PROJECT_NAMES = DASHBOARD_PROJECT_NAMES;
|
|
43
|
-
|
|
44
41
|
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
45
42
|
|
|
46
43
|
export const CONSOLE_TOKEN_COOKIE = 'pv_token';
|
|
@@ -394,7 +394,7 @@ allowedIssueAuthors: 'user1, user2, user3'
|
|
|
394
394
|
mockFetchReturningReadme(null);
|
|
395
395
|
const configWithCommaSeparatedAuthors = {
|
|
396
396
|
...validConfig,
|
|
397
|
-
allowedIssueAuthors: 'owner,
|
|
397
|
+
allowedIssueAuthors: 'owner, acme-bot ,dependabot[bot]',
|
|
398
398
|
};
|
|
399
399
|
jest
|
|
400
400
|
.mocked(fs.readFileSync)
|
|
@@ -404,7 +404,7 @@ allowedIssueAuthors: 'user1, user2, user3'
|
|
|
404
404
|
await handler.handle('config.yml', false);
|
|
405
405
|
|
|
406
406
|
expect(capturedRunInputs[0][0]).toMatchObject({
|
|
407
|
-
allowedIssueAuthors: ['owner', '
|
|
407
|
+
allowedIssueAuthors: ['owner', 'acme-bot', 'dependabot[bot]'],
|
|
408
408
|
});
|
|
409
409
|
});
|
|
410
410
|
|
|
@@ -62,6 +62,7 @@ import { UpdateRateLimitCacheUseCase } from '../../../domain/usecases/UpdateRate
|
|
|
62
62
|
import { RevertOrphanedPreparationUseCase } from '../../../domain/usecases/RevertOrphanedPreparationUseCase';
|
|
63
63
|
import { RevertNotReadyReviewQueueIssueUseCase } from '../../../domain/usecases/RevertNotReadyReviewQueueIssueUseCase';
|
|
64
64
|
import { GitHubIssueCommentRepository } from '../../repositories/GitHubIssueCommentRepository';
|
|
65
|
+
import { ProjectRequiredFieldCreateUseCase } from '../../../domain/usecases/ProjectRequiredFieldCreateUseCase';
|
|
65
66
|
import { SetupTowerDefenceProjectUseCase } from '../../../domain/usecases/SetupTowerDefenceProjectUseCase';
|
|
66
67
|
import { DailySecurityScanUseCase } from '../../../domain/usecases/DailySecurityScanUseCase';
|
|
67
68
|
import { KyHttpRepository } from '../../repositories/KyHttpRepository';
|
|
@@ -334,6 +335,8 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
334
335
|
systemDateRepository,
|
|
335
336
|
...githubRepositoryParams,
|
|
336
337
|
);
|
|
338
|
+
const projectRequiredFieldCreateUseCase =
|
|
339
|
+
new ProjectRequiredFieldCreateUseCase(projectRepository);
|
|
337
340
|
const setupTowerDefenceProjectUseCase = new SetupTowerDefenceProjectUseCase(
|
|
338
341
|
projectRepository,
|
|
339
342
|
issueRepository,
|
|
@@ -425,6 +428,7 @@ export class HandleScheduledEventUseCaseHandler {
|
|
|
425
428
|
: null;
|
|
426
429
|
|
|
427
430
|
const handleScheduledEventUseCase = new HandleScheduledEventUseCase(
|
|
431
|
+
projectRequiredFieldCreateUseCase,
|
|
428
432
|
setupTowerDefenceProjectUseCase,
|
|
429
433
|
actionAnnouncement,
|
|
430
434
|
setWorkflowManagementIssueToStoryUseCase,
|
|
@@ -56,7 +56,7 @@ describe('writeDashboardRow', () => {
|
|
|
56
56
|
it('writes a per-project row file with counts, pjcode and capturedAt', () => {
|
|
57
57
|
writeDashboardRow({
|
|
58
58
|
dashboardDataDir: dir,
|
|
59
|
-
pjcode: '
|
|
59
|
+
pjcode: 'ac',
|
|
60
60
|
assigneeLogin: ASSIGNEE,
|
|
61
61
|
issues: [
|
|
62
62
|
makeIssue({ status: 'Unread' }),
|
|
@@ -65,9 +65,9 @@ describe('writeDashboardRow', () => {
|
|
|
65
65
|
generatedAt: '2026-06-26T12:00:00.000Z',
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
-
const written = readJson(path.join(dir, 'projects', '
|
|
68
|
+
const written = readJson(path.join(dir, 'projects', 'ac.json'));
|
|
69
69
|
const expected: DashboardRowFile = {
|
|
70
|
-
pjcode: '
|
|
70
|
+
pjcode: 'ac',
|
|
71
71
|
capturedAt: '2026-06-26T12:00:00.000Z',
|
|
72
72
|
unread: 1,
|
|
73
73
|
todo: 0,
|
|
@@ -84,7 +84,7 @@ describe('writeDashboardRow', () => {
|
|
|
84
84
|
it('is a no-op when dashboardDataDir is unset', () => {
|
|
85
85
|
writeDashboardRow({
|
|
86
86
|
dashboardDataDir: null,
|
|
87
|
-
pjcode: '
|
|
87
|
+
pjcode: 'ac',
|
|
88
88
|
assigneeLogin: ASSIGNEE,
|
|
89
89
|
issues: [makeIssue({ status: 'Unread' })],
|
|
90
90
|
});
|
|
@@ -101,7 +101,7 @@ describe('writeDashboardRow', () => {
|
|
|
101
101
|
});
|
|
102
102
|
writeDashboardRow({
|
|
103
103
|
dashboardDataDir: dir,
|
|
104
|
-
pjcode: '
|
|
104
|
+
pjcode: 'ac',
|
|
105
105
|
assigneeLogin: null,
|
|
106
106
|
issues: [makeIssue({ status: 'Unread' })],
|
|
107
107
|
});
|
|
@@ -195,21 +195,21 @@ describe('writeInTmuxByHumanData', () => {
|
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
it('writes index files listing configured projects whose per-project file exists', () => {
|
|
198
|
-
fs.writeFileSync(file('
|
|
198
|
+
fs.writeFileSync(file('globex.json'), '[]\n');
|
|
199
199
|
writeInTmuxByHumanData({
|
|
200
200
|
...baseParams(outDir),
|
|
201
|
-
inTmuxProjectOrder: ['demo', '
|
|
201
|
+
inTmuxProjectOrder: ['demo', 'globex', 'initech'],
|
|
202
202
|
});
|
|
203
203
|
expect(readJson(file('index.json'))).toEqual({
|
|
204
|
-
projects: ['demo', '
|
|
204
|
+
projects: ['demo', 'globex'],
|
|
205
205
|
});
|
|
206
206
|
expect(readJson(file('index.v2.json'))).toEqual({
|
|
207
207
|
version: 2,
|
|
208
|
-
projects: ['demo', '
|
|
208
|
+
projects: ['demo', 'globex'],
|
|
209
209
|
});
|
|
210
210
|
expect(readJson(file('index.v3.json'))).toEqual({
|
|
211
211
|
version: 3,
|
|
212
|
-
projects: ['demo', '
|
|
212
|
+
projects: ['demo', 'globex'],
|
|
213
213
|
});
|
|
214
214
|
});
|
|
215
215
|
|
|
@@ -4,6 +4,13 @@ import { LocalStorageCacheRepository } from './LocalStorageCacheRepository';
|
|
|
4
4
|
import { LocalStorageRepository } from './LocalStorageRepository';
|
|
5
5
|
import { ProjectRepository } from '../../domain/usecases/adapter-interfaces/ProjectRepository';
|
|
6
6
|
import { FieldOption, Project } from '../../domain/entities/Project';
|
|
7
|
+
import {
|
|
8
|
+
DEPENDED_ISSUE_URL_FIELD_NAME,
|
|
9
|
+
NEXT_ACTION_DATE_FIELD_NAME,
|
|
10
|
+
NEXT_ACTION_HOUR_FIELD_NAME,
|
|
11
|
+
RequiredProjectFieldDefinition,
|
|
12
|
+
STORY_FIELD_NAME,
|
|
13
|
+
} from '../../domain/entities/RequiredProjectField';
|
|
7
14
|
import { normalizeFieldName } from './utils';
|
|
8
15
|
|
|
9
16
|
const ONE_HOUR_MS = 60 * 60 * 1000;
|
|
@@ -38,6 +45,8 @@ export class GraphqlProjectRepository
|
|
|
38
45
|
| 'getByUrl'
|
|
39
46
|
| 'updateStoryList'
|
|
40
47
|
| 'updateStatusList'
|
|
48
|
+
| 'listFieldNames'
|
|
49
|
+
| 'createField'
|
|
41
50
|
>
|
|
42
51
|
{
|
|
43
52
|
private readonly projectIdCache = new Map<string, string>();
|
|
@@ -339,10 +348,14 @@ export class GraphqlProjectRepository
|
|
|
339
348
|
return null;
|
|
340
349
|
}
|
|
341
350
|
const nextActionDate = project.fields.nodes.find(
|
|
342
|
-
(field) =>
|
|
351
|
+
(field) =>
|
|
352
|
+
normalizeFieldName(field.name) ===
|
|
353
|
+
normalizeFieldName(NEXT_ACTION_DATE_FIELD_NAME),
|
|
343
354
|
);
|
|
344
355
|
const nextActionHour = project.fields.nodes.find(
|
|
345
|
-
(field) =>
|
|
356
|
+
(field) =>
|
|
357
|
+
normalizeFieldName(field.name) ===
|
|
358
|
+
normalizeFieldName(NEXT_ACTION_HOUR_FIELD_NAME),
|
|
346
359
|
);
|
|
347
360
|
const status = project.fields.nodes.find(
|
|
348
361
|
(field) => normalizeFieldName(field.name) === 'status',
|
|
@@ -351,7 +364,8 @@ export class GraphqlProjectRepository
|
|
|
351
364
|
throw new Error('status field is not found');
|
|
352
365
|
}
|
|
353
366
|
const story = project.fields.nodes.find(
|
|
354
|
-
(field) =>
|
|
367
|
+
(field) =>
|
|
368
|
+
normalizeFieldName(field.name) === normalizeFieldName(STORY_FIELD_NAME),
|
|
355
369
|
);
|
|
356
370
|
const workflowManagementStory = story?.options.find((option) =>
|
|
357
371
|
normalizeFieldName(option.name).includes('workflowmanagement'),
|
|
@@ -363,7 +377,7 @@ export class GraphqlProjectRepository
|
|
|
363
377
|
const dependedIssueUrlSeparatedByComma = project.fields.nodes.find(
|
|
364
378
|
(field) =>
|
|
365
379
|
normalizeFieldName(field.name).startsWith(
|
|
366
|
-
|
|
380
|
+
normalizeFieldName(DEPENDED_ISSUE_URL_FIELD_NAME),
|
|
367
381
|
),
|
|
368
382
|
);
|
|
369
383
|
const completionDate50PercentConfidence = project.fields.nodes.find(
|
|
@@ -442,6 +456,98 @@ export class GraphqlProjectRepository
|
|
|
442
456
|
}
|
|
443
457
|
return project;
|
|
444
458
|
};
|
|
459
|
+
listFieldNames = async (project: Project): Promise<string[]> => {
|
|
460
|
+
const query = `query ListProjectV2FieldNames($projectId: ID!) {
|
|
461
|
+
node(id: $projectId) {
|
|
462
|
+
... on ProjectV2 {
|
|
463
|
+
fields(first: 100) {
|
|
464
|
+
nodes {
|
|
465
|
+
... on ProjectV2FieldCommon {
|
|
466
|
+
name
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}`;
|
|
473
|
+
const response = await postGithubGraphqlJson<{
|
|
474
|
+
data?: {
|
|
475
|
+
node: {
|
|
476
|
+
fields: {
|
|
477
|
+
nodes: { name?: string }[];
|
|
478
|
+
};
|
|
479
|
+
} | null;
|
|
480
|
+
};
|
|
481
|
+
errors?: { message: string }[];
|
|
482
|
+
}>({
|
|
483
|
+
ghToken: this.ghToken,
|
|
484
|
+
query,
|
|
485
|
+
variables: { projectId: project.id },
|
|
486
|
+
});
|
|
487
|
+
if (!response.data || !response.data.node) {
|
|
488
|
+
const errorMessages = response.errors
|
|
489
|
+
? response.errors.map((e) => e.message).join('; ')
|
|
490
|
+
: 'no data field in response';
|
|
491
|
+
throw new Error(
|
|
492
|
+
`GitHub GraphQL API returned no data for listFieldNames: ${errorMessages}`,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return response.data.node.fields.nodes
|
|
496
|
+
.map((field) => field.name)
|
|
497
|
+
.filter((name): name is string => typeof name === 'string');
|
|
498
|
+
};
|
|
499
|
+
createField = async (
|
|
500
|
+
project: Project,
|
|
501
|
+
field: RequiredProjectFieldDefinition,
|
|
502
|
+
): Promise<void> => {
|
|
503
|
+
const mutation = `mutation CreateProjectV2Field($projectId: ID!, $dataType: ProjectV2CustomFieldType!, $name: String!, $singleSelectOptions: [ProjectV2SingleSelectFieldOptionInput!]) {
|
|
504
|
+
createProjectV2Field(input: {
|
|
505
|
+
projectId: $projectId
|
|
506
|
+
dataType: $dataType
|
|
507
|
+
name: $name
|
|
508
|
+
singleSelectOptions: $singleSelectOptions
|
|
509
|
+
}) {
|
|
510
|
+
projectV2Field {
|
|
511
|
+
... on ProjectV2FieldCommon {
|
|
512
|
+
id
|
|
513
|
+
name
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}`;
|
|
518
|
+
const response = await postGithubGraphqlJson<{
|
|
519
|
+
data?: {
|
|
520
|
+
createProjectV2Field: {
|
|
521
|
+
projectV2Field: { id: string; name: string };
|
|
522
|
+
};
|
|
523
|
+
};
|
|
524
|
+
errors?: { message: string }[];
|
|
525
|
+
}>({
|
|
526
|
+
ghToken: this.ghToken,
|
|
527
|
+
query: mutation,
|
|
528
|
+
variables: {
|
|
529
|
+
projectId: project.id,
|
|
530
|
+
dataType: field.dataType,
|
|
531
|
+
name: field.name,
|
|
532
|
+
singleSelectOptions:
|
|
533
|
+
field.dataType === 'SINGLE_SELECT'
|
|
534
|
+
? field.options.map(({ name, color, description }) => ({
|
|
535
|
+
name,
|
|
536
|
+
color,
|
|
537
|
+
description,
|
|
538
|
+
}))
|
|
539
|
+
: null,
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
if (!response.data) {
|
|
543
|
+
const errorMessages = response.errors
|
|
544
|
+
? response.errors.map((e) => e.message).join('; ')
|
|
545
|
+
: 'no data field in response';
|
|
546
|
+
throw new Error(
|
|
547
|
+
`GitHub GraphQL API returned no data for createField ${field.name}: ${errorMessages}`,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
445
551
|
updateStoryList = async (
|
|
446
552
|
project: Project,
|
|
447
553
|
newStoryList: (Omit<FieldOption, 'id'> & {
|
|
@@ -3092,7 +3092,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
|
|
|
3092
3092
|
|
|
3093
3093
|
const { repository } = createApiV3CheerioRestIssueRepository();
|
|
3094
3094
|
const result = await repository.findRelatedOpenPRs(
|
|
3095
|
-
'https://github.com/HiromiShikata/
|
|
3095
|
+
'https://github.com/HiromiShikata/acme-corporait-operation/issues/30106',
|
|
3096
3096
|
);
|
|
3097
3097
|
|
|
3098
3098
|
expect(result).toHaveLength(1);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeProjectFieldName } from '../../domain/entities/ProjectFieldName';
|
|
2
|
+
|
|
1
3
|
// 0:50 , 1:10 = 0:20
|
|
2
4
|
// 23:50 , 0:00 = 0:10
|
|
3
5
|
// 0:20, 0:10 = 23:50
|
|
@@ -40,6 +42,4 @@ export const totalDuration = (
|
|
|
40
42
|
return `${String(totalHour).padStart(2, '0')}:${String(totalMinute).padStart(2, '0')}`;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
export const normalizeFieldName =
|
|
44
|
-
return fieldName.toLowerCase().replace(/[\s-()]/g, '');
|
|
45
|
-
};
|
|
45
|
+
export const normalizeFieldName = normalizeProjectFieldName;
|