github-issue-tower-defence-management 1.24.0 → 1.26.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 (69) hide show
  1. package/.github/workflows/test.yml +2 -2
  2. package/CHANGELOG.md +15 -0
  3. package/bin/adapter/repositories/GraphqlProjectRepository.js +12 -0
  4. package/bin/adapter/repositories/GraphqlProjectRepository.js.map +1 -1
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +9 -0
  6. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  7. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js +5 -1
  8. package/bin/adapter/repositories/issue/GraphqlProjectItemRepository.js.map +1 -1
  9. package/bin/domain/entities/ClaudeWindowUsage.js +3 -0
  10. package/bin/domain/entities/ClaudeWindowUsage.js.map +1 -0
  11. package/bin/domain/entities/Comment.js +3 -0
  12. package/bin/domain/entities/Comment.js.map +1 -0
  13. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +81 -0
  14. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -0
  15. package/bin/domain/usecases/StartPreparationUseCase.js +104 -0
  16. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -0
  17. package/bin/domain/usecases/adapter-interfaces/ClaudeRepository.js +3 -0
  18. package/bin/domain/usecases/adapter-interfaces/ClaudeRepository.js.map +1 -0
  19. package/bin/domain/usecases/adapter-interfaces/IssueCommentRepository.js +3 -0
  20. package/bin/domain/usecases/adapter-interfaces/IssueCommentRepository.js.map +1 -0
  21. package/bin/domain/usecases/adapter-interfaces/LocalCommandRunner.js +3 -0
  22. package/bin/domain/usecases/adapter-interfaces/LocalCommandRunner.js.map +1 -0
  23. package/package.json +1 -1
  24. package/src/adapter/repositories/CheerioProjectRepository.test.ts +1 -0
  25. package/src/adapter/repositories/GraphqlProjectRepository.test.ts +1 -0
  26. package/src/adapter/repositories/GraphqlProjectRepository.ts +14 -1
  27. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +1 -0
  28. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +15 -1
  29. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.test.ts +162 -34
  30. package/src/adapter/repositories/issue/GraphqlProjectItemRepository.ts +6 -0
  31. package/src/domain/entities/ClaudeWindowUsage.ts +5 -0
  32. package/src/domain/entities/Comment.ts +5 -0
  33. package/src/domain/entities/Project.ts +1 -0
  34. package/src/domain/usecases/GetStoryObjectMapUseCase.test.ts +1 -0
  35. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +669 -0
  36. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +132 -0
  37. package/src/domain/usecases/StartPreparationUseCase.test.ts +716 -0
  38. package/src/domain/usecases/StartPreparationUseCase.ts +191 -0
  39. package/src/domain/usecases/adapter-interfaces/ClaudeRepository.ts +5 -0
  40. package/src/domain/usecases/adapter-interfaces/IssueCommentRepository.ts +7 -0
  41. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +11 -0
  42. package/src/domain/usecases/adapter-interfaces/LocalCommandRunner.ts +7 -0
  43. package/src/domain/usecases/adapter-interfaces/ProjectRepository.ts +1 -0
  44. package/types/adapter/repositories/GraphqlProjectRepository.d.ts +2 -1
  45. package/types/adapter/repositories/GraphqlProjectRepository.d.ts.map +1 -1
  46. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +4 -1
  47. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  48. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts +1 -0
  49. package/types/adapter/repositories/issue/GraphqlProjectItemRepository.d.ts.map +1 -1
  50. package/types/domain/entities/ClaudeWindowUsage.d.ts +6 -0
  51. package/types/domain/entities/ClaudeWindowUsage.d.ts.map +1 -0
  52. package/types/domain/entities/Comment.d.ts +6 -0
  53. package/types/domain/entities/Comment.d.ts.map +1 -0
  54. package/types/domain/entities/Project.d.ts +1 -0
  55. package/types/domain/entities/Project.d.ts.map +1 -1
  56. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +24 -0
  57. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -0
  58. package/types/domain/usecases/StartPreparationUseCase.d.ts +37 -0
  59. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -0
  60. package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts +5 -0
  61. package/types/domain/usecases/adapter-interfaces/ClaudeRepository.d.ts.map +1 -0
  62. package/types/domain/usecases/adapter-interfaces/IssueCommentRepository.d.ts +7 -0
  63. package/types/domain/usecases/adapter-interfaces/IssueCommentRepository.d.ts.map +1 -0
  64. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +10 -0
  65. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
  66. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts +8 -0
  67. package/types/domain/usecases/adapter-interfaces/LocalCommandRunner.d.ts.map +1 -0
  68. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts +1 -0
  69. package/types/domain/usecases/adapter-interfaces/ProjectRepository.d.ts.map +1 -1
