github-issue-tower-defence-management 1.122.2 → 1.122.4
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 +15 -0
- package/README.md +3 -3
- package/bin/adapter/entry-points/cli/index.js +9 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleDoneStore.js +19 -5
- package/bin/adapter/entry-points/console/consoleDoneStore.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +78 -39
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +8 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -1
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js +2 -0
- package/bin/adapter/entry-points/handlers/consoleListsWriter.js.map +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js +1 -1
- package/bin/adapter/entry-points/handlers/notifySilentTmuxSessions.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +6 -7
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js +60 -11
- package/bin/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.js.map +1 -1
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +64 -7
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +23 -4
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +5 -7
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +51 -16
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/cli/index.test.ts +3 -1
- package/src/adapter/entry-points/cli/index.ts +11 -0
- package/src/adapter/entry-points/console/consoleDoneStore.test.ts +38 -0
- package/src/adapter/entry-points/console/consoleDoneStore.ts +29 -4
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +190 -11
- package/src/adapter/entry-points/console/consoleOperationApi.ts +97 -56
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +13 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +11 -0
- package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +3 -0
- package/src/adapter/entry-points/console/webServer.test.ts +4 -0
- package/src/adapter/entry-points/console/webServer.ts +9 -1
- package/src/adapter/entry-points/handlers/consoleListsWriter.test.ts +72 -0
- package/src/adapter/entry-points/handlers/consoleListsWriter.ts +3 -0
- package/src/adapter/entry-points/handlers/notifySilentTmuxSessions.ts +1 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +60 -37
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -18
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.test.ts +223 -0
- package/src/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.ts +94 -10
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +12 -2
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +196 -4
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +91 -7
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +86 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +34 -7
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +112 -51
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +8 -17
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +250 -18
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +72 -22
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.ts +8 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +4 -7
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleDoneStore.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +2 -0
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +2 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -1
- package/types/adapter/entry-points/console/webServer.d.ts +2 -1
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/consoleListsWriter.d.ts.map +1 -1
- package/types/adapter/entry-points/handlers/notifySilentTmuxSessions.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -3
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts +13 -2
- package/types/adapter/repositories/FileSystemSilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts +5 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +2 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -3
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts +2 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts +8 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionCandidateStateRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +4 -4
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -7,6 +7,7 @@ import { Project } from '../../../domain/entities/Project';
|
|
|
7
7
|
import { Issue } from '../../../domain/entities/Issue';
|
|
8
8
|
import {
|
|
9
9
|
ConsoleOperationContext,
|
|
10
|
+
ConsoleProjectBinding,
|
|
10
11
|
handleComment,
|
|
11
12
|
handleIntmux,
|
|
12
13
|
handleReview,
|
|
@@ -73,6 +74,7 @@ describe('consoleOperationApi', () => {
|
|
|
73
74
|
issueRepository,
|
|
74
75
|
resolveProject: async (pjcode: string) =>
|
|
75
76
|
pjcode === 'umino' ? { pjcode, project } : null,
|
|
77
|
+
isPjcodeConfigured: (pjcode: string) => pjcode === 'umino',
|
|
76
78
|
consoleDataOutputDir: baseDir,
|
|
77
79
|
};
|
|
78
80
|
});
|
|
@@ -83,6 +85,7 @@ describe('consoleOperationApi', () => {
|
|
|
83
85
|
issueRepository,
|
|
84
86
|
resolveProject: async (pjcode: string) =>
|
|
85
87
|
pjcode === 'umino' ? { pjcode, project: nextProject } : null,
|
|
88
|
+
isPjcodeConfigured: (pjcode: string) => pjcode === 'umino',
|
|
86
89
|
consoleDataOutputDir: baseDir,
|
|
87
90
|
});
|
|
88
91
|
|
|
@@ -112,9 +115,13 @@ describe('consoleOperationApi', () => {
|
|
|
112
115
|
);
|
|
113
116
|
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
114
117
|
project,
|
|
115
|
-
{
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
itemId: 'PVTI_a',
|
|
120
|
+
url: 'https://github.com/o/r/pull/1',
|
|
121
|
+
}),
|
|
116
122
|
'status_aw',
|
|
117
123
|
);
|
|
124
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
118
125
|
expectRecordedAcrossTabs('PVTI_a');
|
|
119
126
|
});
|
|
120
127
|
|
|
@@ -234,7 +241,7 @@ describe('consoleOperationApi', () => {
|
|
|
234
241
|
expect(response.statusCode).toBe(400);
|
|
235
242
|
});
|
|
236
243
|
|
|
237
|
-
it('
|
|
244
|
+
it('approves using the request project item id without a GraphQL item fetch', async () => {
|
|
238
245
|
issueRepository.get.mockResolvedValue(null);
|
|
239
246
|
const response = await handleReview(context, {
|
|
240
247
|
pjcode: 'umino',
|
|
@@ -242,8 +249,13 @@ describe('consoleOperationApi', () => {
|
|
|
242
249
|
prUrl: 'https://github.com/o/r/pull/1',
|
|
243
250
|
projectItemId: 'PVTI_c',
|
|
244
251
|
});
|
|
245
|
-
expect(response.statusCode).toBe(
|
|
246
|
-
expect(issueRepository.
|
|
252
|
+
expect(response.statusCode).toBe(200);
|
|
253
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
254
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
255
|
+
project,
|
|
256
|
+
expect.objectContaining({ itemId: 'PVTI_c' }),
|
|
257
|
+
'status_aw',
|
|
258
|
+
);
|
|
247
259
|
});
|
|
248
260
|
|
|
249
261
|
it('returns 400 when the Awaiting workspace status is absent', async () => {
|
|
@@ -273,9 +285,13 @@ describe('consoleOperationApi', () => {
|
|
|
273
285
|
expect(response.statusCode).toBe(200);
|
|
274
286
|
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
275
287
|
project,
|
|
276
|
-
{
|
|
288
|
+
expect.objectContaining({
|
|
289
|
+
itemId: 'PVTI_d',
|
|
290
|
+
url: 'https://github.com/o/r/issues/1',
|
|
291
|
+
}),
|
|
277
292
|
'status_todo',
|
|
278
293
|
);
|
|
294
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
279
295
|
expectRecordedAcrossTabs('PVTI_d');
|
|
280
296
|
});
|
|
281
297
|
|
|
@@ -302,9 +318,10 @@ describe('consoleOperationApi', () => {
|
|
|
302
318
|
expect(response.statusCode).toBe(200);
|
|
303
319
|
expect(issueRepository.updateStory).toHaveBeenCalledWith(
|
|
304
320
|
expect.objectContaining({ id: 'PVT_1' }),
|
|
305
|
-
{
|
|
321
|
+
expect.objectContaining({ itemId: 'PVTI_e' }),
|
|
306
322
|
'story_opt_1',
|
|
307
323
|
);
|
|
324
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
308
325
|
expectRecordedAcrossTabs('PVTI_e');
|
|
309
326
|
});
|
|
310
327
|
|
|
@@ -457,7 +474,7 @@ describe('consoleOperationApi', () => {
|
|
|
457
474
|
expect(response.statusCode).toBe(400);
|
|
458
475
|
});
|
|
459
476
|
|
|
460
|
-
it('
|
|
477
|
+
it('sets the story using the request project item id without a GraphQL item fetch', async () => {
|
|
461
478
|
issueRepository.get.mockResolvedValue(null);
|
|
462
479
|
const response = await handleTriage(context, {
|
|
463
480
|
pjcode: 'umino',
|
|
@@ -466,7 +483,157 @@ describe('consoleOperationApi', () => {
|
|
|
466
483
|
projectItemId: 'PVTI_h',
|
|
467
484
|
storyOptionId: 'story_opt_1',
|
|
468
485
|
});
|
|
486
|
+
expect(response.statusCode).toBe(200);
|
|
487
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
488
|
+
expect(issueRepository.updateStory).toHaveBeenCalledWith(
|
|
489
|
+
expect.objectContaining({ id: 'PVT_1' }),
|
|
490
|
+
expect.objectContaining({ itemId: 'PVTI_h' }),
|
|
491
|
+
'story_opt_1',
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('close operations avoid loading the project via GraphQL', () => {
|
|
497
|
+
let resolveProjectSpy: jest.Mock<
|
|
498
|
+
Promise<ConsoleProjectBinding | null>,
|
|
499
|
+
[string]
|
|
500
|
+
>;
|
|
501
|
+
let spiedContext: ConsoleOperationContext;
|
|
502
|
+
|
|
503
|
+
beforeEach(() => {
|
|
504
|
+
resolveProjectSpy = jest.fn(async (pjcode: string) =>
|
|
505
|
+
pjcode === 'umino' ? { pjcode, project } : null,
|
|
506
|
+
);
|
|
507
|
+
spiedContext = {
|
|
508
|
+
issueRepository,
|
|
509
|
+
resolveProject: resolveProjectSpy,
|
|
510
|
+
isPjcodeConfigured: (pjcode: string) => pjcode === 'umino',
|
|
511
|
+
consoleDataOutputDir: baseDir,
|
|
512
|
+
};
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
it('closes an issue via triage without resolving the project', async () => {
|
|
516
|
+
const response = await handleTriage(spiedContext, {
|
|
517
|
+
pjcode: 'umino',
|
|
518
|
+
action: 'close',
|
|
519
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
520
|
+
projectItemId: 'PVTI_noproj_close',
|
|
521
|
+
commentBody: 'duplicate',
|
|
522
|
+
});
|
|
523
|
+
expect(response.statusCode).toBe(200);
|
|
524
|
+
expect(issueRepository.closeIssueByUrl).toHaveBeenCalledWith(
|
|
525
|
+
'https://github.com/o/r/issues/1',
|
|
526
|
+
'completed',
|
|
527
|
+
);
|
|
528
|
+
expect(issueRepository.createCommentByUrl).toHaveBeenCalledWith(
|
|
529
|
+
'https://github.com/o/r/issues/1',
|
|
530
|
+
'duplicate',
|
|
531
|
+
);
|
|
532
|
+
expect(resolveProjectSpy).not.toHaveBeenCalled();
|
|
533
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it('closes not planned via triage without resolving the project', async () => {
|
|
537
|
+
const response = await handleTriage(spiedContext, {
|
|
538
|
+
pjcode: 'umino',
|
|
539
|
+
action: 'close_not_planned',
|
|
540
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
541
|
+
projectItemId: 'PVTI_noproj_np',
|
|
542
|
+
});
|
|
543
|
+
expect(response.statusCode).toBe(200);
|
|
544
|
+
expect(issueRepository.closeIssueByUrl).toHaveBeenCalledWith(
|
|
545
|
+
'https://github.com/o/r/issues/1',
|
|
546
|
+
'not_planned',
|
|
547
|
+
);
|
|
548
|
+
expect(resolveProjectSpy).not.toHaveBeenCalled();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('closes a pull request via review without resolving the project', async () => {
|
|
552
|
+
const response = await handleReview(spiedContext, {
|
|
553
|
+
pjcode: 'umino',
|
|
554
|
+
action: 'close',
|
|
555
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
556
|
+
projectItemId: 'PVTI_noproj_reviewclose',
|
|
557
|
+
});
|
|
558
|
+
expect(response.statusCode).toBe(200);
|
|
559
|
+
expect(issueRepository.closePullRequest).toHaveBeenCalledWith(
|
|
560
|
+
'https://github.com/o/r/pull/1',
|
|
561
|
+
);
|
|
562
|
+
expect(resolveProjectSpy).not.toHaveBeenCalled();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('rejects a close whose pjcode is not configured without resolving the project', async () => {
|
|
566
|
+
const response = await handleTriage(spiedContext, {
|
|
567
|
+
pjcode: 'unconfigured',
|
|
568
|
+
action: 'close',
|
|
569
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
570
|
+
projectItemId: 'PVTI_noproj_badpj',
|
|
571
|
+
});
|
|
469
572
|
expect(response.statusCode).toBe(400);
|
|
573
|
+
expect(issueRepository.closeIssueByUrl).not.toHaveBeenCalled();
|
|
574
|
+
expect(resolveProjectSpy).not.toHaveBeenCalled();
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('sets intmux without a GraphQL item fetch while resolving the project', async () => {
|
|
578
|
+
const response = await handleIntmux(spiedContext, {
|
|
579
|
+
pjcode: 'umino',
|
|
580
|
+
action: 'set_intmux',
|
|
581
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
582
|
+
projectItemId: 'PVTI_needproj_intmux',
|
|
583
|
+
});
|
|
584
|
+
expect(response.statusCode).toBe(200);
|
|
585
|
+
expect(resolveProjectSpy).toHaveBeenCalledTimes(1);
|
|
586
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
587
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
588
|
+
project,
|
|
589
|
+
expect.objectContaining({ itemId: 'PVTI_needproj_intmux' }),
|
|
590
|
+
'status_intmux',
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('resolves the project for set_status but performs no GraphQL item fetch', async () => {
|
|
595
|
+
const response = await handleTriage(spiedContext, {
|
|
596
|
+
pjcode: 'umino',
|
|
597
|
+
action: 'set_status',
|
|
598
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
599
|
+
projectItemId: 'PVTI_needproj_status',
|
|
600
|
+
statusName: 'Todo',
|
|
601
|
+
});
|
|
602
|
+
expect(response.statusCode).toBe(200);
|
|
603
|
+
expect(resolveProjectSpy).toHaveBeenCalledTimes(1);
|
|
604
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
605
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledTimes(1);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('resolves the project for set_story but performs no GraphQL item fetch', async () => {
|
|
609
|
+
const response = await handleTriage(spiedContext, {
|
|
610
|
+
pjcode: 'umino',
|
|
611
|
+
action: 'set_story',
|
|
612
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
613
|
+
projectItemId: 'PVTI_needproj_story',
|
|
614
|
+
storyOptionId: 'story_opt_1',
|
|
615
|
+
});
|
|
616
|
+
expect(response.statusCode).toBe(200);
|
|
617
|
+
expect(resolveProjectSpy).toHaveBeenCalledTimes(1);
|
|
618
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
619
|
+
expect(issueRepository.updateStory).toHaveBeenCalledTimes(1);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('resolves the project for snooze and passes the item id without a GraphQL item fetch', async () => {
|
|
623
|
+
const response = await handleTriage(spiedContext, {
|
|
624
|
+
pjcode: 'umino',
|
|
625
|
+
action: 'snooze_1day',
|
|
626
|
+
issueUrl: 'https://github.com/o/r/issues/1',
|
|
627
|
+
projectItemId: 'PVTI_needproj_snooze',
|
|
628
|
+
});
|
|
629
|
+
expect(response.statusCode).toBe(200);
|
|
630
|
+
expect(resolveProjectSpy).toHaveBeenCalledTimes(1);
|
|
631
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
632
|
+
const call = issueRepository.updateNextActionDate.mock.calls[0];
|
|
633
|
+
expect(call[0]).toBe('https://github.com/o/r/issues/1');
|
|
634
|
+
expect(call[1]).toBe(project);
|
|
635
|
+
expect(call[2]).toBeInstanceOf(Date);
|
|
636
|
+
expect(call[3]).toBe('PVTI_needproj_snooze');
|
|
470
637
|
});
|
|
471
638
|
});
|
|
472
639
|
|
|
@@ -481,9 +648,13 @@ describe('consoleOperationApi', () => {
|
|
|
481
648
|
expect(response.statusCode).toBe(200);
|
|
482
649
|
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
483
650
|
project,
|
|
484
|
-
{
|
|
651
|
+
expect.objectContaining({
|
|
652
|
+
itemId: 'PVTI_i',
|
|
653
|
+
url: 'https://github.com/o/r/issues/1',
|
|
654
|
+
}),
|
|
485
655
|
'status_intmux',
|
|
486
656
|
);
|
|
657
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
487
658
|
expectRecordedAcrossTabs('PVTI_i');
|
|
488
659
|
});
|
|
489
660
|
|
|
@@ -497,7 +668,7 @@ describe('consoleOperationApi', () => {
|
|
|
497
668
|
expect(response.statusCode).toBe(400);
|
|
498
669
|
});
|
|
499
670
|
|
|
500
|
-
it('
|
|
671
|
+
it('sets intmux using the request project item id without a GraphQL item fetch', async () => {
|
|
501
672
|
issueRepository.get.mockResolvedValue(null);
|
|
502
673
|
const response = await handleIntmux(context, {
|
|
503
674
|
pjcode: 'umino',
|
|
@@ -505,7 +676,13 @@ describe('consoleOperationApi', () => {
|
|
|
505
676
|
issueUrl: 'https://github.com/o/r/issues/1',
|
|
506
677
|
projectItemId: 'PVTI_i',
|
|
507
678
|
});
|
|
508
|
-
expect(response.statusCode).toBe(
|
|
679
|
+
expect(response.statusCode).toBe(200);
|
|
680
|
+
expect(issueRepository.get).not.toHaveBeenCalled();
|
|
681
|
+
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
682
|
+
project,
|
|
683
|
+
expect.objectContaining({ itemId: 'PVTI_i' }),
|
|
684
|
+
'status_intmux',
|
|
685
|
+
);
|
|
509
686
|
});
|
|
510
687
|
|
|
511
688
|
it('rejects a missing issueUrl', async () => {
|
|
@@ -573,6 +750,8 @@ describe('consoleOperationApi', () => {
|
|
|
573
750
|
}
|
|
574
751
|
return null;
|
|
575
752
|
},
|
|
753
|
+
isPjcodeConfigured: (pjcode: string) =>
|
|
754
|
+
pjcode === 'umino' || pjcode === 'xmile',
|
|
576
755
|
consoleDataOutputDir: baseDir,
|
|
577
756
|
};
|
|
578
757
|
const response = await handleTriage(multiContext, {
|
|
@@ -585,7 +764,7 @@ describe('consoleOperationApi', () => {
|
|
|
585
764
|
expect(response.statusCode).toBe(200);
|
|
586
765
|
expect(issueRepository.updateStatus).toHaveBeenCalledWith(
|
|
587
766
|
otherProject,
|
|
588
|
-
{
|
|
767
|
+
expect.objectContaining({ itemId: 'PVTI_x' }),
|
|
589
768
|
'status_todo',
|
|
590
769
|
);
|
|
591
770
|
for (const tab of CONSOLE_DONE_TAB_NAMES) {
|
|
@@ -18,9 +18,12 @@ export type ConsoleProjectResolver = (
|
|
|
18
18
|
pjcode: string,
|
|
19
19
|
) => Promise<ConsoleProjectBinding | null>;
|
|
20
20
|
|
|
21
|
+
export type ConsolePjcodeValidator = (pjcode: string) => boolean;
|
|
22
|
+
|
|
21
23
|
export type ConsoleOperationContext = {
|
|
22
24
|
issueRepository: IssueRepository;
|
|
23
25
|
resolveProject: ConsoleProjectResolver;
|
|
26
|
+
isPjcodeConfigured: ConsolePjcodeValidator;
|
|
24
27
|
consoleDataOutputDir: string | null;
|
|
25
28
|
};
|
|
26
29
|
|
|
@@ -69,18 +72,40 @@ const resolveStatusId = (
|
|
|
69
72
|
return match ? match.id : null;
|
|
70
73
|
};
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// Builds the minimal Issue reference the ProjectV2 mutations need. The dashboard
|
|
76
|
+
// already supplies the project item id in the request body, and updateStatus,
|
|
77
|
+
// updateStory and updateNextActionHour read only `issue.itemId` (plus `url` for
|
|
78
|
+
// context). Constructing the reference locally avoids the GraphQL
|
|
79
|
+
// fetchProjectItemByUrl call that issueRepository.get would otherwise perform.
|
|
80
|
+
const projectItemReference = (
|
|
75
81
|
issueUrl: string,
|
|
76
82
|
projectItemId: string,
|
|
77
|
-
):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
): Issue => ({
|
|
84
|
+
nameWithOwner: '',
|
|
85
|
+
number: 0,
|
|
86
|
+
title: '',
|
|
87
|
+
state: 'OPEN',
|
|
88
|
+
status: null,
|
|
89
|
+
story: null,
|
|
90
|
+
nextActionDate: null,
|
|
91
|
+
nextActionHour: null,
|
|
92
|
+
estimationMinutes: null,
|
|
93
|
+
dependedIssueUrls: [],
|
|
94
|
+
completionDate50PercentConfidence: null,
|
|
95
|
+
url: issueUrl,
|
|
96
|
+
assignees: [],
|
|
97
|
+
labels: [],
|
|
98
|
+
org: '',
|
|
99
|
+
repo: '',
|
|
100
|
+
body: '',
|
|
101
|
+
itemId: projectItemId,
|
|
102
|
+
isPr: isPullRequestUrl(issueUrl),
|
|
103
|
+
isInProgress: false,
|
|
104
|
+
isClosed: false,
|
|
105
|
+
createdAt: new Date(0),
|
|
106
|
+
author: '',
|
|
107
|
+
closingIssueReferenceUrls: [],
|
|
108
|
+
});
|
|
84
109
|
|
|
85
110
|
const recordDone = (
|
|
86
111
|
context: ConsoleOperationContext,
|
|
@@ -117,6 +142,25 @@ const isOperationResponse = (
|
|
|
117
142
|
): value is ConsoleOperationResponse =>
|
|
118
143
|
Object.prototype.hasOwnProperty.call(value, 'statusCode');
|
|
119
144
|
|
|
145
|
+
// Validates that a pjcode is configured WITHOUT loading the ProjectV2 via
|
|
146
|
+
// GraphQL. Close operations only need the pjcode (for recordDone namespacing)
|
|
147
|
+
// and the issue/PR URL, both of which are handled through REST. Loading the
|
|
148
|
+
// full project here would make close fail whenever the GraphQL quota is
|
|
149
|
+
// exhausted, even though closing a GitHub issue or PR needs only REST.
|
|
150
|
+
const resolveConfiguredPjcode = (
|
|
151
|
+
context: ConsoleOperationContext,
|
|
152
|
+
body: Record<string, unknown>,
|
|
153
|
+
): string | ConsoleOperationResponse => {
|
|
154
|
+
const pjcode = body.pjcode;
|
|
155
|
+
if (!isNonEmptyString(pjcode)) {
|
|
156
|
+
return badRequest('pjcode is required');
|
|
157
|
+
}
|
|
158
|
+
if (!context.isPjcodeConfigured(pjcode)) {
|
|
159
|
+
return badRequest(`no project configured for pjcode "${pjcode}"`);
|
|
160
|
+
}
|
|
161
|
+
return pjcode;
|
|
162
|
+
};
|
|
163
|
+
|
|
120
164
|
const updateStatusByName = async (
|
|
121
165
|
issueRepository: IssueRepository,
|
|
122
166
|
project: Project,
|
|
@@ -128,16 +172,11 @@ const updateStatusByName = async (
|
|
|
128
172
|
if (statusId === null) {
|
|
129
173
|
return badRequest(`status option "${statusName}" not found in project`);
|
|
130
174
|
}
|
|
131
|
-
|
|
132
|
-
issueRepository,
|
|
175
|
+
await issueRepository.updateStatus(
|
|
133
176
|
project,
|
|
134
|
-
issueUrl,
|
|
135
|
-
|
|
177
|
+
projectItemReference(issueUrl, projectItemId),
|
|
178
|
+
statusId,
|
|
136
179
|
);
|
|
137
|
-
if (issue === null) {
|
|
138
|
-
return badRequest('issue not found');
|
|
139
|
-
}
|
|
140
|
-
await issueRepository.updateStatus(project, issue, statusId);
|
|
141
180
|
return null;
|
|
142
181
|
};
|
|
143
182
|
|
|
@@ -157,6 +196,20 @@ export const handleReview = async (
|
|
|
157
196
|
if (!isNonEmptyString(projectItemId)) {
|
|
158
197
|
return badRequest('projectItemId is required');
|
|
159
198
|
}
|
|
199
|
+
|
|
200
|
+
if (action === 'close') {
|
|
201
|
+
const pjcodeResult = resolveConfiguredPjcode(context, body);
|
|
202
|
+
if (typeof pjcodeResult !== 'string') {
|
|
203
|
+
return pjcodeResult;
|
|
204
|
+
}
|
|
205
|
+
await context.issueRepository.closePullRequest(prUrl);
|
|
206
|
+
if (isNonEmptyString(body.commentBody)) {
|
|
207
|
+
await context.issueRepository.createCommentByUrl(prUrl, body.commentBody);
|
|
208
|
+
}
|
|
209
|
+
recordDone(context, pjcodeResult, projectItemId);
|
|
210
|
+
return ok();
|
|
211
|
+
}
|
|
212
|
+
|
|
160
213
|
const binding = await resolveBinding(context, body);
|
|
161
214
|
if (isOperationResponse(binding)) {
|
|
162
215
|
return binding;
|
|
@@ -213,15 +266,6 @@ export const handleReview = async (
|
|
|
213
266
|
return ok();
|
|
214
267
|
}
|
|
215
268
|
|
|
216
|
-
if (action === 'close') {
|
|
217
|
-
await context.issueRepository.closePullRequest(prUrl);
|
|
218
|
-
if (isNonEmptyString(body.commentBody)) {
|
|
219
|
-
await context.issueRepository.createCommentByUrl(prUrl, body.commentBody);
|
|
220
|
-
}
|
|
221
|
-
recordDone(context, pjcode, projectItemId);
|
|
222
|
-
return ok();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
269
|
return badRequest(`unknown review action "${action}"`);
|
|
226
270
|
};
|
|
227
271
|
|
|
@@ -241,6 +285,30 @@ export const handleTriage = async (
|
|
|
241
285
|
if (!isNonEmptyString(projectItemId)) {
|
|
242
286
|
return badRequest('projectItemId is required');
|
|
243
287
|
}
|
|
288
|
+
|
|
289
|
+
if (action === 'close' || action === 'close_not_planned') {
|
|
290
|
+
const pjcodeResult = resolveConfiguredPjcode(context, body);
|
|
291
|
+
if (typeof pjcodeResult !== 'string') {
|
|
292
|
+
return pjcodeResult;
|
|
293
|
+
}
|
|
294
|
+
if (isNonEmptyString(body.commentBody)) {
|
|
295
|
+
await context.issueRepository.createCommentByUrl(
|
|
296
|
+
issueUrl,
|
|
297
|
+
body.commentBody,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (isPullRequestUrl(issueUrl)) {
|
|
301
|
+
await context.issueRepository.closePullRequest(issueUrl);
|
|
302
|
+
} else {
|
|
303
|
+
await context.issueRepository.closeIssueByUrl(
|
|
304
|
+
issueUrl,
|
|
305
|
+
action === 'close_not_planned' ? 'not_planned' : 'completed',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
recordDone(context, pjcodeResult, projectItemId);
|
|
309
|
+
return ok();
|
|
310
|
+
}
|
|
311
|
+
|
|
244
312
|
const binding = await resolveBinding(context, body);
|
|
245
313
|
if (isOperationResponse(binding)) {
|
|
246
314
|
return binding;
|
|
@@ -274,43 +342,15 @@ export const handleTriage = async (
|
|
|
274
342
|
if (project.story === null) {
|
|
275
343
|
return badRequest('project does not have a story field');
|
|
276
344
|
}
|
|
277
|
-
const issue = await loadIssueWithProjectItemId(
|
|
278
|
-
context.issueRepository,
|
|
279
|
-
project,
|
|
280
|
-
issueUrl,
|
|
281
|
-
projectItemId,
|
|
282
|
-
);
|
|
283
|
-
if (issue === null) {
|
|
284
|
-
return badRequest('issue not found');
|
|
285
|
-
}
|
|
286
345
|
await context.issueRepository.updateStory(
|
|
287
346
|
{ ...project, story: project.story },
|
|
288
|
-
|
|
347
|
+
projectItemReference(issueUrl, projectItemId),
|
|
289
348
|
storyOptionId,
|
|
290
349
|
);
|
|
291
350
|
recordDone(context, pjcode, projectItemId);
|
|
292
351
|
return ok();
|
|
293
352
|
}
|
|
294
353
|
|
|
295
|
-
if (action === 'close' || action === 'close_not_planned') {
|
|
296
|
-
if (isNonEmptyString(body.commentBody)) {
|
|
297
|
-
await context.issueRepository.createCommentByUrl(
|
|
298
|
-
issueUrl,
|
|
299
|
-
body.commentBody,
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
if (isPullRequestUrl(issueUrl)) {
|
|
303
|
-
await context.issueRepository.closePullRequest(issueUrl);
|
|
304
|
-
} else {
|
|
305
|
-
await context.issueRepository.closeIssueByUrl(
|
|
306
|
-
issueUrl,
|
|
307
|
-
action === 'close_not_planned' ? 'not_planned' : 'completed',
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
recordDone(context, pjcode, projectItemId);
|
|
311
|
-
return ok();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
354
|
if (action === 'snooze_1day' || action === 'snooze_1week') {
|
|
315
355
|
const days = action === 'snooze_1day' ? 1 : 7;
|
|
316
356
|
const target = new Date(Date.now() + days * 24 * 60 * 60 * 1000);
|
|
@@ -318,6 +358,7 @@ export const handleTriage = async (
|
|
|
318
358
|
issueUrl,
|
|
319
359
|
project,
|
|
320
360
|
target,
|
|
361
|
+
projectItemId,
|
|
321
362
|
);
|
|
322
363
|
recordDone(context, pjcode, projectItemId);
|
|
323
364
|
return ok();
|
|
@@ -3,6 +3,7 @@ import { Project } from '../../../domain/entities/Project';
|
|
|
3
3
|
import {
|
|
4
4
|
buildPjcodeToProjectUrl,
|
|
5
5
|
createConsoleProjectResolver,
|
|
6
|
+
createPjcodeConfigChecker,
|
|
6
7
|
} from './consoleProjectResolver';
|
|
7
8
|
|
|
8
9
|
describe('buildPjcodeToProjectUrl', () => {
|
|
@@ -39,6 +40,18 @@ describe('buildPjcodeToProjectUrl', () => {
|
|
|
39
40
|
});
|
|
40
41
|
});
|
|
41
42
|
|
|
43
|
+
describe('createPjcodeConfigChecker', () => {
|
|
44
|
+
it('reports true only for a configured pjcode without loading any project', () => {
|
|
45
|
+
const isConfigured = createPjcodeConfigChecker({
|
|
46
|
+
umino: 'https://github.com/orgs/umino/projects/1',
|
|
47
|
+
xmile: 'https://github.com/orgs/xmile/projects/2',
|
|
48
|
+
});
|
|
49
|
+
expect(isConfigured('umino')).toBe(true);
|
|
50
|
+
expect(isConfigured('xmile')).toBe(true);
|
|
51
|
+
expect(isConfigured('unknown')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
42
55
|
describe('createConsoleProjectResolver', () => {
|
|
43
56
|
const uminoProject: Project = { ...mock<Project>(), id: 'PVT_umino' };
|
|
44
57
|
const xmileProject: Project = { ...mock<Project>(), id: 'PVT_xmile' };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Project } from '../../../domain/entities/Project';
|
|
2
2
|
import {
|
|
3
|
+
ConsolePjcodeValidator,
|
|
3
4
|
ConsoleProjectBinding,
|
|
4
5
|
ConsoleProjectResolver,
|
|
5
6
|
} from './consoleOperationApi';
|
|
@@ -25,6 +26,16 @@ export const buildPjcodeToProjectUrl = (
|
|
|
25
26
|
return mapping;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
// Builds a synchronous predicate that reports whether a pjcode is configured,
|
|
30
|
+
// using only the local pjcode-to-project-url mapping. This lets close
|
|
31
|
+
// operations validate the pjcode without loading the ProjectV2 via GraphQL.
|
|
32
|
+
export const createPjcodeConfigChecker = (
|
|
33
|
+
pjcodeToProjectUrl: Record<string, string>,
|
|
34
|
+
): ConsolePjcodeValidator => {
|
|
35
|
+
return (pjcode: string): boolean =>
|
|
36
|
+
Object.prototype.hasOwnProperty.call(pjcodeToProjectUrl, pjcode);
|
|
37
|
+
};
|
|
38
|
+
|
|
28
39
|
export const createConsoleProjectResolver = (
|
|
29
40
|
pjcodeToProjectUrl: Record<string, string>,
|
|
30
41
|
loadProject: ConsoleProjectLoader,
|
|
@@ -505,6 +505,8 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
505
505
|
pjcode: string,
|
|
506
506
|
): Promise<ConsoleProjectBinding | null> =>
|
|
507
507
|
pjcode === CONSOLE_E2E_PJCODE ? { pjcode, project } : null;
|
|
508
|
+
const isPjcodeConfigured = (pjcode: string): boolean =>
|
|
509
|
+
pjcode === CONSOLE_E2E_PJCODE;
|
|
508
510
|
|
|
509
511
|
const reviewCommentCalls: ConsoleE2eReviewCommentCall[] = [];
|
|
510
512
|
const requestChangesCalls: ConsoleE2eRequestChangesCall[] = [];
|
|
@@ -518,6 +520,7 @@ export const startConsoleE2eHarness = async (): Promise<ConsoleE2eHarness> => {
|
|
|
518
520
|
requestChangesCalls,
|
|
519
521
|
),
|
|
520
522
|
resolveProject,
|
|
523
|
+
isPjcodeConfigured,
|
|
521
524
|
issueTitleStateCache: new IssueTitleStateCache(),
|
|
522
525
|
pullRequestStatusCache: new PullRequestStatusCache(),
|
|
523
526
|
inTmuxDataDir: null,
|
|
@@ -651,6 +651,7 @@ describe('webServer new routes integration', () => {
|
|
|
651
651
|
issueRepository,
|
|
652
652
|
resolveProject: async (pjcode) =>
|
|
653
653
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
654
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'umino',
|
|
654
655
|
port: 0,
|
|
655
656
|
});
|
|
656
657
|
try {
|
|
@@ -702,6 +703,7 @@ describe('webServer new routes integration', () => {
|
|
|
702
703
|
issueRepository,
|
|
703
704
|
resolveProject: async (pjcode) =>
|
|
704
705
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
706
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'umino',
|
|
705
707
|
port: 0,
|
|
706
708
|
});
|
|
707
709
|
try {
|
|
@@ -750,6 +752,7 @@ describe('webServer new routes integration', () => {
|
|
|
750
752
|
issueRepository,
|
|
751
753
|
resolveProject: async (pjcode) =>
|
|
752
754
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
755
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'umino',
|
|
753
756
|
port: 0,
|
|
754
757
|
});
|
|
755
758
|
try {
|
|
@@ -788,6 +791,7 @@ describe('webServer new routes integration', () => {
|
|
|
788
791
|
issueRepository,
|
|
789
792
|
resolveProject: async (pjcode) =>
|
|
790
793
|
pjcode === 'umino' ? { pjcode, project: buildProject() } : null,
|
|
794
|
+
isPjcodeConfigured: (pjcode) => pjcode === 'umino',
|
|
791
795
|
port: 0,
|
|
792
796
|
});
|
|
793
797
|
try {
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from './consoleReadApi';
|
|
21
21
|
import {
|
|
22
22
|
ConsoleOperationContext,
|
|
23
|
+
ConsolePjcodeValidator,
|
|
23
24
|
ConsoleProjectResolver,
|
|
24
25
|
handleComment,
|
|
25
26
|
handleIntmux,
|
|
@@ -173,6 +174,7 @@ export type WebServerOptions = {
|
|
|
173
174
|
imageFetcher?: ImageFetcher | null;
|
|
174
175
|
issueRepository?: IssueRepository | null;
|
|
175
176
|
resolveProject?: ConsoleProjectResolver | null;
|
|
177
|
+
isPjcodeConfigured?: ConsolePjcodeValidator | null;
|
|
176
178
|
issueTitleStateCache?: IssueTitleStateCache | null;
|
|
177
179
|
pullRequestStatusCache?: PullRequestStatusCache | null;
|
|
178
180
|
};
|
|
@@ -430,12 +432,18 @@ const handleOperationApi = async (
|
|
|
430
432
|
): Promise<{ statusCode: number; body: unknown } | null> => {
|
|
431
433
|
const issueRepository = options.issueRepository ?? null;
|
|
432
434
|
const resolveProject = options.resolveProject ?? null;
|
|
433
|
-
|
|
435
|
+
const isPjcodeConfigured = options.isPjcodeConfigured ?? null;
|
|
436
|
+
if (
|
|
437
|
+
issueRepository === null ||
|
|
438
|
+
resolveProject === null ||
|
|
439
|
+
isPjcodeConfigured === null
|
|
440
|
+
) {
|
|
434
441
|
return null;
|
|
435
442
|
}
|
|
436
443
|
const context: ConsoleOperationContext = {
|
|
437
444
|
issueRepository,
|
|
438
445
|
resolveProject,
|
|
446
|
+
isPjcodeConfigured,
|
|
439
447
|
consoleDataOutputDir: options.consoleDataOutputDir,
|
|
440
448
|
};
|
|
441
449
|
const dispatched = dispatchOperation(context, requestPath, body);
|