github-issue-tower-defence-management 1.89.0 → 1.91.0
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/.github/CODEOWNERS +1 -2
- package/CHANGELOG.md +14 -0
- package/README.md +15 -2
- package/bin/adapter/entry-points/cli/index.js +16 -12
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
- package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
- package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
- package/bin/adapter/entry-points/console/consoleServer.js +43 -17
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/jest.config.js +57 -9
- package/package.json +17 -13
- package/src/adapter/entry-points/cli/index.test.ts +12 -2
- package/src/adapter/entry-points/cli/index.ts +30 -12
- package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
- package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
- package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
- package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
- package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
- package/src/adapter/entry-points/console/consoleServer.ts +53 -23
- package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
- package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
- package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
- package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
- package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
- package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
- package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
- package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
- package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
- package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
- package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
- package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
- package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
- package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
- package/src/adapter/entry-points/console/ui/src/index.css +352 -2
- package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
- package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
- package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
- package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
- package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
- package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { useConsoleToken } from './useConsoleToken';
|
|
3
|
+
|
|
4
|
+
describe('useConsoleToken', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear();
|
|
7
|
+
window.history.replaceState({}, '', '/');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('reads the token from the query string and appends it to data urls', () => {
|
|
11
|
+
window.history.replaceState({}, '', '/?k=secret');
|
|
12
|
+
const { result } = renderHook(() => useConsoleToken());
|
|
13
|
+
expect(result.current.token).toBe('secret');
|
|
14
|
+
expect(result.current.appendToken('./prs/list.json')).toBe(
|
|
15
|
+
'./prs/list.json?k=secret',
|
|
16
|
+
);
|
|
17
|
+
expect(result.current.appendToken('./api/itembody?url=x')).toBe(
|
|
18
|
+
'./api/itembody?url=x&k=secret',
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('persists the token to localStorage', () => {
|
|
23
|
+
window.history.replaceState({}, '', '/?k=persisted');
|
|
24
|
+
renderHook(() => useConsoleToken());
|
|
25
|
+
expect(localStorage.getItem('tdpm-console-token')).toBe('persisted');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('reads a persisted token when no query token exists', () => {
|
|
29
|
+
localStorage.setItem('tdpm-console-token', 'stored');
|
|
30
|
+
const { result } = renderHook(() => useConsoleToken());
|
|
31
|
+
expect(result.current.token).toBe('stored');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns the url unchanged when there is no token', () => {
|
|
35
|
+
const { result } = renderHook(() => useConsoleToken());
|
|
36
|
+
act(() => {});
|
|
37
|
+
expect(result.current.appendToken('./prs/list.json')).toBe(
|
|
38
|
+
'./prs/list.json',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { CONSOLE_ITEM_ICONS, resolveConsoleItemIconKind } from './itemIcons';
|
|
2
|
+
|
|
3
|
+
describe('resolveConsoleItemIconKind', () => {
|
|
4
|
+
it('resolves a draft pull request to prDraft', () => {
|
|
5
|
+
expect(
|
|
6
|
+
resolveConsoleItemIconKind({
|
|
7
|
+
isPr: true,
|
|
8
|
+
state: 'open',
|
|
9
|
+
merged: false,
|
|
10
|
+
isDraft: true,
|
|
11
|
+
stateReason: '',
|
|
12
|
+
}),
|
|
13
|
+
).toBe('prDraft');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resolves a merged pull request to prMerged', () => {
|
|
17
|
+
expect(
|
|
18
|
+
resolveConsoleItemIconKind({
|
|
19
|
+
isPr: true,
|
|
20
|
+
state: 'closed',
|
|
21
|
+
merged: true,
|
|
22
|
+
isDraft: false,
|
|
23
|
+
stateReason: '',
|
|
24
|
+
}),
|
|
25
|
+
).toBe('prMerged');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('resolves a closed pull request to prClosed', () => {
|
|
29
|
+
expect(
|
|
30
|
+
resolveConsoleItemIconKind({
|
|
31
|
+
isPr: true,
|
|
32
|
+
state: 'closed',
|
|
33
|
+
merged: false,
|
|
34
|
+
isDraft: false,
|
|
35
|
+
stateReason: '',
|
|
36
|
+
}),
|
|
37
|
+
).toBe('prClosed');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('resolves an open pull request to prOpen', () => {
|
|
41
|
+
expect(
|
|
42
|
+
resolveConsoleItemIconKind({
|
|
43
|
+
isPr: true,
|
|
44
|
+
state: 'open',
|
|
45
|
+
merged: false,
|
|
46
|
+
isDraft: false,
|
|
47
|
+
stateReason: '',
|
|
48
|
+
}),
|
|
49
|
+
).toBe('prOpen');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('resolves a completed closed issue to issueClosed', () => {
|
|
53
|
+
expect(
|
|
54
|
+
resolveConsoleItemIconKind({
|
|
55
|
+
isPr: false,
|
|
56
|
+
state: 'closed',
|
|
57
|
+
merged: false,
|
|
58
|
+
isDraft: false,
|
|
59
|
+
stateReason: 'completed',
|
|
60
|
+
}),
|
|
61
|
+
).toBe('issueClosed');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('resolves a not-planned closed issue to issueClosedNotPlanned', () => {
|
|
65
|
+
expect(
|
|
66
|
+
resolveConsoleItemIconKind({
|
|
67
|
+
isPr: false,
|
|
68
|
+
state: 'closed',
|
|
69
|
+
merged: false,
|
|
70
|
+
isDraft: false,
|
|
71
|
+
stateReason: 'not_planned',
|
|
72
|
+
}),
|
|
73
|
+
).toBe('issueClosedNotPlanned');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('resolves an open issue to issueOpen', () => {
|
|
77
|
+
expect(
|
|
78
|
+
resolveConsoleItemIconKind({
|
|
79
|
+
isPr: false,
|
|
80
|
+
state: 'open',
|
|
81
|
+
merged: false,
|
|
82
|
+
isDraft: false,
|
|
83
|
+
stateReason: '',
|
|
84
|
+
}),
|
|
85
|
+
).toBe('issueOpen');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses the documented colors for each kind', () => {
|
|
89
|
+
expect(CONSOLE_ITEM_ICONS.prDraft.color).toBe('#8b949e');
|
|
90
|
+
expect(CONSOLE_ITEM_ICONS.prMerged.color).toBe('#a371f7');
|
|
91
|
+
expect(CONSOLE_ITEM_ICONS.prClosed.color).toBe('#f85149');
|
|
92
|
+
expect(CONSOLE_ITEM_ICONS.prOpen.color).toBe('#3fb950');
|
|
93
|
+
expect(CONSOLE_ITEM_ICONS.issueClosedNotPlanned.color).toBe('#848d97');
|
|
94
|
+
expect(CONSOLE_ITEM_ICONS.issueClosed.color).toBe('#a371f7');
|
|
95
|
+
expect(CONSOLE_ITEM_ICONS.issueOpen.color).toBe('#3fb950');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type ConsoleItemIconKind =
|
|
2
|
+
| 'issueOpen'
|
|
3
|
+
| 'issueClosed'
|
|
4
|
+
| 'issueClosedNotPlanned'
|
|
5
|
+
| 'prOpen'
|
|
6
|
+
| 'prMerged'
|
|
7
|
+
| 'prClosed'
|
|
8
|
+
| 'prDraft';
|
|
9
|
+
|
|
10
|
+
export type ConsoleItemIconDefinition = {
|
|
11
|
+
color: string;
|
|
12
|
+
paths: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const CONSOLE_ITEM_ICONS: Record<
|
|
16
|
+
ConsoleItemIconKind,
|
|
17
|
+
ConsoleItemIconDefinition
|
|
18
|
+
> = {
|
|
19
|
+
issueOpen: {
|
|
20
|
+
color: '#3fb950',
|
|
21
|
+
paths: [
|
|
22
|
+
'M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z',
|
|
23
|
+
'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
issueClosed: {
|
|
27
|
+
color: '#a371f7',
|
|
28
|
+
paths: [
|
|
29
|
+
'M11.28 6.78a.75.75 0 0 0-1.06-1.06L7.25 8.69 5.78 7.22a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.06 0l3.5-3.5Z',
|
|
30
|
+
'M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1.5 0a6.5 6.5 0 1 0-13 0 6.5 6.5 0 0 0 13 0Z',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
issueClosedNotPlanned: {
|
|
34
|
+
color: '#848d97',
|
|
35
|
+
paths: [
|
|
36
|
+
'M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Zm9.78-2.22-5.5 5.5a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l5.5-5.5a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042Z',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
prOpen: {
|
|
40
|
+
color: '#3fb950',
|
|
41
|
+
paths: [
|
|
42
|
+
'M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z',
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
prMerged: {
|
|
46
|
+
color: '#a371f7',
|
|
47
|
+
paths: [
|
|
48
|
+
'M5.45 5.154A4.25 4.25 0 0 0 9.25 7.5h1.378a2.251 2.251 0 1 1 0 1.5H9.25A5.734 5.734 0 0 1 5 7.123v3.505a2.25 2.25 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.95-.218ZM4.25 13.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm8.5-4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5ZM5 3.25a.75.75 0 1 0 0 .005V3.25Z',
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
prClosed: {
|
|
52
|
+
color: '#f85149',
|
|
53
|
+
paths: [
|
|
54
|
+
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 5.5a.75.75 0 0 1 .75.75v3.378a2.251 2.251 0 1 1-1.5 0V7.25a.75.75 0 0 1 .75-.75Zm-2.03-5.273a.75.75 0 0 1 1.06 0l.97.97.97-.97a.748.748 0 0 1 1.265.332.75.75 0 0 1-.205.729l-.97.97.97.97a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018l-.97-.97-.97.97a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734l.97-.97-.97-.97a.75.75 0 0 1 0-1.06ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z',
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
prDraft: {
|
|
58
|
+
color: '#8b949e',
|
|
59
|
+
paths: [
|
|
60
|
+
'M3.25 1A2.25 2.25 0 0 1 4 5.372v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.251 2.251 0 0 1 3.25 1Zm9.5 14a2.25 2.25 0 1 1 0-4.5 2.25 2.25 0 0 1 0 4.5ZM2.5 3.25a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0ZM3.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm9.5 0a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM14 7.5a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Zm0-4.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type ConsoleItemIconInput = {
|
|
66
|
+
isPr: boolean;
|
|
67
|
+
state: string;
|
|
68
|
+
merged: boolean;
|
|
69
|
+
isDraft: boolean;
|
|
70
|
+
stateReason: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const resolveConsoleItemIconKind = ({
|
|
74
|
+
isPr,
|
|
75
|
+
state,
|
|
76
|
+
merged,
|
|
77
|
+
isDraft,
|
|
78
|
+
stateReason,
|
|
79
|
+
}: ConsoleItemIconInput): ConsoleItemIconKind => {
|
|
80
|
+
if (isPr) {
|
|
81
|
+
if (isDraft) {
|
|
82
|
+
return 'prDraft';
|
|
83
|
+
}
|
|
84
|
+
if (merged) {
|
|
85
|
+
return 'prMerged';
|
|
86
|
+
}
|
|
87
|
+
return state === 'closed' ? 'prClosed' : 'prOpen';
|
|
88
|
+
}
|
|
89
|
+
if (state === 'closed') {
|
|
90
|
+
return stateReason === 'not_planned'
|
|
91
|
+
? 'issueClosedNotPlanned'
|
|
92
|
+
: 'issueClosed';
|
|
93
|
+
}
|
|
94
|
+
return 'issueOpen';
|
|
95
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createConsoleApiClient, postConsoleOperation } from './consoleApi';
|
|
2
|
+
|
|
3
|
+
const appendToken = (url: string): string =>
|
|
4
|
+
url.includes('?') ? `${url}&k=token` : `${url}?k=token`;
|
|
5
|
+
|
|
6
|
+
const mockFetchOnce = (body: unknown, ok = true): jest.Mock => {
|
|
7
|
+
const fetchMock = jest.fn().mockResolvedValue({
|
|
8
|
+
ok,
|
|
9
|
+
status: ok ? 200 : 500,
|
|
10
|
+
json: async () => body,
|
|
11
|
+
});
|
|
12
|
+
global.fetch = fetchMock as unknown as typeof fetch;
|
|
13
|
+
return fetchMock;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('createConsoleApiClient', () => {
|
|
17
|
+
it('reads the item body and appends the token to the url query', async () => {
|
|
18
|
+
const fetchMock = mockFetchOnce({ body: '# Title' });
|
|
19
|
+
const client = createConsoleApiClient(appendToken);
|
|
20
|
+
const body = await client.fetchItemBody('https://github.com/o/r/issues/1');
|
|
21
|
+
expect(body).toBe('# Title');
|
|
22
|
+
const requested = fetchMock.mock.calls[0][0] as string;
|
|
23
|
+
expect(requested).toContain('./api/itembody?url=');
|
|
24
|
+
expect(requested).toContain('&k=token');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('parses comments', async () => {
|
|
28
|
+
mockFetchOnce({
|
|
29
|
+
comments: [
|
|
30
|
+
{ author: 'a', body: 'hello', createdAt: '2026-06-19T00:00:00.000Z' },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
const client = createConsoleApiClient(appendToken);
|
|
34
|
+
const comments = await client.fetchComments(
|
|
35
|
+
'https://github.com/o/r/issues/1',
|
|
36
|
+
);
|
|
37
|
+
expect(comments).toEqual([
|
|
38
|
+
{ author: 'a', body: 'hello', createdAt: '2026-06-19T00:00:00.000Z' },
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parses changed files supporting path and filename keys', async () => {
|
|
43
|
+
mockFetchOnce({
|
|
44
|
+
files: [
|
|
45
|
+
{ path: 'a.ts', additions: 3, deletions: 1, status: 'modified' },
|
|
46
|
+
{ filename: 'b.ts', additions: 9, deletions: 0, status: 'added' },
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
const client = createConsoleApiClient(appendToken);
|
|
50
|
+
const files = await client.fetchPrFiles('https://github.com/o/r/pull/1');
|
|
51
|
+
expect(files.map((file) => file.path)).toEqual(['a.ts', 'b.ts']);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns no files when the response files array is null', async () => {
|
|
55
|
+
mockFetchOnce({ files: null });
|
|
56
|
+
const client = createConsoleApiClient(appendToken);
|
|
57
|
+
expect(await client.fetchPrFiles('https://github.com/o/r/pull/1')).toEqual(
|
|
58
|
+
[],
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('parses the issue state', async () => {
|
|
63
|
+
mockFetchOnce({ state: 'closed', merged: true, isPullRequest: true });
|
|
64
|
+
const client = createConsoleApiClient(appendToken);
|
|
65
|
+
expect(
|
|
66
|
+
await client.fetchIssueState('https://github.com/o/r/pull/1'),
|
|
67
|
+
).toEqual({ state: 'closed', merged: true, isPullRequest: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('parses pull request commits', async () => {
|
|
71
|
+
mockFetchOnce({
|
|
72
|
+
commits: [
|
|
73
|
+
{
|
|
74
|
+
sha: 'abc1234',
|
|
75
|
+
message: 'fix: thing',
|
|
76
|
+
author: 'dev',
|
|
77
|
+
authoredAt: '2026-06-19T00:00:00.000Z',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
const client = createConsoleApiClient(appendToken);
|
|
82
|
+
const commits = await client.fetchPrCommits(
|
|
83
|
+
'https://github.com/o/r/pull/1',
|
|
84
|
+
);
|
|
85
|
+
expect(commits[0].sha).toBe('abc1234');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('parses related pull requests with summaries', async () => {
|
|
89
|
+
mockFetchOnce({
|
|
90
|
+
relatedPullRequests: [
|
|
91
|
+
{
|
|
92
|
+
url: 'https://github.com/o/r/pull/9',
|
|
93
|
+
branchName: 'feat',
|
|
94
|
+
createdAt: '2026-06-19T00:00:00.000Z',
|
|
95
|
+
isDraft: false,
|
|
96
|
+
isConflicted: false,
|
|
97
|
+
isPassedAllCiJob: true,
|
|
98
|
+
isCiStateSuccess: true,
|
|
99
|
+
isResolvedAllReviewComments: true,
|
|
100
|
+
isBranchOutOfDate: false,
|
|
101
|
+
missingRequiredCheckNames: ['build'],
|
|
102
|
+
summary: {
|
|
103
|
+
title: 'Linked',
|
|
104
|
+
body: 'body',
|
|
105
|
+
additions: 10,
|
|
106
|
+
deletions: 2,
|
|
107
|
+
changedFiles: 3,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
const client = createConsoleApiClient(appendToken);
|
|
113
|
+
const related = await client.fetchRelatedPrs(
|
|
114
|
+
'https://github.com/o/r/issues/1',
|
|
115
|
+
);
|
|
116
|
+
expect(related[0].url).toBe('https://github.com/o/r/pull/9');
|
|
117
|
+
expect(related[0].summary?.changedFiles).toBe(3);
|
|
118
|
+
expect(related[0].missingRequiredCheckNames).toEqual(['build']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws on a non-ok response', async () => {
|
|
122
|
+
mockFetchOnce({}, false);
|
|
123
|
+
const client = createConsoleApiClient(appendToken);
|
|
124
|
+
await expect(
|
|
125
|
+
client.fetchComments('https://github.com/o/r/issues/1'),
|
|
126
|
+
).rejects.toThrow('HTTP 500');
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('postConsoleOperation', () => {
|
|
131
|
+
it('posts a JSON body and appends the token', async () => {
|
|
132
|
+
const fetchMock = mockFetchOnce({ ok: true });
|
|
133
|
+
await postConsoleOperation(appendToken, '/api/review', {
|
|
134
|
+
pjcode: 'umino',
|
|
135
|
+
action: 'approve',
|
|
136
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
137
|
+
projectItemId: 'PVTI_1',
|
|
138
|
+
});
|
|
139
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
140
|
+
expect(url).toBe('/api/review?k=token');
|
|
141
|
+
expect(init).toMatchObject({ method: 'POST' });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('throws on a failed operation', async () => {
|
|
145
|
+
mockFetchOnce({}, false);
|
|
146
|
+
await expect(
|
|
147
|
+
postConsoleOperation(appendToken, '/api/review', {
|
|
148
|
+
pjcode: 'umino',
|
|
149
|
+
action: 'approve',
|
|
150
|
+
prUrl: 'https://github.com/o/r/pull/1',
|
|
151
|
+
projectItemId: 'PVTI_1',
|
|
152
|
+
}),
|
|
153
|
+
).rejects.toThrow('HTTP 500');
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConsoleChangedFile,
|
|
3
|
+
ConsoleComment,
|
|
4
|
+
ConsoleCommit,
|
|
5
|
+
ConsoleIssueState,
|
|
6
|
+
ConsoleRelatedPullRequest,
|
|
7
|
+
} from '../types';
|
|
8
|
+
|
|
9
|
+
export type ConsoleApiClient = {
|
|
10
|
+
fetchItemBody: (url: string) => Promise<string>;
|
|
11
|
+
fetchComments: (url: string) => Promise<ConsoleComment[]>;
|
|
12
|
+
fetchPrFiles: (url: string) => Promise<ConsoleChangedFile[]>;
|
|
13
|
+
fetchPrCommits: (url: string) => Promise<ConsoleCommit[]>;
|
|
14
|
+
fetchRelatedPrs: (url: string) => Promise<ConsoleRelatedPullRequest[]>;
|
|
15
|
+
fetchIssueState: (url: string) => Promise<ConsoleIssueState>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ConsoleReviewRequest = {
|
|
19
|
+
pjcode: string;
|
|
20
|
+
action: string;
|
|
21
|
+
prUrl: string;
|
|
22
|
+
projectItemId: string;
|
|
23
|
+
commentBody?: string;
|
|
24
|
+
changedFilePath?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type ConsoleTriageRequest = {
|
|
28
|
+
pjcode: string;
|
|
29
|
+
action: string;
|
|
30
|
+
issueUrl: string;
|
|
31
|
+
projectItemId: string;
|
|
32
|
+
statusName?: string;
|
|
33
|
+
storyOptionId?: string;
|
|
34
|
+
commentBody?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type ConsoleIntmuxRequest = {
|
|
38
|
+
pjcode: string;
|
|
39
|
+
action: 'set_intmux';
|
|
40
|
+
issueUrl: string;
|
|
41
|
+
projectItemId: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type AppendToken = (url: string) => string;
|
|
45
|
+
|
|
46
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
47
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
48
|
+
|
|
49
|
+
const getString = (value: unknown): string =>
|
|
50
|
+
typeof value === 'string' ? value : '';
|
|
51
|
+
|
|
52
|
+
const getNumber = (value: unknown): number =>
|
|
53
|
+
typeof value === 'number' ? value : 0;
|
|
54
|
+
|
|
55
|
+
const getBoolean = (value: unknown): boolean => value === true;
|
|
56
|
+
|
|
57
|
+
const requestJson = async (
|
|
58
|
+
appendToken: AppendToken,
|
|
59
|
+
apiPath: string,
|
|
60
|
+
resourceUrl: string,
|
|
61
|
+
): Promise<unknown> => {
|
|
62
|
+
const target = appendToken(
|
|
63
|
+
`${apiPath}?url=${encodeURIComponent(resourceUrl)}`,
|
|
64
|
+
);
|
|
65
|
+
const response = await fetch(target);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`HTTP ${response.status}`);
|
|
68
|
+
}
|
|
69
|
+
return response.json();
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const parseComments = (payload: unknown): ConsoleComment[] => {
|
|
73
|
+
if (!isRecord(payload) || !Array.isArray(payload.comments)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
return payload.comments.filter(isRecord).map((comment) => ({
|
|
77
|
+
author: getString(comment.author),
|
|
78
|
+
body: getString(comment.body),
|
|
79
|
+
createdAt: getString(comment.createdAt),
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const parseFiles = (payload: unknown): ConsoleChangedFile[] => {
|
|
84
|
+
if (!isRecord(payload) || !Array.isArray(payload.files)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return payload.files.filter(isRecord).map((file) => ({
|
|
88
|
+
path: getString(file.path) || getString(file.filename),
|
|
89
|
+
additions: getNumber(file.additions),
|
|
90
|
+
deletions: getNumber(file.deletions),
|
|
91
|
+
status: getString(file.status),
|
|
92
|
+
patch: typeof file.patch === 'string' ? file.patch : null,
|
|
93
|
+
}));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const parseCommits = (payload: unknown): ConsoleCommit[] => {
|
|
97
|
+
if (!isRecord(payload) || !Array.isArray(payload.commits)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
return payload.commits.filter(isRecord).map((commit) => ({
|
|
101
|
+
sha: getString(commit.sha),
|
|
102
|
+
message: getString(commit.message),
|
|
103
|
+
author: getString(commit.author),
|
|
104
|
+
authoredAt: getString(commit.authoredAt),
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const parseSummary = (value: unknown): ConsoleRelatedPullRequest['summary'] => {
|
|
109
|
+
if (!isRecord(value)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
title: getString(value.title),
|
|
114
|
+
body: getString(value.body),
|
|
115
|
+
additions: getNumber(value.additions),
|
|
116
|
+
deletions: getNumber(value.deletions),
|
|
117
|
+
changedFiles: getNumber(value.changedFiles),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const parseRelatedPrs = (payload: unknown): ConsoleRelatedPullRequest[] => {
|
|
122
|
+
if (!isRecord(payload) || !Array.isArray(payload.relatedPullRequests)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return payload.relatedPullRequests.filter(isRecord).map((pr) => ({
|
|
126
|
+
url: getString(pr.url),
|
|
127
|
+
branchName: typeof pr.branchName === 'string' ? pr.branchName : null,
|
|
128
|
+
createdAt: getString(pr.createdAt),
|
|
129
|
+
isDraft: getBoolean(pr.isDraft),
|
|
130
|
+
isConflicted: getBoolean(pr.isConflicted),
|
|
131
|
+
isPassedAllCiJob: getBoolean(pr.isPassedAllCiJob),
|
|
132
|
+
isCiStateSuccess: getBoolean(pr.isCiStateSuccess),
|
|
133
|
+
isResolvedAllReviewComments: getBoolean(pr.isResolvedAllReviewComments),
|
|
134
|
+
isBranchOutOfDate: getBoolean(pr.isBranchOutOfDate),
|
|
135
|
+
missingRequiredCheckNames: Array.isArray(pr.missingRequiredCheckNames)
|
|
136
|
+
? pr.missingRequiredCheckNames.filter(
|
|
137
|
+
(name): name is string => typeof name === 'string',
|
|
138
|
+
)
|
|
139
|
+
: [],
|
|
140
|
+
summary: parseSummary(pr.summary),
|
|
141
|
+
}));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const parseState = (payload: unknown): ConsoleIssueState => {
|
|
145
|
+
if (!isRecord(payload)) {
|
|
146
|
+
return { state: 'open', merged: false, isPullRequest: false };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
state: getString(payload.state) || 'open',
|
|
150
|
+
merged: getBoolean(payload.merged),
|
|
151
|
+
isPullRequest: getBoolean(payload.isPullRequest),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const createConsoleApiClient = (
|
|
156
|
+
appendToken: AppendToken,
|
|
157
|
+
): ConsoleApiClient => ({
|
|
158
|
+
fetchItemBody: async (url) => {
|
|
159
|
+
const payload = await requestJson(appendToken, './api/itembody', url);
|
|
160
|
+
return isRecord(payload) ? getString(payload.body) : '';
|
|
161
|
+
},
|
|
162
|
+
fetchComments: async (url) =>
|
|
163
|
+
parseComments(await requestJson(appendToken, './api/comments', url)),
|
|
164
|
+
fetchPrFiles: async (url) =>
|
|
165
|
+
parseFiles(await requestJson(appendToken, './api/prfiles', url)),
|
|
166
|
+
fetchPrCommits: async (url) =>
|
|
167
|
+
parseCommits(await requestJson(appendToken, './api/prcommits', url)),
|
|
168
|
+
fetchRelatedPrs: async (url) =>
|
|
169
|
+
parseRelatedPrs(await requestJson(appendToken, './api/relatedprs', url)),
|
|
170
|
+
fetchIssueState: async (url) =>
|
|
171
|
+
parseState(await requestJson(appendToken, './api/issuetitle', url)),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const postConsoleOperation = async (
|
|
175
|
+
appendToken: AppendToken,
|
|
176
|
+
apiPath: string,
|
|
177
|
+
body: ConsoleReviewRequest | ConsoleTriageRequest | ConsoleIntmuxRequest,
|
|
178
|
+
): Promise<void> => {
|
|
179
|
+
const response = await fetch(appendToken(apiPath), {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: { 'Content-Type': 'application/json' },
|
|
182
|
+
body: JSON.stringify(body),
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
throw new Error(`HTTP ${response.status}`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConsoleMarkdownSegment,
|
|
3
|
+
hasMermaidFence,
|
|
4
|
+
renderMarkdownToSafeHtml,
|
|
5
|
+
splitMarkdownSegments,
|
|
6
|
+
} from './markdown';
|
|
7
|
+
|
|
8
|
+
const stripKey = (segment: ConsoleMarkdownSegment): Record<string, unknown> => {
|
|
9
|
+
if (segment.kind === 'mermaid') {
|
|
10
|
+
return { kind: segment.kind, code: segment.code };
|
|
11
|
+
}
|
|
12
|
+
return { kind: segment.kind, source: segment.source };
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('renderMarkdownToSafeHtml', () => {
|
|
16
|
+
it('renders headings and lists', () => {
|
|
17
|
+
const html = renderMarkdownToSafeHtml('# Title\n\n- one\n- two');
|
|
18
|
+
expect(html).toContain('<h1');
|
|
19
|
+
expect(html).toContain('<li>one</li>');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('strips script tags via DOMPurify', () => {
|
|
23
|
+
const html = renderMarkdownToSafeHtml(
|
|
24
|
+
'before <script>alert(1)</script> after',
|
|
25
|
+
);
|
|
26
|
+
expect(html).not.toContain('<script>');
|
|
27
|
+
expect(html).not.toContain('alert(1)');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('strips event-handler attributes', () => {
|
|
31
|
+
const html = renderMarkdownToSafeHtml('<img src="x" onerror="alert(1)">');
|
|
32
|
+
expect(html).not.toContain('onerror');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('returns an empty string for blank input', () => {
|
|
36
|
+
expect(renderMarkdownToSafeHtml(' ')).toBe('');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('splitMarkdownSegments', () => {
|
|
41
|
+
it('separates a mermaid fence from surrounding markdown', () => {
|
|
42
|
+
const segments = splitMarkdownSegments(
|
|
43
|
+
'intro\n\n```mermaid\ngraph TD; A-->B;\n```\n\noutro',
|
|
44
|
+
);
|
|
45
|
+
expect(segments.map(stripKey)).toEqual([
|
|
46
|
+
{ kind: 'markdown', source: 'intro\n' },
|
|
47
|
+
{ kind: 'mermaid', code: 'graph TD; A-->B;' },
|
|
48
|
+
{ kind: 'markdown', source: '\noutro' },
|
|
49
|
+
]);
|
|
50
|
+
expect(new Set(segments.map((segment) => segment.key)).size).toBe(3);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps plain markdown as a single segment', () => {
|
|
54
|
+
const segments = splitMarkdownSegments('just text');
|
|
55
|
+
expect(segments.map(stripKey)).toEqual([
|
|
56
|
+
{ kind: 'markdown', source: 'just text' },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps an unterminated mermaid fence as markdown', () => {
|
|
61
|
+
const segments = splitMarkdownSegments('```mermaid\ngraph TD; A-->B;');
|
|
62
|
+
expect(segments.map(stripKey)).toEqual([
|
|
63
|
+
{ kind: 'markdown', source: '```mermaid\ngraph TD; A-->B;' },
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('hasMermaidFence', () => {
|
|
69
|
+
it('detects a mermaid fence', () => {
|
|
70
|
+
expect(hasMermaidFence('```mermaid\ngraph TD; A-->B;\n```')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('returns false without a mermaid fence', () => {
|
|
74
|
+
expect(hasMermaidFence('```ts\nconst a = 1;\n```')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
});
|