github-issue-tower-defence-management 1.197.1 → 2.0.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 (24) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +21 -1
  3. package/bin/adapter/entry-points/console/webServer.js +5 -0
  4. package/bin/adapter/entry-points/console/webServer.js.map +1 -1
  5. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +2 -2
  6. package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
  7. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +12 -2
  8. package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
  9. package/bin/domain/usecases/StartPreparationUseCase.js +2 -3
  10. package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/adapter/entry-points/console/webServer.test.ts +68 -7
  13. package/src/adapter/entry-points/console/webServer.ts +9 -0
  14. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +38 -6
  15. package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -7
  16. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +315 -143
  17. package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +21 -7
  18. package/src/domain/usecases/StartPreparationUseCase.ts +2 -3
  19. package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
  20. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +0 -1
  21. package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
  22. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +0 -1
  23. package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
  24. package/types/domain/usecases/StartPreparationUseCase.d.ts.map +1 -1
@@ -1499,6 +1499,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1499
1499
  server: http.Server,
1500
1500
  requestPath: string,
1501
1501
  method = 'GET',
1502
+ headers?: Record<string, string>,
1502
1503
  ): Promise<{
1503
1504
  statusCode: number;
1504
1505
  body: string;
@@ -1514,7 +1515,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1514
1515
  const port = address.port;
1515
1516
  return new Promise((resolve, reject) => {
1516
1517
  const httpRequest = http.request(
1517
- { host: '127.0.0.1', port, path: requestPath, method },
1518
+ { host: '127.0.0.1', port, path: requestPath, method, headers },
1518
1519
  (response) => {
1519
1520
  const chunks: Uint8Array[] = [];
1520
1521
  response.on('data', (chunk: Uint8Array) => chunks.push(chunk));
@@ -1653,7 +1654,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1653
1654
  dashboardDataDir: dataDir,
1654
1655
  });
1655
1656
  try {
1656
- const response = await requestServer(server, '/tdpm.txt');
1657
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1657
1658
  expect(response.statusCode).toBe(200);
1658
1659
  expect(response.body).toBe(expectedComposed);
1659
1660
  expect(response.contentType).toBe('text/html; charset=utf-8');
@@ -1680,7 +1681,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1680
1681
  dashboardDataDir: null,
1681
1682
  });
1682
1683
  try {
1683
- const response = await requestServer(server, '/tdpm.txt');
1684
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1684
1685
  expect(response.statusCode).toBe(200);
1685
1686
  expect(response.body).toBe(staticDashboardRaw);
1686
1687
  expect(response.contentType).toBe('text/html; charset=utf-8');
@@ -1706,7 +1707,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1706
1707
  dashboardDataDir: dataDir,
1707
1708
  });
1708
1709
  try {
1709
- const response = await requestServer(server, '/tdpm.txt');
1710
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1710
1711
  expect(response.statusCode).toBe(200);
1711
1712
  expect(response.body).toBe(staticDashboardRaw);
1712
1713
  } finally {
@@ -1739,7 +1740,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1739
1740
  dashboardDataDir: dataDir,
1740
1741
  });
1741
1742
  try {
1742
- const response = await requestServer(server, '/tdpm.txt');
1743
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1743
1744
  expect(response.statusCode).toBe(200);
1744
1745
  expect(response.body).toBe(staticDashboardRaw);
1745
1746
  } finally {
@@ -1759,7 +1760,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1759
1760
  dashboardDataDir: null,
1760
1761
  });
1761
1762
  try {
1762
- const response = await requestServer(server, '/tdpm.txt');
1763
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1763
1764
  expect(response.statusCode).toBe(404);
1764
1765
  } finally {
1765
1766
  await closeServer(server);
@@ -1774,7 +1775,7 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1774
1775
  dashboardDataDir: null,
1775
1776
  });
1776
1777
  try {
1777
- const response = await requestServer(server, '/tdpm.txt');
1778
+ const response = await requestServer(server, `/tdpm.txt?k=${testToken}`);
1778
1779
  expect(response.statusCode).toBe(404);
1779
1780
  } finally {
1780
1781
  await closeServer(server);
@@ -1803,6 +1804,66 @@ describe('webServer dashboard /tdpm.txt route integration', () => {
1803
1804
  fs.rmSync(staticDir, { recursive: true, force: true });
1804
1805
  }
1805
1806
  });
