github-issue-tower-defence-management 1.172.2 → 1.173.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -0
  3. package/bin/adapter/entry-points/console/consoleReadApi.js +126 -48
  4. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  5. package/bin/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
  6. package/bin/adapter/entry-points/console/ui-dist/index.html +1 -1
  7. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +3 -1
  8. package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
  9. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +19 -17
  10. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  11. package/bin/adapter/repositories/issue/githubRateLimitRetry.js +8 -1
  12. package/bin/adapter/repositories/issue/githubRateLimitRetry.js.map +1 -1
  13. package/bin/domain/usecases/HandleScheduledEventUseCase.js +17 -3
  14. package/bin/domain/usecases/HandleScheduledEventUseCase.js.map +1 -1
  15. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js +50 -0
  16. package/bin/domain/usecases/QualityCheckAdvanceUseCase.js.map +1 -0
  17. package/package.json +1 -1
  18. package/src/adapter/entry-points/console/consoleReadApi.test.ts +254 -0
  19. package/src/adapter/entry-points/console/consoleReadApi.ts +133 -55
  20. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.test.ts +10 -0
  21. package/src/adapter/entry-points/console/ui/src/features/console/lib/consoleApi.ts +14 -1
  22. package/src/adapter/entry-points/console/ui-dist/assets/{index-ZYMkO0fm.js → index-BcnLfodS.js} +1 -1
  23. package/src/adapter/entry-points/console/ui-dist/index.html +1 -1
  24. package/src/adapter/entry-points/console/webServer.test.ts +35 -0
  25. package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +6 -0
  26. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +47 -19
  27. package/src/adapter/repositories/issue/githubRateLimitRetry.ts +4 -0
  28. package/src/domain/usecases/HandleScheduledEventUseCase.test.ts +116 -0
  29. package/src/domain/usecases/HandleScheduledEventUseCase.ts +21 -0
  30. package/src/domain/usecases/QualityCheckAdvanceUseCase.test.ts +387 -0
  31. package/src/domain/usecases/QualityCheckAdvanceUseCase.ts +75 -0
  32. package/types/adapter/entry-points/console/consoleReadApi.d.ts +2 -0
  33. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  34. package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
  35. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  36. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  37. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts +3 -0
  38. package/types/adapter/repositories/issue/githubRateLimitRetry.d.ts.map +1 -1
  39. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts +5 -2
  40. package/types/domain/usecases/HandleScheduledEventUseCase.d.ts.map +1 -1
  41. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts +14 -0
  42. package/types/domain/usecases/QualityCheckAdvanceUseCase.d.ts.map +1 -0
@@ -3,6 +3,7 @@ import {
3
3
  IssueComment,
4
4
  PullRequestCommit,
5
5
  } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
6
+ import { GitHubRateLimitError } from '../../repositories/issue/githubRateLimitRetry';
6
7
 
7
8
  export const ISSUE_TITLE_CACHE_TTL_MS = 300 * 1000;
8
9
 
@@ -72,6 +73,11 @@ export class IssueTitleStateCache {
72
73
  return entry.state;
73
74
  };
74
75
 
76
+ getStale = (url: string): IssueOrPullRequestState | null => {
77
+ const entry = this.entries.get(url);
78
+ return entry?.state ?? null;
79
+ };
80
+
75
81
  set = (url: string, state: IssueOrPullRequestState): void => {
76
82
  this.entries.set(url, { state, fetchedAtMs: this.nowMs() });
77
83
  };
@@ -93,6 +99,11 @@ export class PullRequestStatusCache {
93
99
  return entry.status;
94
100
  };
95
101
 
102
+ getStale = (url: string): PullRequestStatusResponse | null => {
103
+ const entry = this.entries.get(url);
104
+ return entry?.status ?? null;
105
+ };
106
+
96
107
  set = (url: string, status: PullRequestStatusResponse): void => {
97
108
  this.entries.set(url, { status, fetchedAtMs: this.nowMs() });
98
109
  };
