github-issue-tower-defence-management 1.148.21 → 1.148.23

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 (25) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/bin/adapter/entry-points/console/consoleReadApi.js +1 -11
  3. package/bin/adapter/entry-points/console/consoleReadApi.js.map +1 -1
  4. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +15 -3
  5. package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
  6. package/bin/domain/usecases/DailySecurityScanUseCase.js +46 -5
  7. package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/adapter/entry-points/console/consoleReadApi.test.ts +15 -47
  10. package/src/adapter/entry-points/console/consoleReadApi.ts +2 -20
  11. package/src/adapter/entry-points/console/ui/e2e/consoleTestHarness.ts +10 -1
  12. package/src/adapter/entry-points/handlers/inTmuxByHumanSessionReconciler.test.ts +1 -0
  13. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +87 -4
  14. package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +24 -4
  15. package/src/domain/usecases/DailySecurityScanUseCase.test.ts +502 -195
  16. package/src/domain/usecases/DailySecurityScanUseCase.ts +60 -5
  17. package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +1 -0
  18. package/src/domain/usecases/intmux/InTmuxByHumanSessionReconcileUseCase.test.ts +1 -0
  19. package/types/adapter/entry-points/console/consoleReadApi.d.ts.map +1 -1
  20. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +1 -0
  21. package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
  22. package/types/domain/usecases/DailySecurityScanUseCase.d.ts +1 -0
  23. package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
  24. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +1 -0
  25. package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
@@ -1,33 +1,5 @@
1
1
  import { mock } from 'jest-mock-extended';
2
- import { Issue } from '../../../domain/entities/Issue';
3
2
  import { IssueRepository } from '../../../domain/usecases/adapter-interfaces/IssueRepository';