1807
+
1808
+ it('returns 401 for GET /tdpm.txt without a token', async () => {
1809
+ const staticDir = fs.mkdtempSync(
1810
+ path.join(os.tmpdir(), 'dashboard-static-'),
1811
+ );
1812
+ writeStaticDashboard(staticDir);
1813
+ const { server, tmpDir } = await startServer({
1814
+ dashboardDir: staticDir,
1815
+ dashboardDataDir: null,
1816
+ });
1817
+ try {
1818
+ const response = await requestServer(server, '/tdpm.txt');
1819
+ expect(response.statusCode).toBe(401);
1820
+ } finally {
1821
+ await closeServer(server);
1822
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1823
+ fs.rmSync(staticDir, { recursive: true, force: true });
1824
+ }
1825
+ });
1826
+
1827
+ it('returns 401 for GET /tdpm.txt with wrong token via query param', async () => {
1828
+ const staticDir = fs.mkdtempSync(
1829
+ path.join(os.tmpdir(), 'dashboard-static-'),
1830
+ );
1831
+ writeStaticDashboard(staticDir);
1832
+ const { server, tmpDir } = await startServer({
1833
+ dashboardDir: staticDir,
1834
+ dashboardDataDir: null,
1835
+ });
1836
+ try {
1837
+ const response = await requestServer(server, '/tdpm.txt?k=wrong-token');
1838
+ expect(response.statusCode).toBe(401);
1839
+ } finally {
1840
+ await closeServer(server);
1841
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1842
+ fs.rmSync(staticDir, { recursive: true, force: true });
1843
+ }
1844
+ });
1845
+
1846
+ it('returns 200 for GET /tdpm.txt with valid token via x-pv-token header', async () => {
1847
+ const staticDir = fs.mkdtempSync(
1848
+ path.join(os.tmpdir(), 'dashboard-static-'),
1849
+ );
1850
+ writeStaticDashboard(staticDir);
1851
+ const { server, tmpDir } = await startServer({
1852
+ dashboardDir: staticDir,
1853
+ dashboardDataDir: null,
1854
+ });
1855
+ try {
1856
+ const response = await requestServer(server, '/tdpm.txt', 'GET', {
1857
+ [CONSOLE_TOKEN_HEADER]: testToken,
1858
+ });
1859
+ expect(response.statusCode).toBe(200);
1860
+ expect(response.body).toBe(staticDashboardRaw);
1861
+ } finally {
1862
+ await closeServer(server);
1863
+ fs.rmSync(tmpDir, { recursive: true, force: true });
1864
+ fs.rmSync(staticDir, { recursive: true, force: true });
1865
+ }
1866
+ });
1806
1867
  });
1807
1868
 