@@ -1,50 +1,178 @@
1
- import { GraphqlProjectItemRepository } from './GraphqlProjectItemRepository';
1
+ import axios, { AxiosHeaders, AxiosResponse } from 'axios';
2
+ import {
3
+ GraphqlProjectItemRepository,
4
+ PAGINATION_DELAY_MS,
5
+ } from './GraphqlProjectItemRepository';
2
6
  import { LocalStorageRepository } from '../LocalStorageRepository';
3
7
 
8
+ jest.mock('axios');
9
+ const mockAxios = jest.mocked(axios);
10
+
11
+ const toAxiosResponse = <T>(data: T): AxiosResponse<T> => ({
12
+ data,
13
+ status: 200,
14
+ statusText: 'OK',
15
+ headers: {},
16
+ config: { headers: new AxiosHeaders() },
17
+ });
18
+
4
19
  describe('GraphqlProjectItemRepository', () => {
5
- const localStorageRepository = new LocalStorageRepository();
6
- let repository: GraphqlProjectItemRepository;
7
-
8
- beforeEach(() => {
9
- repository = new GraphqlProjectItemRepository(
10
- localStorageRepository,
11
- '',
12
- process.env.GH_TOKEN,
13
- );
20
+ const makePageResponse = (hasNextPage: boolean, endCursor: string) =>
21
+ toAxiosResponse({
22
+ data: {
23
+ node: {
24
+ items: {
25
+ totalCount: 2,
26
+ pageInfo: {
27
+ endCursor,
28
+ startCursor: 'cursor-start',
29
+ hasNextPage,
30
+ },
31
+ nodes: [
32
+ {
33
+ id: `item-${endCursor}`,
34
+ fieldValues: {
35
+ nodes: [
36
+ {
37
+ text: 'some text',
38
+ field: { name: 'Status' },
39
+ },
40
+ ],
41
+ },
42
+ content: {
43
+ repository: { nameWithOwner: 'owner/repo' },
44
+ number: 1,
45
+ title: 'Test Issue',
46
+ state: 'OPEN',
47
+ url: 'https://github.com/owner/repo/issues/1',
48
+ body: 'body',
49
+ createdAt: '2024-01-01T00:00:00Z',
50
+ labels: { nodes: [] },
51
+ assignees: { nodes: [] },
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ },
57
+ },
58
+ });
59
+
60
+ describe('fetchProjectItems', () => {
61
+ beforeEach(() => {
62
+ jest.useFakeTimers();
63
+ });
64
+
65
+ afterEach(() => {
66
+ jest.useRealTimers();
67
+ mockAxios.mockClear();
68
+ });
69
+
70
+ it('should sleep between paginated requests to avoid 403', async () => {
71
+ const localStorageRepository = new LocalStorageRepository();
72
+ const repository = new GraphqlProjectItemRepository(
73
+ localStorageRepository,
74
+ '',
75
+ 'dummy-token',
76
+ );
77
+
78
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
79
+ mockAxios
80
+ .mockResolvedValueOnce(makePageResponse(true, 'cursor-1'))
81
+ .mockResolvedValueOnce(makePageResponse(false, 'cursor-2'));
82
+
83
+ const resultPromise = repository.fetchProjectItems('test-project-id');
84
+ await jest.advanceTimersByTimeAsync(PAGINATION_DELAY_MS);
85
+ const result = await resultPromise;
86
+
87
+ expect(mockAxios).toHaveBeenCalledTimes(2);
88
+ expect(result).toHaveLength(2);
89
+ expect(setTimeoutSpy).toHaveBeenCalledWith(
90
+ expect.any(Function),
91
+ PAGINATION_DELAY_MS,
92
+ );
93
+ setTimeoutSpy.mockRestore();
94
+ });
95
+
96
+ it('should not sleep on first request when there is only one page', async () => {
97
+ const localStorageRepository = new LocalStorageRepository();
98
+ const repository = new GraphqlProjectItemRepository(
99
+ localStorageRepository,
100
+ '',
101
+ 'dummy-token',
102
+ );
103
+
104
+ const setTimeoutSpy = jest.spyOn(global, 'setTimeout');
105
+ mockAxios.mockResolvedValueOnce(makePageResponse(false, 'cursor-1'));
106
+
107
+ const result = await repository.fetchProjectItems('test-project-id');
108
+
109
+ expect(mockAxios).toHaveBeenCalledTimes(1);
110
+ expect(result).toHaveLength(1);
111
+ expect(setTimeoutSpy).not.toHaveBeenCalledWith(
112
+ expect.any(Function),
113
+ PAGINATION_DELAY_MS,
114
+ );
115
+ setTimeoutSpy.mockRestore();
116
+ });
14
117
  });
118
+
15
119
  describe('getProjectItemFields', () => {
120
+ afterEach(() => {
121
+ mockAxios.mockClear();
122
+ });
123
+
16
124
  it('should return project item fields', async () => {
17
- const owner = 'HiromiShikata';
18
- const repositoryName = 'test-repository';
19
- const issueNumber = 38;
20
-
21
- const result = await repository.getProjectItemFields(
22
- owner,
23
- repositoryName,
24
- issueNumber,
125
+ const localStorageRepository = new LocalStorageRepository();
126
+ const repository = new GraphqlProjectItemRepository(
127
+ localStorageRepository,
128
+ '',
129
+ 'dummy-token',
130
+ );
131
+
132
+ mockAxios.mockResolvedValueOnce(
133
+ toAxiosResponse({
134
+ data: {
135
+ repository: {
136
+ issue: {
137
+ projectItems: {
138
+ nodes: [
139
+ {
140
+ id: 'item-1',
141
+ fieldValues: {
142
+ nodes: [
143
+ {
144
+ __typename: 'ProjectV2ItemFieldDateValue',
145
+ date: '2024-04-25',
146
+ field: { name: 'NextActionDate' },
147
+ },
148
+ {
149
+ __typename: 'ProjectV2ItemFieldSingleSelectValue',
150
+ name: 'In Progress',
151
+ field: { name: 'Status' },
152
+ },
153
+ ],
154
+ },
155
+ },
156
+ ],
157
+ },
158
+ },
159
+ },
160
+ },
161
+ }),
25
162
  );
26
163
 
164
+ const result = await repository.getProjectItemFields('owner', 'repo', 1);
165
+
166
+ expect(mockAxios).toHaveBeenCalledTimes(1);
27
167
  expect(result).toEqual([
28
- {
29
- fieldName: 'Assignees',
30
- fieldValue: '',
31
- },
32
- {
33
- fieldName: 'Repository',
34
- fieldValue: 'test-repository',
35
- },
36
- {
37
- fieldName: '',
38
- fieldValue: '',
39
- },
40
- {
41
- fieldName: '',
42
- fieldValue: 'In progress test title',
43
- },
44
168
  {
45
169
  fieldName: 'NextActionDate',
46
170
  fieldValue: '2024-04-25',
47
171
  },
172
+ {
173
+ fieldName: 'Status',
174
+ fieldValue: 'In Progress',
175
+ },
48
176
  ]);
49
177
  });
50
178
  });
@@ -18,6 +18,7 @@ export type ProjectItem = {
18
18
  value: string | null;
19
19
  }[];
20
20
  };
21
+ export const PAGINATION_DELAY_MS = 5000;
21
22
  export class GraphqlProjectItemRepository extends BaseGitHubRepository {
22
23
  fetchItemId = async (
23
24
  projectId: string,
@@ -295,6 +296,11 @@ query GetProjectItems($projectId: ID!, $after: String) {
295
296
  let hasNextPage = true;
296
297
 
297
298
  while (hasNextPage) {
299
+ if (after !== null) {
300
+ await new Promise((resolve) =>
301
+ setTimeout(resolve, PAGINATION_DELAY_MS),
302
+ );
303
+ }
298
304
  const data = await callGraphql(projectId, after);
299
305
  const projectItems: {
300
306
  id: string;
@@ -0,0 +1,5 @@
1
+ export type ClaudeWindowUsage = {
2
+ hour: number;
3
+ utilizationPercentage: number;
4
+ resetsAt: Date;
5
+ };
@@ -0,0 +1,5 @@
1
+ export type Comment = {
2
+ author: string;
3
+ content: string;
4
+ createdAt: Date;
5
+ };
@@ -15,6 +15,7 @@ export type FieldOption = {
15
15
  };
16
16
  export type Project = {
17
17
  id: string;
18
+ url: string;
18
19
  databaseId: number;
19
20
  name: string;
20
21
  // fields: ProjectField[];
@@ -15,6 +15,7 @@ describe('GetStoryObjectMapUseCase', () => {
15
15
 
16
16
  const basicProject: Project = {
17
17
  id: 'project-1',
18
+ url: 'https://github.com/users/user/projects/1',
18
19
  databaseId: 1,
19
20
  name: 'Test Project',
20
21
  status: {