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.
- package/CHANGELOG.md +21 -0
- package/README.md +21 -1
- package/bin/adapter/entry-points/console/webServer.js +5 -0
- package/bin/adapter/entry-points/console/webServer.js.map +1 -1
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js +2 -2
- package/bin/domain/usecases/CheckIssueReviewReadinessUseCase.js.map +1 -1
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js +12 -2
- package/bin/domain/usecases/NotifyFinishedIssuePreparationUseCase.js.map +1 -1
- package/bin/domain/usecases/StartPreparationUseCase.js +2 -3
- package/bin/domain/usecases/StartPreparationUseCase.js.map +1 -1
- package/package.json +1 -1
- package/src/adapter/entry-points/console/webServer.test.ts +68 -7
- package/src/adapter/entry-points/console/webServer.ts +9 -0
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.test.ts +38 -6
- package/src/domain/usecases/CheckIssueReviewReadinessUseCase.ts +2 -7
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.test.ts +315 -143
- package/src/domain/usecases/NotifyFinishedIssuePreparationUseCase.ts +21 -7
- package/src/domain/usecases/StartPreparationUseCase.ts +2 -3
- package/types/adapter/entry-points/console/webServer.d.ts.map +1 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts +0 -1
- package/types/domain/usecases/CheckIssueReviewReadinessUseCase.d.ts.map +1 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts +0 -1
- package/types/domain/usecases/NotifyFinishedIssuePreparationUseCase.d.ts.map +1 -1
- 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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
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(
|
|
313
|
-
expect(result.rejections).
|
|
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
|
-
|
|
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```/,
|