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.
Files changed (160) hide show
  1. package/.github/CODEOWNERS +1 -2
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +15 -2
  4. package/bin/adapter/entry-points/cli/index.js +16 -12
  5. package/bin/adapter/entry-points/cli/index.js.map +1 -1
  6. package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
  7. package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
  8. package/bin/adapter/entry-points/console/consoleOperationApi.js +54 -27
  9. package/bin/adapter/entry-points/console/consoleOperationApi.js.map +1 -1
  10. package/bin/adapter/entry-points/console/consoleProjectResolver.js +38 -0
  11. package/bin/adapter/entry-points/console/consoleProjectResolver.js.map +1 -0
  12. package/bin/adapter/entry-points/console/consoleServer.js +43 -17
  13. package/bin/adapter/entry-points/console/consoleServer.js.map +1 -1
  14. package/bin/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  15. package/bin/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  16. package/bin/adapter/entry-points/console/ui-dist/index.html +2 -2
  17. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +16 -0
  18. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  19. package/jest.config.js +57 -9
  20. package/package.json +17 -13
  21. package/src/adapter/entry-points/cli/index.test.ts +12 -2
  22. package/src/adapter/entry-points/cli/index.ts +30 -12
  23. package/src/adapter/entry-points/cli/projectConfig.ts +3 -0
  24. package/src/adapter/entry-points/console/consoleOperationApi.test.ts +129 -15
  25. package/src/adapter/entry-points/console/consoleOperationApi.ts +83 -28
  26. package/src/adapter/entry-points/console/consoleProjectResolver.test.ts +96 -0
  27. package/src/adapter/entry-points/console/consoleProjectResolver.ts +50 -0
  28. package/src/adapter/entry-points/console/consoleServer.test.ts +86 -4
  29. package/src/adapter/entry-points/console/consoleServer.ts +53 -23
  30. package/src/adapter/entry-points/console/ui/jest.setup.ts +1 -0
  31. package/src/adapter/entry-points/console/ui/jest.styleMock.js +1 -0
  32. package/src/adapter/entry-points/console/ui/src/features/console/colors.test.ts +34 -0
  33. package/src/adapter/entry-points/console/ui/src/features/console/colors.ts +73 -0
  34. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.stories.tsx +28 -0
  35. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.test.tsx +42 -0
  36. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleChangedFileList.tsx +55 -0
  37. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.stories.tsx +14 -0
  38. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.test.tsx +23 -0
  39. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCloseButtonGroup.tsx +26 -0
  40. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.stories.tsx +29 -0
  41. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.test.tsx +55 -0
  42. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommentList.tsx +66 -0
  43. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.stories.tsx +25 -0
  44. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.test.tsx +53 -0
  45. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleCommitList.tsx +53 -0
  46. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.stories.tsx +79 -0
  47. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.test.tsx +81 -0
  48. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemDetail.tsx +226 -0
  49. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.stories.tsx +82 -0
  50. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.test.tsx +52 -0
  51. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleItemIcon.tsx +32 -0
  52. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.stories.tsx +25 -0
  53. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.test.tsx +43 -0
  54. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListItemRow.tsx +34 -0
  55. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +22 -6
  56. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.test.tsx +87 -0
  57. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +36 -32
  58. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.stories.tsx +27 -0
  59. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.test.tsx +36 -0
  60. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMarkdownView.tsx +50 -0
  61. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.stories.tsx +22 -0
  62. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.test.tsx +38 -0
  63. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleMermaidDiagram.tsx +65 -0
  64. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.stories.tsx +20 -0
  65. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.test.tsx +42 -0
  66. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleNextActionDateGroup.tsx +28 -0
  67. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.stories.tsx +55 -0
  68. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.test.tsx +85 -0
  69. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleOperationBar.tsx +55 -0
  70. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.stories.tsx +26 -0
  71. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.test.tsx +32 -0
  72. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePanel.tsx +36 -0
  73. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.stories.tsx +29 -0
  74. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.test.tsx +14 -0
  75. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleProjectHeader.tsx +14 -0
  76. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.stories.tsx +14 -0
  77. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.test.tsx +33 -0
  78. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestReviewGroup.tsx +34 -0
  79. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.stories.tsx +31 -0
  80. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.test.tsx +40 -0
  81. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsolePullRequestSection.tsx +88 -0
  82. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.stories.tsx +17 -0
  83. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.test.tsx +49 -0
  84. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStatusButtonGroup.tsx +63 -0
  85. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.stories.tsx +17 -0
  86. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.test.tsx +45 -0
  87. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryButtonGroup.tsx +42 -0
  88. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.stories.tsx +27 -0
  89. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.test.tsx +24 -0
  90. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleStoryGroupHeader.tsx +28 -0
  91. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +41 -5
  92. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.test.tsx +59 -0
  93. package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +28 -19
  94. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.test.ts +35 -0
  95. package/src/adapter/entry-points/console/ui/src/features/console/fileStatus.ts +21 -0
  96. package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +206 -9
  97. package/src/adapter/entry-points/console/ui/src/features/console/grouping.test.ts +91 -0
  98. package/src/adapter/entry-points/console/ui/src/features/console/grouping.ts +79 -0
  99. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.test.ts +22 -0
  100. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleCaches.ts +42 -0
  101. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.test.ts +126 -0
  102. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleItemDetailData.ts +167 -0
  103. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.test.ts +198 -0
  104. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOperations.ts +243 -0
  105. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.test.ts +40 -0
  106. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleOverlay.ts +71 -0
  107. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.test.ts +24 -0
  108. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsolePjcode.ts +17 -0
  109. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.test.ts +41 -0
  110. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleResource.ts +57 -0
  111. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.test.ts +63 -0
  112. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleTabData.ts +129 -0
  113. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.test.ts +41 -0
  114. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.test.ts +97 -0
  115. package/src/adapter/entry-points/console/ui/src/features/console/itemIcons.ts +95 -0
  116. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +155 -0
  117. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +187 -0
  118. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.test.ts +76 -0
  119. package/src/adapter/entry-points/console/ui/src/features/console/lib/markdown.ts +73 -0
  120. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.test.ts +27 -0
  121. package/src/adapter/entry-points/console/ui/src/features/console/lib/mermaidLoader.ts +71 -0
  122. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.test.ts +56 -0
  123. package/src/adapter/entry-points/console/ui/src/features/console/lib/resourceCache.ts +51 -0
  124. package/src/adapter/entry-points/console/ui/src/features/console/operations.test.ts +37 -0
  125. package/src/adapter/entry-points/console/ui/src/features/console/operations.ts +35 -0
  126. package/src/adapter/entry-points/console/ui/src/features/console/overlay.test.ts +124 -0
  127. package/src/adapter/entry-points/console/ui/src/features/console/overlay.ts +101 -0
  128. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.test.tsx +79 -0
  129. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsoleItemDetailContainer.tsx +109 -0
  130. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.test.tsx +74 -0
  131. package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +137 -7
  132. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.test.ts +52 -0
  133. package/src/adapter/entry-points/console/ui/src/features/console/relativeTime.ts +51 -0
  134. package/src/adapter/entry-points/console/ui/src/features/console/types.ts +73 -1
  135. package/src/adapter/entry-points/console/ui/src/index.css +352 -2
  136. package/src/adapter/entry-points/console/ui/tsconfig.json +3 -1
  137. package/src/adapter/entry-points/console/ui/vite.config.ts +6 -1
  138. package/src/adapter/entry-points/console/ui-dist/assets/index-BU6p3cGU.css +1 -0
  139. package/src/adapter/entry-points/console/ui-dist/assets/index-BvuSQN9s.js +100 -0
  140. package/src/adapter/entry-points/console/ui-dist/index.html +2 -2
  141. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +25 -0
  142. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +4 -0
  143. package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
  144. package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
  145. package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
  146. package/types/adapter/entry-points/console/consoleOperationApi.d.ts +6 -2
  147. package/types/adapter/entry-points/console/consoleOperationApi.d.ts.map +1 -1
  148. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts +6 -0
  149. package/types/adapter/entry-points/console/consoleProjectResolver.d.ts.map +1 -0
  150. package/types/adapter/entry-points/console/consoleServer.d.ts +3 -3
  151. package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -1
  152. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  153. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  154. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  155. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  156. package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  157. package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
  158. package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +0 -65
  159. package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +0 -1
  160. package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +0 -49
@@ -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
+ });