@jrpool/kilotest 59.1.4 → 60.0.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.
- package/README.md +2 -5
- package/api/awaitTest.test.ts +9 -9
- package/api/awaitTest.ts +12 -12
- package/api/getReport.test.ts +4 -4
- package/api/getReport.ts +9 -9
- package/api/listDiagnoses.test.ts +14 -14
- package/api/listDiagnoses.ts +8 -8
- package/api/listIssues.test.ts +15 -15
- package/api/listIssues.ts +18 -20
- package/api/listPages.test.ts +2 -2
- package/api/listPages.ts +4 -4
- package/api/listViolators.test.ts +10 -10
- package/api/listViolators.ts +10 -10
- package/api/orderNewTest.test.ts +1 -2
- package/api/orderNewTest.ts +3 -4
- package/api/orderRetest.test.ts +10 -10
- package/api/orderRetest.ts +13 -14
- package/api/requestRetest.test.ts +11 -11
- package/api/requestRetest.ts +11 -11
- package/api/routes.ts +9 -11
- package/api/schemas.ts +18 -28
- package/api/util.test.ts +2 -2
- package/api/util.ts +7 -7
- package/capability.md +4 -4
- package/docs/AI-TOOLS.md +43 -1
- package/docs/decisions/009-report-id.md +16 -0
- package/index.test.ts +91 -41
- package/index.ts +66 -15
- package/lhm.plugin.json +33 -119
- package/llms-full.txt +18 -24
- package/llms.txt +12 -12
- package/mcp.test.ts +11 -20
- package/mcp.ts +18 -18
- package/openapi-3.0.yaml +1847 -0
- package/openapi.yaml +38 -97
- package/package.json +1 -1
- package/routing.test.ts +1 -1
- package/scripts/generate-openapi.ts +83 -1
- package/server.json +1 -1
- package/util.test.ts +32 -33
- package/util.ts +47 -43
- package/web/hideReportForm/index.ts +5 -6
- package/web/listDiagnoses/index.ts +4 -5
- package/web/listDiagnoses/listDiagnoses.test.ts +13 -13
- package/web/listIssues/index.ts +11 -12
- package/web/listIssues/listIssues.test.ts +24 -24
- package/web/listPages/index.ts +2 -2
- package/web/listPages/listPages.test.ts +2 -2
- package/web/listTopIssues/index.ts +2 -2
- package/web/listViolators/index.ts +5 -6
- package/web/listViolators/listViolators.test.ts +12 -12
- package/web/reannotate/index.ts +3 -3
- package/web/reannotateForm/index.ts +1 -2
- package/web/reportDeletion.ts +9 -12
- package/web/requestRetest/index.ts +2 -2
- package/web/requestRetest/requestRetest.test.ts +7 -7
- package/web/requestRetestForm/index.html +2 -2
- package/web/requestRetestForm/index.ts +3 -4
- package/web/requestRetestForm/requestRetestForm.test.ts +7 -8
- package/web/unhideReportForm/index.ts +4 -6
- package/web/util.test.ts +29 -29
- package/web/util.ts +1 -1
package/index.test.ts
CHANGED
|
@@ -341,29 +341,29 @@ test('GET /api/listPages returns JSON with report data and records an API-operat
|
|
|
341
341
|
assert.equal(await getMetricCount('apiOperations', 'listPages'), countBefore + 1);
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
test('GET /api/listIssues/260101T0000
|
|
345
|
-
const res = await request('GET', '/api/listIssues/260101T0000
|
|
344
|
+
test('GET /api/listIssues/260101T0000-mix returns JSON with issue data', async () => {
|
|
345
|
+
const res = await request('GET', '/api/listIssues/260101T0000-mix');
|
|
346
346
|
assert.equal(res.statusCode, 200);
|
|
347
347
|
const body = jsonBody(res);
|
|
348
348
|
assert.equal(body['tool name'], 'listIssues');
|
|
349
349
|
});
|
|
350
350
|
|
|
351
|
-
test('GET /api/listViolators/linkNoText/260101T0000
|
|
352
|
-
const res = await request('GET', '/api/listViolators/linkNoText/260101T0000
|
|
351
|
+
test('GET /api/listViolators/linkNoText/260101T0000-mix returns JSON', async () => {
|
|
352
|
+
const res = await request('GET', '/api/listViolators/linkNoText/260101T0000-mix');
|
|
353
353
|
assert.equal(res.statusCode, 200);
|
|
354
354
|
const body = jsonBody(res);
|
|
355
355
|
assert.equal(body['tool name'], 'listViolators');
|
|
356
356
|
});
|
|
357
357
|
|
|
358
|
-
test('GET /api/listDiagnoses/0/linkNoText/260101T0000
|
|
359
|
-
const res = await request('GET', '/api/listDiagnoses/0/linkNoText/260101T0000
|
|
358
|
+
test('GET /api/listDiagnoses/0/linkNoText/260101T0000-mix returns JSON', {timeout: 500}, async () => {
|
|
359
|
+
const res = await request('GET', '/api/listDiagnoses/0/linkNoText/260101T0000-mix');
|
|
360
360
|
assert.equal(res.statusCode, 200);
|
|
361
361
|
const body = jsonBody(res);
|
|
362
362
|
assert.equal(body['tool name'], 'listDiagnoses');
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
-
test('GET /api/getReport/260101T0000
|
|
366
|
-
const res = await request('GET', '/api/getReport/260101T0000
|
|
365
|
+
test('GET /api/getReport/260101T0000-mix returns JSON with the report', async () => {
|
|
366
|
+
const res = await request('GET', '/api/getReport/260101T0000-mix');
|
|
367
367
|
assert.equal(res.statusCode, 200);
|
|
368
368
|
const body = jsonBody(res);
|
|
369
369
|
assert.equal(body['tool name'], 'getReport');
|
|
@@ -376,8 +376,8 @@ test('GET /api/invalidService returns an error', {timeout: 500}, async () => {
|
|
|
376
376
|
assert.ok(body.error);
|
|
377
377
|
});
|
|
378
378
|
|
|
379
|
-
test('GET /fullReport.json/260101T0000
|
|
380
|
-
const res = await request('GET', '/fullReport.json/260101T0000
|
|
379
|
+
test('GET /fullReport.json/260101T0000-mix downloads the report as JSON', async () => {
|
|
380
|
+
const res = await request('GET', '/fullReport.json/260101T0000-mix');
|
|
381
381
|
assert.equal(res.statusCode, 200);
|
|
382
382
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
383
383
|
assert.ok(res.headers['content-disposition'].includes('attachment'));
|
|
@@ -392,8 +392,8 @@ test('GET /fullReport.json/invalid/invalid returns an error page', {timeout: 500
|
|
|
392
392
|
assert.ok(res.body.includes('Invalid report request'));
|
|
393
393
|
});
|
|
394
394
|
|
|
395
|
-
test('GET /fullReport.json/990101T0000
|
|
396
|
-
const res = await request('GET', '/fullReport.json/990101T0000
|
|
395
|
+
test('GET /fullReport.json/990101T0000-xxx returns an abuse error for a nonexistent report', async () => {
|
|
396
|
+
const res = await request('GET', '/fullReport.json/990101T0000-xxx');
|
|
397
397
|
assert.equal(res.statusCode, 400);
|
|
398
398
|
assert.ok(res.body.includes('Invalid request'));
|
|
399
399
|
});
|
|
@@ -495,10 +495,10 @@ test('POST /requestNewTest.html with an unreadable body returns an error', async
|
|
|
495
495
|
assert.ok(res.body.includes('Unreadable request body'));
|
|
496
496
|
});
|
|
497
497
|
|
|
498
|
-
test('POST /requestRetest.html/260202T0000
|
|
498
|
+
test('POST /requestRetest.html/260202T0000-new with valid data returns HTML', async () => {
|
|
499
499
|
// Reset testRequests.json to avoid duplicate-request errors from prior tests.
|
|
500
500
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
501
|
-
const res = await formRequest('POST', '/requestRetest.html/260202T0000
|
|
501
|
+
const res = await formRequest('POST', '/requestRetest.html/260202T0000-new', {
|
|
502
502
|
why: 'Because the report is obsolete and needs refreshing'
|
|
503
503
|
});
|
|
504
504
|
assert.equal(res.statusCode, 200);
|
|
@@ -524,8 +524,8 @@ test('POST /api/requestNewTest with valid JSON returns a JSON response', {timeou
|
|
|
524
524
|
assert.ok(body['tool name'] || body['response content']);
|
|
525
525
|
});
|
|
526
526
|
|
|
527
|
-
test('POST /api/requestRetest/260202T0000
|
|
528
|
-
const res = await request('POST', '/api/requestRetest/260202T0000
|
|
527
|
+
test('POST /api/requestRetest/260202T0000-new with valid JSON returns a JSON response', async () => {
|
|
528
|
+
const res = await request('POST', '/api/requestRetest/260202T0000-new', {
|
|
529
529
|
reason: 'Because the report is obsolete and needs refreshing'
|
|
530
530
|
});
|
|
531
531
|
assert.equal(res.statusCode, 200);
|
|
@@ -557,12 +557,12 @@ test('POST /api/orderNewTest with valid JSON enqueues a job directly and records
|
|
|
557
557
|
assert.equal(await getMetricCount('apiOperations', 'orderNewTest'), countBefore + 1);
|
|
558
558
|
const reportIdentifier = body['response content']['disposition of your order']['report identifier'];
|
|
559
559
|
assert.ok(reportIdentifier);
|
|
560
|
-
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier
|
|
560
|
+
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier}.json`));
|
|
561
561
|
});
|
|
562
562
|
|
|
563
|
-
test('POST /api/orderRetest/260202T0000
|
|
563
|
+
test('POST /api/orderRetest/260202T0000-new with valid JSON enqueues a job directly and records an API-operation metric', async () => {
|
|
564
564
|
const countBefore = await getMetricCount('apiOperations', 'orderRetest');
|
|
565
|
-
const res = await request('POST', '/api/orderRetest/260202T0000
|
|
565
|
+
const res = await request('POST', '/api/orderRetest/260202T0000-new', {
|
|
566
566
|
reason: 'Because the report is obsolete and needs refreshing'
|
|
567
567
|
});
|
|
568
568
|
assert.equal(res.statusCode, 200);
|
|
@@ -571,12 +571,12 @@ test('POST /api/orderRetest/260202T0000/new with valid JSON enqueues a job direc
|
|
|
571
571
|
assert.equal(await getMetricCount('apiOperations', 'orderRetest'), countBefore + 1);
|
|
572
572
|
const reportIdentifier = body['response content']['disposition of your order']['report identifier'];
|
|
573
573
|
assert.ok(reportIdentifier);
|
|
574
|
-
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier
|
|
574
|
+
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier}.json`));
|
|
575
575
|
});
|
|
576
576
|
|
|
577
|
-
test('POST /api/awaitTest/260101T0000
|
|
577
|
+
test('POST /api/awaitTest/260101T0000-mix with an already-completed report records an API-operation metric', async () => {
|
|
578
578
|
const countBefore = await getMetricCount('apiOperations', 'awaitTest');
|
|
579
|
-
const res = await request('POST', '/api/awaitTest/260101T0000
|
|
579
|
+
const res = await request('POST', '/api/awaitTest/260101T0000-mix', {});
|
|
580
580
|
assert.equal(res.statusCode, 200);
|
|
581
581
|
const body = jsonBody(res);
|
|
582
582
|
assert.equal(body['response content']['disposition of your wait'].outcome, 'completed');
|
|
@@ -771,8 +771,8 @@ test('POST /tutorialWebComment.html with content returns JSON', async () => {
|
|
|
771
771
|
|
|
772
772
|
// TESTS: additional branch coverage
|
|
773
773
|
|
|
774
|
-
test('GET /fullReport.json/260101T0007
|
|
775
|
-
const res = await request('GET', '/fullReport.json/260101T0007
|
|
774
|
+
test('GET /fullReport.json/260101T0007-hid returns an error for an unavailable report', async () => {
|
|
775
|
+
const res = await request('GET', '/fullReport.json/260101T0007-hid');
|
|
776
776
|
assert.equal(res.statusCode, 400);
|
|
777
777
|
assert.ok(res.body.includes('Invalid request'));
|
|
778
778
|
});
|
|
@@ -906,24 +906,74 @@ test('GET /qai/comments redirects to /tutorialAI.html', async () => {
|
|
|
906
906
|
|
|
907
907
|
test('GET /diagnoses.html/<segments> redirects to the equivalent /listDiagnoses.html/<segments>, preserving the query string', async () => {
|
|
908
908
|
const res = await request(
|
|
909
|
-
'GET', '/diagnoses.html/duplicateID/260426T1741
|
|
909
|
+
'GET', '/diagnoses.html/duplicateID/260426T1741-77w/718?pathID=/html/body', {}, {followRedirects: false}
|
|
910
910
|
);
|
|
911
911
|
assert.equal(res.statusCode, 301);
|
|
912
|
-
assert.equal(res.headers['location'], '/listDiagnoses.html/duplicateID/260426T1741
|
|
912
|
+
assert.equal(res.headers['location'], '/listDiagnoses.html/duplicateID/260426T1741-77w/718?pathID=/html/body');
|
|
913
913
|
});
|
|
914
914
|
|
|
915
|
-
test('GET /reportIssues.html/<
|
|
916
|
-
const res = await request('GET', '/reportIssues.html/260101T0000
|
|
915
|
+
test('GET /reportIssues.html/<reportID> redirects to the equivalent /listIssues.html/<reportID>', async () => {
|
|
916
|
+
const res = await request('GET', '/reportIssues.html/260101T0000-mix', {}, {followRedirects: false});
|
|
917
917
|
assert.equal(res.statusCode, 301);
|
|
918
|
-
assert.equal(res.headers['location'], '/listIssues.html/260101T0000
|
|
918
|
+
assert.equal(res.headers['location'], '/listIssues.html/260101T0000-mix');
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
// TESTS: legacy two-segment report-identifier redirects
|
|
922
|
+
// Before the reportID consolidation, report-identifying URL tails used a
|
|
923
|
+
// timeStamp segment and a jobID segment. These verify that each legacy form
|
|
924
|
+
// permanently redirects to the single-reportID form, and that a POST redirect
|
|
925
|
+
// uses 308 to preserve the method and body.
|
|
926
|
+
|
|
927
|
+
test('GET /listIssues.html/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
|
|
928
|
+
const res = await request('GET', '/listIssues.html/260101T0000/mix', {}, {followRedirects: false});
|
|
929
|
+
assert.equal(res.statusCode, 301);
|
|
930
|
+
assert.equal(res.headers['location'], '/listIssues.html/260101T0000-mix');
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
test('GET /listViolators.html/<issueID>/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
|
|
934
|
+
const res = await request(
|
|
935
|
+
'GET', '/listViolators.html/linkNoText/260101T0000/mix', {}, {followRedirects: false}
|
|
936
|
+
);
|
|
937
|
+
assert.equal(res.statusCode, 301);
|
|
938
|
+
assert.equal(res.headers['location'], '/listViolators.html/linkNoText/260101T0000-mix');
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
test('GET /listDiagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> redirects to the single-reportID URL, preserving the query string', async () => {
|
|
942
|
+
const res = await request(
|
|
943
|
+
'GET', '/listDiagnoses.html/linkNoText/260101T0000/mix/0?pathID=/html/body', {}, {followRedirects: false}
|
|
944
|
+
);
|
|
945
|
+
assert.equal(res.statusCode, 301);
|
|
946
|
+
assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0?pathID=/html/body');
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
test('GET /requestRetestForm.html/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
|
|
950
|
+
const res = await request(
|
|
951
|
+
'GET', '/requestRetestForm.html/260202T0000/new', {}, {followRedirects: false}
|
|
952
|
+
);
|
|
953
|
+
assert.equal(res.statusCode, 301);
|
|
954
|
+
assert.equal(res.headers['location'], '/requestRetestForm.html/260202T0000-new');
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('GET /fullReport.json/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
|
|
958
|
+
const res = await request('GET', '/fullReport.json/260101T0000/mix', {}, {followRedirects: false});
|
|
959
|
+
assert.equal(res.statusCode, 301);
|
|
960
|
+
assert.equal(res.headers['location'], '/fullReport.json/260101T0000-mix');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('POST /requestRetest.html/<timeStamp>/<jobID> redirects with 308 to the single-reportID URL', async () => {
|
|
964
|
+
const res = await formRequest(
|
|
965
|
+
'POST', '/requestRetest.html/260202T0000/new', {why: 'Because the report is obsolete and needs refreshing'}
|
|
966
|
+
);
|
|
967
|
+
assert.equal(res.statusCode, 308);
|
|
968
|
+
assert.equal(res.headers['location'], '/requestRetest.html/260202T0000-new');
|
|
919
969
|
});
|
|
920
970
|
|
|
921
971
|
test('GET /reportIssue.html/<segments> redirects to the equivalent /listViolators.html/<segments>', async () => {
|
|
922
972
|
const res = await request(
|
|
923
|
-
'GET', '/reportIssue.html/duplicateID/260101T0000
|
|
973
|
+
'GET', '/reportIssue.html/duplicateID/260101T0000-mix', {}, {followRedirects: false}
|
|
924
974
|
);
|
|
925
975
|
assert.equal(res.statusCode, 301);
|
|
926
|
-
assert.equal(res.headers['location'], '/listViolators.html/duplicateID/260101T0000
|
|
976
|
+
assert.equal(res.headers['location'], '/listViolators.html/duplicateID/260101T0000-mix');
|
|
927
977
|
});
|
|
928
978
|
|
|
929
979
|
test('GET /rules.html/<segment> redirects to the equivalent /listRules.html/<segment>', async () => {
|
|
@@ -1007,8 +1057,8 @@ test('POST /requestNewTest.html with a private-address URL returns an error unle
|
|
|
1007
1057
|
}
|
|
1008
1058
|
});
|
|
1009
1059
|
|
|
1010
|
-
test('POST /requestRetest.html/260202T0000
|
|
1011
|
-
const res = await formRequest('POST', '/requestRetest.html/260202T0000
|
|
1060
|
+
test('POST /requestRetest.html/260202T0000-new with missing why returns an error', async () => {
|
|
1061
|
+
const res = await formRequest('POST', '/requestRetest.html/260202T0000-new', {
|
|
1012
1062
|
why: ''
|
|
1013
1063
|
});
|
|
1014
1064
|
assert.equal(res.statusCode, 400);
|
|
@@ -1464,8 +1514,8 @@ test('GET /requestNewTest.html without arguments is rejected as an invalid GET r
|
|
|
1464
1514
|
assert.equal(res.statusCode, 400);
|
|
1465
1515
|
});
|
|
1466
1516
|
|
|
1467
|
-
test('GET /requestRetest.html/260202T0000
|
|
1468
|
-
const res = await request('GET', '/requestRetest.html/260202T0000
|
|
1517
|
+
test('GET /requestRetest.html/260202T0000-new is rejected as an invalid GET request', async () => {
|
|
1518
|
+
const res = await request('GET', '/requestRetest.html/260202T0000-new');
|
|
1469
1519
|
assert.equal(res.statusCode, 400);
|
|
1470
1520
|
});
|
|
1471
1521
|
|
|
@@ -1476,8 +1526,8 @@ test('GET /requestRetest.html/260202T0000/new is rejected as an invalid GET requ
|
|
|
1476
1526
|
|
|
1477
1527
|
const htmlPagePaths = [
|
|
1478
1528
|
'/listPages.html',
|
|
1479
|
-
'/listViolators.html/linkNoText/260101T0000
|
|
1480
|
-
'/listDiagnoses.html/linkNoText/260101T0000
|
|
1529
|
+
'/listViolators.html/linkNoText/260101T0000-mix',
|
|
1530
|
+
'/listDiagnoses.html/linkNoText/260101T0000-mix/0',
|
|
1481
1531
|
'/enqueueForm.html',
|
|
1482
1532
|
'/manage.html',
|
|
1483
1533
|
'/tutorialWeb.html',
|
|
@@ -1839,11 +1889,11 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
1839
1889
|
// First retest to create the recommendation. As above, the reason must match
|
|
1840
1890
|
// exactly between the two requests for processTestRequest to treat the second as
|
|
1841
1891
|
// a duplicate rather than a second, independent (and thus also 'ok') request.
|
|
1842
|
-
await formRequest('POST', '/requestRetest.html/260202T0000
|
|
1892
|
+
await formRequest('POST', '/requestRetest.html/260202T0000-new', {
|
|
1843
1893
|
why: 'Because the report is obsolete and needs refreshing'
|
|
1844
1894
|
});
|
|
1845
1895
|
// Second, identical retest of the same report should get a duplicate error.
|
|
1846
|
-
const res = await formRequest('POST', '/requestRetest.html/260202T0000
|
|
1896
|
+
const res = await formRequest('POST', '/requestRetest.html/260202T0000-new', {
|
|
1847
1897
|
why: 'Because the report is obsolete and needs refreshing'
|
|
1848
1898
|
});
|
|
1849
1899
|
assert.equal(res.statusCode, 400);
|
|
@@ -1985,7 +2035,7 @@ test('a request logs its path without its query string', async () => {
|
|
|
1985
2035
|
const originalLog = console.log;
|
|
1986
2036
|
console.log = (line: string) => logged.push(line);
|
|
1987
2037
|
try {
|
|
1988
|
-
await request('GET', '/listIssues.html/260101T0000
|
|
2038
|
+
await request('GET', '/listIssues.html/260101T0000-mix?authCode=secret');
|
|
1989
2039
|
}
|
|
1990
2040
|
finally {
|
|
1991
2041
|
console.log = originalLog;
|
|
@@ -1994,7 +2044,7 @@ test('a request logs its path without its query string', async () => {
|
|
|
1994
2044
|
.map(line => { try {return JSON.parse(line);} catch {return null;} })
|
|
1995
2045
|
.find(parsed => parsed?.type === 'request');
|
|
1996
2046
|
assert.ok(record);
|
|
1997
|
-
assert.equal(record.path, '/listIssues.html/260101T0000
|
|
2047
|
+
assert.equal(record.path, '/listIssues.html/260101T0000-mix');
|
|
1998
2048
|
assert.ok(!JSON.stringify(record).includes('secret'));
|
|
1999
2049
|
});
|
|
2000
2050
|
|
package/index.ts
CHANGED
|
@@ -23,8 +23,7 @@ import {
|
|
|
23
23
|
isReportError,
|
|
24
24
|
getReportPath,
|
|
25
25
|
hiddenReportsPath,
|
|
26
|
-
|
|
27
|
-
isJobID,
|
|
26
|
+
isReportID,
|
|
28
27
|
isURL,
|
|
29
28
|
isUsableReport,
|
|
30
29
|
isValidAuthCode,
|
|
@@ -306,6 +305,17 @@ const renamedPagePrefixes: Record<string, string> = {
|
|
|
306
305
|
requestTest: 'requestNewTest',
|
|
307
306
|
requestTestForm: 'requestNewTestForm'
|
|
308
307
|
};
|
|
308
|
+
// Pages whose report-identifying path tails once used two segments (a timeStamp
|
|
309
|
+
// segment and a jobID segment) where they now use one reportID segment. Each value
|
|
310
|
+
// gives the index of the first legacy segment within the path tail and the total
|
|
311
|
+
// segment count of the legacy tail.
|
|
312
|
+
// TEMPORARY: retire these redirects after 2027-04-17 once bookmarked links have aged out.
|
|
313
|
+
const legacyReportTails: Record<string, {idIndex: number; segmentCount: number}> = {
|
|
314
|
+
listDiagnoses: {idIndex: 1, segmentCount: 4},
|
|
315
|
+
listIssues: {idIndex: 0, segmentCount: 2},
|
|
316
|
+
listViolators: {idIndex: 1, segmentCount: 3},
|
|
317
|
+
requestRetestForm: {idIndex: 0, segmentCount: 2}
|
|
318
|
+
};
|
|
309
319
|
const jobLock = createLock();
|
|
310
320
|
|
|
311
321
|
// FUNCTIONS
|
|
@@ -325,6 +335,24 @@ export const isPathAllowed = (method: string, pathname: string): boolean => {
|
|
|
325
335
|
const patterns = (routes as Record<string, string[]>)[method] || [];
|
|
326
336
|
return patterns.some(pattern => matchPath(pattern, pathname));
|
|
327
337
|
};
|
|
338
|
+
// Returns the single-reportID URL that a legacy two-segment report URL (one whose
|
|
339
|
+
// path tail identifies a report with separate timeStamp and jobID segments)
|
|
340
|
+
// redirects to, or null if the page has no legacy form or the tail lacks its shape.
|
|
341
|
+
const getLegacyReportURL = (topic: string, pathTail: string): string | null => {
|
|
342
|
+
const legacy = legacyReportTails[topic];
|
|
343
|
+
if (!legacy) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const segments = pathTail.split('/');
|
|
347
|
+
const reportID = `${segments[legacy.idIndex]}-${segments[legacy.idIndex + 1]}`;
|
|
348
|
+
if (segments.length !== legacy.segmentCount || !isReportID(reportID)) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
const newTail = [
|
|
352
|
+
...segments.slice(0, legacy.idIndex), reportID, ...segments.slice(legacy.idIndex + 2)
|
|
353
|
+
].join('/');
|
|
354
|
+
return `/${topic}.html/${newTail}`;
|
|
355
|
+
};
|
|
328
356
|
// Serves or sends an error message.
|
|
329
357
|
export const serveError = async (error: Record<string, unknown>, response: ServerResponse, isHumanUser = true, statusCode = 400): Promise<void> => {
|
|
330
358
|
const errorLines = Object.entries(error).map(pair => `${pair[0]}: ${pair[1]}`);
|
|
@@ -711,11 +739,19 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
711
739
|
}
|
|
712
740
|
// Otherwise, if it is for a full report download:
|
|
713
741
|
else if (pageName === 'fullReport.json') {
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
742
|
+
const segments = pathTail.split('/');
|
|
743
|
+
const legacyID = `${segments[0]}-${segments[1]}`;
|
|
744
|
+
// If the request identifies the report with the legacy two-segment form (a
|
|
745
|
+
// timeStamp segment and a jobID segment), redirect the request permanently to
|
|
746
|
+
// the single-reportID URL, preserving any query string.
|
|
747
|
+
if (segments.length === 2 && isReportID(legacyID)) {
|
|
748
|
+
response.writeHead(301, {Location: `/fullReport.json/${legacyID}${search}`});
|
|
749
|
+
response.end();
|
|
750
|
+
}
|
|
751
|
+
// Otherwise, if the request is syntactically valid:
|
|
752
|
+
else if (isReportID(pathTail)) {
|
|
717
753
|
// Get it.
|
|
718
|
-
const report = await getReport(
|
|
754
|
+
const report = await getReport(pathTail);
|
|
719
755
|
// If this failed:
|
|
720
756
|
if (isReportError(report)) {
|
|
721
757
|
// This is not necessarily abuse: the link may have been generated (e.g. by
|
|
@@ -725,11 +761,11 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
725
761
|
// but alert a manager with the real reason, since a syntactically valid report ID
|
|
726
762
|
// should otherwise always be usable.
|
|
727
763
|
console.error(
|
|
728
|
-
`Full report ${
|
|
764
|
+
`Full report ${pathTail} requested but unavailable (${report.error})`
|
|
729
765
|
);
|
|
730
766
|
await sendAlert(
|
|
731
767
|
'Kilotest: requested full report unavailable',
|
|
732
|
-
`Full report ${
|
|
768
|
+
`Full report ${pathTail} was requested but could not be retrieved: ${report.error}`
|
|
733
769
|
);
|
|
734
770
|
await serveError({message: 'ERROR: Invalid request'}, response, true);
|
|
735
771
|
}
|
|
@@ -738,7 +774,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
738
774
|
// Serve response headers for a JSON download.
|
|
739
775
|
setHeaders('application/json', null, 'low');
|
|
740
776
|
response.setHeader(
|
|
741
|
-
'content-disposition', `attachment; filename="${
|
|
777
|
+
'content-disposition', `attachment; filename="${pathTail}.json"`,
|
|
742
778
|
);
|
|
743
779
|
// Download the report.
|
|
744
780
|
response.end(getJSON(report));
|
|
@@ -753,6 +789,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
753
789
|
// Otherwise, if it is for an HTML page other than the home page:
|
|
754
790
|
else if (pageName.endsWith('.html')) {
|
|
755
791
|
const topic = pageName.slice(0, -5);
|
|
792
|
+
const legacyReportURL = getLegacyReportURL(topic, pathTail);
|
|
756
793
|
// If it is for a page's old, pre-rename name:
|
|
757
794
|
if (renamedPagePrefixes[topic]) {
|
|
758
795
|
// Redirect the client permanently to the current page name, preserving any
|
|
@@ -761,6 +798,13 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
761
798
|
response.writeHead(301, {Location: newLocation});
|
|
762
799
|
response.end();
|
|
763
800
|
}
|
|
801
|
+
// Otherwise, if the URL identifies a report with the legacy two-segment form
|
|
802
|
+
// (a timeStamp segment and a jobID segment), redirect the request permanently
|
|
803
|
+
// to the URL with the single reportID segment, preserving the query string.
|
|
804
|
+
else if (legacyReportURL !== null) {
|
|
805
|
+
response.writeHead(301, {Location: `${legacyReportURL}${search}`});
|
|
806
|
+
response.end();
|
|
807
|
+
}
|
|
764
808
|
// Otherwise, if the page can be generated and is not POST-only (a POST-only path also matches
|
|
765
809
|
// the '*.html*' GET pattern, but its handler expects POST's argument list and
|
|
766
810
|
// performs no GET-appropriate rendering; a self-submitting manager page is POST-allowed
|
|
@@ -985,9 +1029,17 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
985
1029
|
// Otherwise, if it is a retest request:
|
|
986
1030
|
else if (pageName === 'requestRetest.html') {
|
|
987
1031
|
const {why} = postData as {why?: string};
|
|
988
|
-
const
|
|
989
|
-
|
|
990
|
-
|
|
1032
|
+
const segments = pathTail.split('/');
|
|
1033
|
+
const legacyID = `${segments[0]}-${segments[1]}`;
|
|
1034
|
+
// If the request identifies the report with the legacy two-segment form,
|
|
1035
|
+
// redirect it to the single-reportID URL. A 308 (not 301) keeps the POST
|
|
1036
|
+
// method and body, which a 301 could rewrite to a GET with no body.
|
|
1037
|
+
if (segments.length === 2 && isReportID(legacyID)) {
|
|
1038
|
+
response.writeHead(308, {Location: `/requestRetest.html/${legacyID}`});
|
|
1039
|
+
response.end();
|
|
1040
|
+
}
|
|
1041
|
+
// Otherwise, if the request is valid:
|
|
1042
|
+
else if (isReportID(pathTail) && why) {
|
|
991
1043
|
// Serve response headers.
|
|
992
1044
|
setHeaders('text/html', pathname, 'ultra');
|
|
993
1045
|
// Get the answer data.
|
|
@@ -1138,9 +1190,8 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
1138
1190
|
const reportObj: Partial<Report> = report ?? {};
|
|
1139
1191
|
const {id, target} = reportObj;
|
|
1140
1192
|
const {what: description, url} = (target ?? {}) as Partial<NonNullable<Report['target']>>;
|
|
1141
|
-
const [timeStamp, jobID] = (id?.split('-') ?? ['', '']) as [string, string];
|
|
1142
1193
|
// If the request is syntactically valid:
|
|
1143
|
-
if (id &&
|
|
1194
|
+
if (id && isReportID(id) && description && url) {
|
|
1144
1195
|
// Get the job the report is from. A missing claimed-job file (the job was
|
|
1145
1196
|
// never claimed, or was already completed) is a normal outcome, handled below
|
|
1146
1197
|
// as if the job were not assigned to this worker; any other failure to read it
|
|
@@ -1186,7 +1237,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
1186
1237
|
// could fail even though the just-written report is fine.
|
|
1187
1238
|
await annotateReportObject(reportObj);
|
|
1188
1239
|
// Save the annotated report.
|
|
1189
|
-
await fs.writeFile(getReportPath(
|
|
1240
|
+
await fs.writeFile(getReportPath(id), getJSON(report));
|
|
1190
1241
|
console.log(`Testaro report ${id} was annotated and saved`);
|
|
1191
1242
|
// Check the monetary balances and send alerts if nearing exhaustion.
|
|
1192
1243
|
await checkBalancesForAlerts(report!);
|