1808
1869
  describe('webServer image proxy', () => {
@@ -791,6 +791,15 @@ export const handleWebRequest = async (
791
791
  sendNotFound(response);
792
792
  return;
793
793
  }
794
+ const providedToken = extractProvidedToken(
795
+ requestUrl.searchParams.get('k'),
796
+ request.headers[CONSOLE_TOKEN_HEADER],
797
+ extractCookieToken(request.headers.cookie),
798
+ );
799
+ if (!isTokenValid(options.accessToken, providedToken)) {
800
+ sendUnauthorized(response);
801
+ return;
802
+ }
794
803
  const dashboardContent = resolveDashboardContent(options, requestPath);
795
804
  if (dashboardContent === null) {
796
805
  sendNotFound(response);
@@ -118,6 +118,7 @@ describe('CheckIssueReviewReadinessUseCase', () => {
118
118
 
119
119
  const result = await useCase.run({
120
120
  issueUrl: 'https://github.com/user/repo/issues/1',
121
+ allowedIssueAuthors: ['agent-bot'],
121
122
  labelsAsLlmAgentName: ['chore'],
122
123
  labelsNotRequiringPullRequest: ['story'],
123
124
  });
@@ -182,6 +183,7 @@ describe('CheckIssueReviewReadinessUseCase', () => {
182
183
 
183
184
  const result = await useCase.run({
184
185
  issueUrl: 'https://github.com/user/repo/issues/1',
186
+ allowedIssueAuthors: ['agent-bot'],
185
187
  });
186
188
 
187
189
  expect(result.reviewReady).toBe(false);
@@ -223,6 +225,7 @@ describe('CheckIssueReviewReadinessUseCase', () => {
223
225
 
224
226
  const result = await useCase.run({
225
227
  issueUrl: 'https://github.com/user/repo/issues/1',
228
+ allowedIssueAuthors: ['agent-bot'],
226
229
  });
227
230
 
228
231
  expect(result.rejections).toEqual([]);
@@ -265,6 +268,7 @@ describe('CheckIssueReviewReadinessUseCase', () => {
265
268
 
266
269
  const result = await useCase.run({
267
270
  issueUrl: 'https://github.com/user/repo/issues/1',
271
+ allowedIssueAuthors: ['agent-bot'],
268
272
  });
269
273
 
270
274
  expect(result.reviewReady).toBe(true);
@@ -286,15 +290,18 @@ describe('CheckIssueReviewReadinessUseCase', () => {
286
290
 
287
291
  const result = await useCase.run({
288
292
  issueUrl: 'https://github.com/user/repo/issues/1',
293
+ allowedIssueAuthors: ['agent-bot'],
289
294
  });
290
295
 
291
296
  expect(result.reviewReady).toBe(false);
292
- expect(result.rejections[0].type).toBe(
293
- 'ANY_CI_JOB_FAILED_OR_IN_PROGRESS',
294
- );
297
+ expect(result.rejections).toContainEqual({
298
+ type: 'ANY_CI_JOB_FAILED_OR_IN_PROGRESS',
299
+ detail:
300
+ 'ANY_CI_JOB_FAILED_OR_IN_PROGRESS: https://github.com/user/repo/pull/1',
301
+ });
295
302
  });
296
303
 
297
- it('should treat all authors as trusted when allowedIssueAuthors is null', async () => {
304
+ it('should reject all authors when allowedIssueAuthors is null (fail-closed)', async () => {
298
305
  const issue = createMockIssue();
299
306
  mockIssueRepository.getIssueByUrl.mockResolvedValue(issue);
300
307
  mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
@@ -309,8 +316,11 @@ describe('CheckIssueReviewReadinessUseCase', () => {
309
316
  allowedIssueAuthors: null,
310
317
  });
311
318
 
312
- expect(result.reviewReady).toBe(true);
313
- expect(result.rejections).toEqual([]);
319
+ expect(result.reviewReady).toBe(false);
320
+ expect(result.rejections).toContainEqual({
321
+ type: 'NO_REPORT_FROM_AGENT_BOT',
322
+ detail: 'NO_REPORT_FROM_AGENT_BOT',
323
+ });
314
324
  });
315
325
 
316
326
  it('should reject when last comment author is not in allowedIssueAuthors list', async () => {
@@ -353,5 +363,27 @@ describe('CheckIssueReviewReadinessUseCase', () => {
353
363
  expect(result.reviewReady).toBe(true);
354
364
  expect(result.rejections).toEqual([]);
355
365
  });
366
+
367
+ it('should accept author when allowedIssueAuthors is an empty array (fail-closed)', async () => {
368
+ const issue = createMockIssue();
369
+ mockIssueRepository.getIssueByUrl.mockResolvedValue(issue);
370
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
371
+ createMockComment({ author: 'any-author' }),
372
+ ]);
373
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([
374
+ createReadyPr(),
375
+ ]);
376
+
377
+ const result = await useCase.run({
378
+ issueUrl: 'https://github.com/user/repo/issues/1',
379
+ allowedIssueAuthors: [],
380
+ });
381
+
382
+ expect(result.reviewReady).toBe(false);
383
+ expect(result.rejections).toContainEqual({
384
+ type: 'NO_REPORT_FROM_AGENT_BOT',
385
+ detail: 'NO_REPORT_FROM_AGENT_BOT',
386
+ });
387
+ });
356
388
  });
357
389
  });
@@ -8,6 +8,7 @@ import { resolveLabelsNotRequiringPullRequest } from './resolveLabelsNotRequirin
8
8
  import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
9
9
  import { normalizeReportBody } from './normalizeReportBody';
10
10
  import { isAgentReportBody } from './isAgentReportBody';
11
+ import { isAuthorAuthorizedForAutoStatusCheck } from './isAuthorAuthorizedForAutoStatusCheck';
11
12
 
12
13
  type RejectedReasonType =
13
14
  | 'ISSUE_NOT_FOUND'
@@ -67,7 +68,7 @@ export class CheckIssueReviewReadinessUseCase {
67
68
  await this.issueCommentRepository.getCommentsFromIssue(issue);
68
69
 
69
70
  const isTrustedAuthor = (author: string): boolean =>
70
- this.isAuthorTrusted(author, params.allowedIssueAuthors ?? null);
71
+ isAuthorAuthorizedForAutoStatusCheck(author, params.allowedIssueAuthors);
71
72
 
72
73
  const lastComment = comments[comments.length - 1];
73
74
  if (
@@ -110,12 +111,6 @@ export class CheckIssueReviewReadinessUseCase {
110
111
  };
111
112
  };
112
113
 
113
- private isAuthorTrusted = (
114
- author: string,
115
- allowedIssueAuthors: string[] | null,
116
- ): boolean =>
117
- allowedIssueAuthors === null || allowedIssueAuthors.includes(author);
118
-
119
114
  private reportBodyHasNextStep = (body: string): boolean => {
120
115
  const reportMatch = normalizeReportBody(body).match(
121
116
  /```json\n([\s\S]*?)\n```/,