4
-
5
- const buildIssueFixture = (title: string): Issue => ({
6
- nameWithOwner: 'o/r',
7
- number: 1,
8
- title,
9
- state: 'OPEN',
10
- status: null,
11
- story: null,
12
- nextActionDate: null,
13
- nextActionHour: null,
14
- estimationMinutes: null,
15
- dependedIssueUrls: [],
16
- completionDate50PercentConfidence: null,
17
- url: 'https://github.com/o/r/issues/1',
18
- assignees: [],
19
- labels: [],
20
- org: 'o',
21
- repo: 'r',
22
- body: '',
23
- itemId: 'itemId',
24
- isPr: false,
25
- isInProgress: false,
26
- isClosed: false,
27
- createdAt: new Date('2026-01-01T00:00:00Z'),
28
- author: 'octocat',
29
- closingIssueReferenceUrls: [],
30
- });
31
3
  import {
32
4
  ISSUE_TITLE_CACHE_TTL_MS,
33
5
  IssueTitleStateCache,
@@ -272,10 +244,8 @@ describe('consoleReadApi', () => {
272
244
  state: 'OPEN',
273
245
  merged: false,
274
246
  isPullRequest: false,
247
+ title: 'Issue title from repository',
275
248
  });
276
- issueRepository.getIssueByUrl.mockResolvedValue(
277
- buildIssueFixture('Issue title from repository'),
278
- );
279
249
  const cache = new IssueTitleStateCache(() => 0);
280
250
  const url = 'https://github.com/o/r/issues/1';
281
251
  const first = await handleIssueTitle(issueRepository, cache, url);
@@ -290,22 +260,16 @@ describe('consoleReadApi', () => {
290
260
  expect(issueRepository.getIssueOrPullRequestState).toHaveBeenCalledTimes(
291
261
  1,
292
262
  );
293
- expect(issueRepository.getIssueByUrl).toHaveBeenCalledTimes(1);
263
+ expect(issueRepository.getIssueByUrl).not.toHaveBeenCalled();
294
264
  });
295
265
 
296
- it('returns the pull request title from the summary for pull request urls', async () => {
266
+ it('returns the pull request title for pull request urls', async () => {
297
267
  const issueRepository = mock<IssueRepository>();
298
268
  issueRepository.getIssueOrPullRequestState.mockResolvedValue({
299
269
  state: 'CLOSED',
300
270
  merged: true,
301
271
  isPullRequest: true,
302
- });
303
- issueRepository.getPullRequestSummary.mockResolvedValue({
304
- title: 'Pull request title from summary',
305
- body: 'body',
306
- additions: 1,
307
- deletions: 0,
308
- changedFiles: 1,
272
+ title: 'Pull request title from state',
309
273
  });
310
274
  const cache = new IssueTitleStateCache(() => 0);
311
275
  const url = 'https://github.com/o/r/pull/2';
@@ -314,32 +278,34 @@ describe('consoleReadApi', () => {
314
278
  state: 'CLOSED',
315
279
  merged: true,
316
280
  isPullRequest: true,
317
- title: 'Pull request title from summary',
281
+ title: 'Pull request title from state',
318
282
  });
319
- expect(issueRepository.getPullRequestSummary).toHaveBeenCalledWith(url);
283
+ expect(issueRepository.getPullRequestSummary).not.toHaveBeenCalled();
320
284
  expect(issueRepository.getIssueByUrl).not.toHaveBeenCalled();
321
285
  });
322
286
 
323
- it('falls back to an empty title when the title source returns nothing', async () => {
287
+ it('returns the title of an issue that is not an item on the project board', async () => {
324
288
  const issueRepository = mock<IssueRepository>();
325
289
  issueRepository.getIssueOrPullRequestState.mockResolvedValue({
326
- state: 'OPEN',
290
+ state: 'CLOSED',
327
291
  merged: false,
328
292
  isPullRequest: false,
293
+ title: 'Title of an issue outside the board',
329
294
  });
330
295
  issueRepository.getIssueByUrl.mockResolvedValue(null);
331
296
  const cache = new IssueTitleStateCache(() => 0);
332
297
  const response = await handleIssueTitle(
333
298
  issueRepository,
334
299
  cache,
335
- 'https://github.com/o/r/issues/3',
300
+ 'https://github.com/o/off-board-repository/issues/656',
336
301
  );
337
302
  expect(response.body).toEqual({
338
- state: 'OPEN',
303
+ state: 'CLOSED',
339
304
  merged: false,
340
305
  isPullRequest: false,
341
- title: '',
306
+ title: 'Title of an issue outside the board',
342
307
  });
308
+ expect(issueRepository.getIssueByUrl).not.toHaveBeenCalled();
343
309
  });
344
310
 
345
311
  it('re-fetches a non-merged result after the TTL elapses', async () => {
@@ -348,6 +314,7 @@ describe('consoleReadApi', () => {
348
314
  state: 'OPEN',
349
315
  merged: false,
350
316
  isPullRequest: true,
317
+ title: 'Open pull request title',
351
318
  });
352
319
  let now = 0;
353
320
  const cache = new IssueTitleStateCache(() => now);
@@ -371,6 +338,7 @@ describe('consoleReadApi', () => {
371
338
  state: 'CLOSED',
372
339
  merged: true,
373
340
  isPullRequest: true,
341
+ title: 'Merged pull request title',
374
342
  });
375
343
  let now = 0;
376
344
  const cache = new IssueTitleStateCache(() => now);
@@ -233,19 +233,6 @@ export const handleRelatedPrs = async (
233
233
  return ok({ relatedPullRequests: withSummaries });
234
234
  };
235
235
 
236
- const resolveIssueOrPullRequestTitle = async (
237
- issueRepository: IssueRepository,
238
- url: string,
239
- isPullRequest: boolean,
240
- ): Promise<string> => {
241
- if (isPullRequest) {
242
- const summary = await issueRepository.getPullRequestSummary(url);
243
- return summary?.title ?? '';
244
- }
245
- const issue = await issueRepository.getIssueByUrl(url);
246
- return issue?.title ?? '';
247
- };
248
-
249
236
  export const handleIssueTitle = async (
250
237
  issueRepository: IssueRepository,
251
238
  cache: IssueTitleStateCache,
@@ -258,13 +245,8 @@ export const handleIssueTitle = async (
258
245
  if (cached !== null) {
259
246
  return ok(cached);
260
247
  }
261
- const baseState = await issueRepository.getIssueOrPullRequestState(url);
262
- const title = await resolveIssueOrPullRequestTitle(
263
- issueRepository,
264
- url,
265
- baseState.isPullRequest,
266
- );
267
- const state: IssueOrPullRequestState = { ...baseState, title };
248
+ const state: IssueOrPullRequestState =
249
+ await issueRepository.getIssueOrPullRequestState(url);
268
250
  cache.set(url, state);
269
251
  return ok(state);
270
252
  };
@@ -473,10 +473,19 @@ const createStubIssueRepository = (
473
473
  getPullRequestCommits: async (): Promise<PullRequestCommit[]> => [],
474
474
  getIssueOrPullRequestState: async (
475
475
  url: string,
476
- ): Promise<{ state: string; merged: boolean; isPullRequest: boolean }> => ({
476
+ ): Promise<{
477
+ state: string;
478
+ merged: boolean;
479
+ isPullRequest: boolean;
480
+ title: string;
481
+ }> => ({
477
482
  state: 'open',
478
483
  merged: false,
479
484
  isPullRequest: url.includes('/pull/'),
485
+ title:
486
+ url === CONSOLE_E2E_INLINE_COMMENT_PR_URL
487
+ ? 'Add inline review comments on the related pull request diff'
488
+ : buildIssueForUrl(url).title,
480
489
  }),
481
490
  getPullRequestSummary: async (
482
491
  url: string,
@@ -47,6 +47,7 @@ const createIssueStateRepository = (
47
47
  state,
48
48
  merged: false,
49
49
  isPullRequest: false,
50
+ title: 'In tmux by human session issue',
50
51
  }),
51
52
  });
52
53
 
@@ -1103,9 +1103,16 @@ describe('ApiV3CheerioRestIssueRepository', () => {
1103
1103
  ),
1104
1104
  )
1105
1105
  .mockResolvedValueOnce(
1106
- new Response(JSON.stringify({ state: 'open', merged: false }), {
1107
- status: 200,
1108
- }),
1106
+ new Response(
1107
+ JSON.stringify({
1108
+ state: 'open',
1109
+ merged: false,
1110
+ title: 'Issue title',
1111
+ }),
1112
+ {
1113
+ status: 200,
1114
+ },
1115
+ ),
1109
1116
  );
1110
1117
 
1111
1118
  const { repository, sleep } = createApiV3CheerioRestIssueRepository();
@@ -2010,6 +2017,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2010
2017
  state: 'closed',
2011
2018
  merged: true,
2012
2019
  isPullRequest: true,
2020
+ title: 'PR title',
2013
2021
  });
2014
2022
  expect(fetchSpy).toHaveBeenCalledWith(
2015
2023
  'https://api.github.com/repos/HiromiShikata/test-repository/pulls/42',
@@ -2019,7 +2027,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2019
2027
 
2020
2028
  it('should fetch issue state with merged always false for an issue URL', async () => {
2021
2029
  const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValueOnce(
2022
- new Response(JSON.stringify({ state: 'open' }), {
2030
+ new Response(JSON.stringify({ state: 'open', title: 'Issue title' }), {
2023
2031
  status: 200,
2024
2032
  headers: { 'Content-Type': 'application/json' },
2025
2033
  }),
@@ -2034,6 +2042,7 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2034
2042
  state: 'open',
2035
2043
  merged: false,
2036
2044
  isPullRequest: false,
2045
+ title: 'Issue title',
2037
2046
  });
2038
2047
  expect(fetchSpy).toHaveBeenCalledWith(
2039
2048
  'https://api.github.com/repos/HiromiShikata/test-repository/issues/42',
@@ -2041,6 +2050,80 @@ describe('ApiV3CheerioRestIssueRepository', () => {
2041
2050
  );
2042
2051
  });
2043
2052
 
2053
+ it('should return the issue title carried by the same REST payload', async () => {
2054
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
2055
+ new Response(
2056
+ JSON.stringify({ state: 'closed', title: 'Issue title from REST' }),
2057
+ {
2058
+ status: 200,
2059
+ headers: { 'Content-Type': 'application/json' },
2060
+ },
2061
+ ),
2062
+ );
2063
+
2064
+ const { repository } = createApiV3CheerioRestIssueRepository();
2065
+ const result = await repository.getIssueOrPullRequestState(
2066
+ 'https://github.com/HiromiShikata/other-repository/issues/656',
2067
+ );
2068
+
2069
+ expect(result).toEqual({
2070
+ state: 'closed',
2071
+ merged: false,
2072
+ isPullRequest: false,
2073
+ title: 'Issue title from REST',
2074
+ });
2075
+ });
2076
+
2077
+ it('should return the pull request title carried by the same REST payload', async () => {
2078
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
2079
+ new Response(
2080
+ JSON.stringify({
2081
+ title: 'PR title from REST',
2082
+ state: 'open',
2083
+ merged: false,
2084
+ draft: false,
2085
+ additions: 1,
2086
+ deletions: 1,
2087
+ changed_files: 1,
2088
+ head: { ref: 'feature/foo' },
2089
+ base: { ref: 'main' },
2090
+ user: { login: 'alice' },
2091
+ body: 'pr body',
2092
+ }),
2093
+ { status: 200, headers: { 'Content-Type': 'application/json' } },
2094
+ ),
2095
+ );
2096
+
2097
+ const { repository } = createApiV3CheerioRestIssueRepository();
2098
+ const result = await repository.getIssueOrPullRequestState(
2099
+ 'https://github.com/HiromiShikata/other-repository/pull/42',
2100
+ );
2101
+
2102
+ expect(result).toEqual({
2103
+ state: 'open',
2104
+ merged: false,
2105
+ isPullRequest: true,
2106
+ title: 'PR title from REST',
2107
+ });
2108
+ });
2109
+
2110
+ it('should throw rather than report an empty title when the payload carries no title', async () => {
2111
+ jest.spyOn(global, 'fetch').mockResolvedValueOnce(
2112
+ new Response(JSON.stringify({ state: 'open' }), {
2113
+ status: 200,
2114
+ headers: { 'Content-Type': 'application/json' },
2115
+ }),
2116
+ );
2117
+
2118
+ const { repository } = createApiV3CheerioRestIssueRepository();
2119
+
2120
+ await expect(
2121
+ repository.getIssueOrPullRequestState(
2122
+ 'https://github.com/HiromiShikata/other-repository/issues/656',
2123
+ ),
2124
+ ).rejects.toThrow('Unexpected response shape when fetching state for');
2125
+ });
2126
+
2044
2127
  it('should throw when the API responds with a non-2xx status', async () => {
2045
2128
  jest.spyOn(global, 'fetch').mockResolvedValueOnce(
2046
2129
  new Response('Not Found', {
@@ -277,12 +277,17 @@ function isIssueOrPullRequestBodyResponse(
277
277
 
278
278
  type IssueOrPullRequestStateResponse = {
279
279
  state: string;
280
+ title: string;
280
281
  };
281
282
 
282
283
  function isIssueOrPullRequestStateResponse(
283
284
  value: unknown,
284
285
  ): value is IssueOrPullRequestStateResponse {
285
- return isRecord(value) && typeof value.state === 'string';
286
+ return (
287
+ isRecord(value) &&
288
+ typeof value.state === 'string' &&
289
+ typeof value.title === 'string'
290
+ );
286
291
  }
287
292
 
288
293
  type IssueCommentsResponseItem = {
@@ -2273,7 +2278,12 @@ export class ApiV3CheerioRestIssueRepository
2273
2278
 
2274
2279
  getIssueOrPullRequestState = async (
2275
2280
  url: string,
2276
- ): Promise<{ state: string; merged: boolean; isPullRequest: boolean }> => {
2281
+ ): Promise<{
2282
+ state: string;
2283
+ merged: boolean;
2284
+ isPullRequest: boolean;
2285
+ title: string;
2286
+ }> => {
2277
2287
  const { owner, repo, issueNumber, isPr } = this.parseIssueUrl(url);
2278
2288
  if (isPr) {
2279
2289
  const response = await this.fetchWithRateLimitRetry(() =>
@@ -2298,7 +2308,12 @@ export class ApiV3CheerioRestIssueRepository
2298
2308
  `Unexpected response shape when fetching state for ${url}`,
2299
2309
  );
2300
2310
  }
2301
- return { state: body.state, merged: body.merged, isPullRequest: true };
2311
+ return {
2312
+ state: body.state,
2313
+ merged: body.merged,
2314
+ isPullRequest: true,
2315
+ title: body.title,
2316
+ };
2302
2317
  }
2303
2318
  const response = await this.fetchWithRateLimitRetry(() =>
2304
2319
  fetch(
@@ -2322,7 +2337,12 @@ export class ApiV3CheerioRestIssueRepository
2322
2337
  `Unexpected response shape when fetching state for ${url}`,
2323
2338
  );
2324
2339
  }
2325
- return { state: body.state, merged: false, isPullRequest: false };
2340
+ return {
2341
+ state: body.state,
2342
+ merged: false,
2343
+ isPullRequest: false,
2344
+ title: body.title,
2345
+ };
2326
2346
  };
2327
2347
 
2328
2348
  getPullRequestSummary = async (