@@ -103,6 +114,10 @@ export type ConsoleReadApiResponse = {
103
114
  body: unknown;
104
115
  };
105
116
 
117
+ const isGitHubRateLimitError = (
118
+ error: unknown,
119
+ ): error is GitHubRateLimitError => error instanceof GitHubRateLimitError;
120
+
106
121
  const badRequest = (message: string): ConsoleReadApiResponse => ({
107
122
  statusCode: 400,
108
123
  body: { error: message },
@@ -113,6 +128,11 @@ const ok = (body: unknown): ConsoleReadApiResponse => ({
113
128
  body,
114
129
  });
115
130
 
131
+ const rateLimited = (error: Error): ConsoleReadApiResponse => ({
132
+ statusCode: 429,
133
+ body: { error: error.message },
134
+ });
135
+
116
136
  export type RelatedPullRequestWithSummary = {
117
137
  url: string;
118
138
  branchName: string | null;
@@ -160,8 +180,15 @@ export const handleItemBody = async (
160
180
  if (!url) {
161
181
  return badRequest('url query parameter is required');
162
182
  }
163
- const body = await issueRepository.getIssueOrPullRequestBody(url);
164
- return ok({ body });
183
+ try {
184
+ const body = await issueRepository.getIssueOrPullRequestBody(url);
185
+ return ok({ body });
186
+ } catch (error) {
187
+ if (isGitHubRateLimitError(error)) {
188
+ return rateLimited(error);
189
+ }
190
+ throw error;
191
+ }
165
192
  };
166
193
 
167
194
  export const handleComments = async (
@@ -171,8 +198,15 @@ export const handleComments = async (
171
198
  if (!url) {
172
199
  return badRequest('url query parameter is required');
173
200
  }
174
- const comments = await issueRepository.getIssueOrPullRequestComments(url);
175
- return ok({ comments: serializeComments(comments) });
201
+ try {
202
+ const comments = await issueRepository.getIssueOrPullRequestComments(url);
203
+ return ok({ comments: serializeComments(comments) });
204
+ } catch (error) {
205
+ if (isGitHubRateLimitError(error)) {
206
+ return rateLimited(error);
207
+ }
208
+ throw error;
209
+ }
176
210
  };
177
211
 
178
212
  export const handlePrFiles = async (
@@ -182,11 +216,18 @@ export const handlePrFiles = async (
182
216
  if (!url) {
183
217
  return badRequest('url query parameter is required');
184
218
  }
185
- const detail = await issueRepository.getPullRequestDetail(url);
186
- if (detail === null) {
187
- return ok({ files: null });
219
+ try {
220
+ const detail = await issueRepository.getPullRequestDetail(url);
221
+ if (detail === null) {
222
+ return ok({ files: null });
223
+ }
224
+ return ok({ files: detail.files });
225
+ } catch (error) {
226
+ if (isGitHubRateLimitError(error)) {
227
+ return rateLimited(error);
228
+ }
229
+ throw error;
188
230
  }
189
- return ok({ files: detail.files });
190
231
  };
191
232
 
192
233
  export const handlePrCommits = async (
@@ -196,8 +237,15 @@ export const handlePrCommits = async (
196
237
  if (!url) {
197
238
  return badRequest('url query parameter is required');
198
239
  }
199
- const commits = await issueRepository.getPullRequestCommits(url);
200
- return ok({ commits: serializeCommits(commits) });
240
+ try {
241
+ const commits = await issueRepository.getPullRequestCommits(url);
242
+ return ok({ commits: serializeCommits(commits) });
243
+ } catch (error) {
244
+ if (isGitHubRateLimitError(error)) {
245
+ return rateLimited(error);
246
+ }
247
+ throw error;
248
+ }
201
249
  };
202
250
 
203
251
  export const handleRelatedPrs = async (
@@ -207,30 +255,38 @@ export const handleRelatedPrs = async (
207
255
  if (!url) {
208
256
  return badRequest('url query parameter is required');
209
257
  }
210
- const relatedPullRequests = await issueRepository.findRelatedOpenPRs(url);
211
- const withSummaries: RelatedPullRequestWithSummary[] = await Promise.all(
212
- relatedPullRequests.map(async (relatedPullRequest) => {
213
- const summary = await issueRepository.getPullRequestSummary(
214
- relatedPullRequest.url,
215
- );
216
- return {
217
- url: relatedPullRequest.url,
218
- branchName: relatedPullRequest.branchName,
219
- createdAt: relatedPullRequest.createdAt.toISOString(),
220
- isDraft: relatedPullRequest.isDraft,
221
- isConflicted: relatedPullRequest.isConflicted,
222
- mergeableStatus: deriveMergeableStatus(relatedPullRequest.mergeable),
223
- isPassedAllCiJob: relatedPullRequest.isPassedAllCiJob,
224
- isCiStateSuccess: relatedPullRequest.isCiStateSuccess,
225
- isResolvedAllReviewComments:
226
- relatedPullRequest.isResolvedAllReviewComments,
227
- isBranchOutOfDate: relatedPullRequest.isBranchOutOfDate,
228
- missingRequiredCheckNames: relatedPullRequest.missingRequiredCheckNames,
229
- summary,
230
- };
231
- }),
232
- );
233
- return ok({ relatedPullRequests: withSummaries });
258
+ try {
259
+ const relatedPullRequests = await issueRepository.findRelatedOpenPRs(url);
260
+ const withSummaries: RelatedPullRequestWithSummary[] = await Promise.all(
261
+ relatedPullRequests.map(async (relatedPullRequest) => {
262
+ const summary = await issueRepository.getPullRequestSummary(
263
+ relatedPullRequest.url,
264
+ );
265
+ return {
266
+ url: relatedPullRequest.url,
267
+ branchName: relatedPullRequest.branchName,
268
+ createdAt: relatedPullRequest.createdAt.toISOString(),
269
+ isDraft: relatedPullRequest.isDraft,
270
+ isConflicted: relatedPullRequest.isConflicted,
271
+ mergeableStatus: deriveMergeableStatus(relatedPullRequest.mergeable),
272
+ isPassedAllCiJob: relatedPullRequest.isPassedAllCiJob,
273
+ isCiStateSuccess: relatedPullRequest.isCiStateSuccess,
274
+ isResolvedAllReviewComments:
275
+ relatedPullRequest.isResolvedAllReviewComments,
276
+ isBranchOutOfDate: relatedPullRequest.isBranchOutOfDate,
277
+ missingRequiredCheckNames:
278
+ relatedPullRequest.missingRequiredCheckNames,
279
+ summary,
280
+ };
281
+ }),
282
+ );
283
+ return ok({ relatedPullRequests: withSummaries });
284
+ } catch (error) {
285
+ if (isGitHubRateLimitError(error)) {
286
+ return rateLimited(error);
287
+ }
288
+ throw error;
289
+ }
234
290
  };
235
291
 
236
292
  export const handleIssueTitle = async (
@@ -245,10 +301,21 @@ export const handleIssueTitle = async (
245
301
  if (cached !== null) {
246
302
  return ok(cached);
247
303
  }
248
- const state: IssueOrPullRequestState =
249
- await issueRepository.getIssueOrPullRequestState(url);
250
- cache.set(url, state);
251
- return ok(state);
304
+ try {
305
+ const state: IssueOrPullRequestState =
306
+ await issueRepository.getIssueOrPullRequestState(url);
307
+ cache.set(url, state);
308
+ return ok(state);
309
+ } catch (error) {
310
+ if (isGitHubRateLimitError(error)) {
311
+ const stale = cache.getStale(url);
312
+ if (stale !== null) {
313
+ return ok(stale);
314
+ }
315
+ return rateLimited(error);
316
+ }
317
+ throw error;
318
+ }
252
319
  };
253
320
 
254
321
  export const handlePullRequestStatus = async (
@@ -263,21 +330,32 @@ export const handlePullRequestStatus = async (
263
330
  if (cached !== null) {
264
331
  return ok(cached);
265
332
  }
266
- const pullRequest = await issueRepository.getOpenPullRequestCiStatus(url);
267
- const response: PullRequestStatusResponse =
268
- pullRequest === null
269
- ? { found: false, status: null }
270
- : {
271
- found: true,
272
- status: {
273
- isConflicted: pullRequest.isConflicted,
274
- mergeableStatus: deriveMergeableStatus(pullRequest.mergeable),
275
- isPassedAllCiJob: pullRequest.isPassedAllCiJob,
276
- isCiStateSuccess: pullRequest.isCiStateSuccess,
277
- isBranchOutOfDate: pullRequest.isBranchOutOfDate,
278
- missingRequiredCheckNames: pullRequest.missingRequiredCheckNames,
279
- },
280
- };
281
- cache.set(url, response);
282
- return ok(response);
333
+ try {
334
+ const pullRequest = await issueRepository.getOpenPullRequestCiStatus(url);
335
+ const response: PullRequestStatusResponse =
336
+ pullRequest === null
337
+ ? { found: false, status: null }
338
+ : {
339
+ found: true,
340
+ status: {
341
+ isConflicted: pullRequest.isConflicted,
342
+ mergeableStatus: deriveMergeableStatus(pullRequest.mergeable),
343
+ isPassedAllCiJob: pullRequest.isPassedAllCiJob,
344
+ isCiStateSuccess: pullRequest.isCiStateSuccess,
345
+ isBranchOutOfDate: pullRequest.isBranchOutOfDate,
346
+ missingRequiredCheckNames: pullRequest.missingRequiredCheckNames,
347
+ },
348
+ };
349
+ cache.set(url, response);
350
+ return ok(response);
351
+ } catch (error) {
352
+ if (isGitHubRateLimitError(error)) {
353
+ const stale = cache.getStale(url);
354
+ if (stale !== null) {
355
+ return ok(stale);
356
+ }
357
+ return rateLimited(error);
358
+ }
359
+ throw error;
360
+ }
283
361
  };
@@ -284,6 +284,16 @@ describe('createConsoleApiClient', () => {
284
284
  ).rejects.toThrow('HTTP 500');
285
285
  });
286
286
 
287
+ it('throws the error message from the JSON body of a non-ok response instead of the HTTP status code', async () => {
288
+ const rateLimitMessage =
289
+ 'Failed to fetch body for https://github.com/o/r/issues/1: HTTP 403 GitHub rate limit exceeded, please retry shortly (resets at 2026-01-01T01:00:00.000Z)';
290
+ mockFetchOnce({ error: rateLimitMessage }, false);
291
+ const client = createConsoleApiClient();
292
+ await expect(
293
+ client.fetchItemBody('https://github.com/o/r/issues/1'),
294
+ ).rejects.toThrow(rateLimitMessage);
295
+ });
296
+
287
297
  it('returns cached comments when the network fetch rejects', async () => {
288
298
  const cachedBody = {
289
299
  comments: [
@@ -80,7 +80,20 @@ const requestJson = async (
80
80
  try {
81
81
  const response = await fetch(url);
82
82
  if (!response.ok) {
83
- throw new Error(`HTTP ${response.status}`);
83
+ let errorMessage = `HTTP ${response.status}`;
84
+ try {
85
+ const payload: unknown = await response.json();
86
+ if (
87
+ isRecord(payload) &&
88
+ typeof payload.error === 'string' &&
89
+ payload.error.length > 0
90
+ ) {
91
+ errorMessage = payload.error;
92
+ }
93
+ } catch (e: unknown) {
94
+ console.warn('Failed to parse error body from non-ok response:', e);
95
+ }
96
+ throw new Error(errorMessage);
84
97
  }
85
98
  const payload: unknown = await response.json();
86
99
  try {