@jrpool/kilotest 55.0.0 → 57.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 +1 -1
- package/api/awaitTest.test.ts +114 -0
- package/api/awaitTest.ts +108 -0
- package/api/listReports.ts +2 -2
- package/api/orderNewTest.test.ts +213 -0
- package/api/orderNewTest.ts +145 -0
- package/api/orderRetest.test.ts +179 -0
- package/api/orderRetest.ts +119 -0
- package/api/{requestTest.test.ts → requestNewTest.test.ts} +25 -25
- package/api/{requestTest.ts → requestNewTest.ts} +16 -39
- package/api/requestRetest.test.ts +6 -6
- package/api/requestRetest.ts +10 -33
- package/api/routes.test.ts +7 -4
- package/api/routes.ts +45 -7
- package/api/schemas.ts +120 -10
- package/api/util.ts +59 -2
- package/api/version.ts +6 -2
- package/capability.md +8 -5
- package/docs/SERVICE.md +6 -8
- package/env.example +15 -0
- package/index.test.ts +95 -24
- package/index.ts +80 -16
- package/lhm.plugin.json +1 -1
- package/linkValidation.test.ts +2 -2
- package/llms-full.txt +34 -3
- package/llms.txt +5 -2
- package/mcp.test.ts +72 -12
- package/mcp.ts +81 -11
- package/memory/MEMORY.md +1 -0
- package/memory/plan_issue_111_instant_test.md +198 -0
- package/openapi.yaml +451 -4
- package/package.json +1 -1
- package/routes.test.ts +1 -1
- package/routing.test.ts +2 -2
- package/server.json +1 -1
- package/sitemap.xml +1 -1
- package/smokeTest.ts +2 -2
- package/util.test.ts +214 -2
- package/util.ts +175 -7
- package/web/listReports/index.html +1 -1
- package/web/listReports/listReports.test.ts +1 -1
- package/web/{requestTest → requestNewTest}/index.ts +1 -1
- package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +2 -2
- package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
- package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
- /package/web/{requestTest → requestNewTest}/index.html +0 -0
- /package/web/{requestTestForm → requestNewTestForm}/index.ts +0 -0
package/index.test.ts
CHANGED
|
@@ -24,7 +24,7 @@ process.env.TESTARO_WORKERS = JSON.stringify({
|
|
|
24
24
|
// isAllowedTarget performs. Tests of the check itself (below) override this per test.
|
|
25
25
|
process.env.ALLOW_INTERNAL_TARGETS = 'true';
|
|
26
26
|
// Blank the alert configuration unconditionally, so this file sends no real alert
|
|
27
|
-
// emails (e.g. from the
|
|
27
|
+
// emails (e.g. from the requestNewTest/requestRetest/tutorial-comment tests below) even
|
|
28
28
|
// when run directly (e.g. `npx tsx --test index.test.ts`) rather than via `npm test`,
|
|
29
29
|
// which normally guards against this by --require-ing test/setup.ts first. dotenv does
|
|
30
30
|
// not override an already-set env var, so setting these here, before index.ts is ever
|
|
@@ -459,8 +459,8 @@ test('GET /forbidden-path returns an invalid GET request error', async () => {
|
|
|
459
459
|
|
|
460
460
|
// TESTS: POST routes
|
|
461
461
|
|
|
462
|
-
test('POST /
|
|
463
|
-
const res = await formRequest('POST', '/
|
|
462
|
+
test('POST /requestNewTest.html with valid data returns an HTML page', async () => {
|
|
463
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
464
464
|
description: `Unique Test Page ${uniqueStamp}`,
|
|
465
465
|
url: `https://example.com/unique-${uniqueStamp}`,
|
|
466
466
|
why: 'Because accessibility matters'
|
|
@@ -469,8 +469,8 @@ test('POST /requestTest.html with valid data returns an HTML page', async () =>
|
|
|
469
469
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
470
470
|
});
|
|
471
471
|
|
|
472
|
-
test('POST /
|
|
473
|
-
const res = await formRequest('POST', '/
|
|
472
|
+
test('POST /requestNewTest.html with an already-tested URL returns an error', async () => {
|
|
473
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
474
474
|
description: 'Mixed Outcomes Page',
|
|
475
475
|
url: 'https://example.com/mixed',
|
|
476
476
|
why: 'Because accessibility matters'
|
|
@@ -479,8 +479,8 @@ test('POST /requestTest.html with an already-tested URL returns an error', async
|
|
|
479
479
|
assert.ok(res.body.includes('already been tested'));
|
|
480
480
|
});
|
|
481
481
|
|
|
482
|
-
test('POST /
|
|
483
|
-
const res = await formRequest('POST', '/
|
|
482
|
+
test('POST /requestNewTest.html with invalid data returns an error', async () => {
|
|
483
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
484
484
|
description: '',
|
|
485
485
|
url: 'not-a-url',
|
|
486
486
|
why: ''
|
|
@@ -489,8 +489,8 @@ test('POST /requestTest.html with invalid data returns an error', async () => {
|
|
|
489
489
|
assert.ok(res.body.includes('Invalid test request'));
|
|
490
490
|
});
|
|
491
491
|
|
|
492
|
-
test('POST /
|
|
493
|
-
const res = await request('POST', '/
|
|
492
|
+
test('POST /requestNewTest.html with an unreadable body returns an error', async () => {
|
|
493
|
+
const res = await request('POST', '/requestNewTest.html', 'rawbody', {'content-type': 'text/plain'});
|
|
494
494
|
assert.equal(res.statusCode, 400);
|
|
495
495
|
assert.ok(res.body.includes('Unreadable request body'));
|
|
496
496
|
});
|
|
@@ -513,8 +513,8 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
513
513
|
assert.ok(res.body.includes('Invalid retest request'));
|
|
514
514
|
});
|
|
515
515
|
|
|
516
|
-
test('POST /api/
|
|
517
|
-
const res = await request('POST', '/api/
|
|
516
|
+
test('POST /api/requestNewTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
517
|
+
const res = await request('POST', '/api/requestNewTest', {
|
|
518
518
|
description: `API Test Page ${uniqueStamp}`,
|
|
519
519
|
URL: `https://example.com/api-test-${uniqueStamp}`,
|
|
520
520
|
reason: 'Because accessibility matters'
|
|
@@ -544,6 +544,45 @@ test('POST /api/requestFeature with valid JSON returns a JSON response and recor
|
|
|
544
544
|
assert.equal(await getMetricCount('apiOperations', 'requestFeature'), countBefore + 1);
|
|
545
545
|
});
|
|
546
546
|
|
|
547
|
+
test('POST /api/orderNewTest with valid JSON enqueues a job directly and records an API-operation metric', {timeout: 500}, async () => {
|
|
548
|
+
const countBefore = await getMetricCount('apiOperations', 'orderNewTest');
|
|
549
|
+
const res = await request('POST', '/api/orderNewTest', {
|
|
550
|
+
description: `API Ordered Page ${uniqueStamp}`,
|
|
551
|
+
URL: `https://example.com/api-ordered-${uniqueStamp}`,
|
|
552
|
+
reason: 'Because accessibility matters here'
|
|
553
|
+
});
|
|
554
|
+
assert.equal(res.statusCode, 200);
|
|
555
|
+
const body = jsonBody(res);
|
|
556
|
+
assert.ok(body['tool name'] || body['response content']);
|
|
557
|
+
assert.equal(await getMetricCount('apiOperations', 'orderNewTest'), countBefore + 1);
|
|
558
|
+
const reportIdentifier = body['response content']['disposition of your order']['report identifier'];
|
|
559
|
+
assert.ok(reportIdentifier);
|
|
560
|
+
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`));
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test('POST /api/orderRetest/260202T0000/new with valid JSON enqueues a job directly and records an API-operation metric', async () => {
|
|
564
|
+
const countBefore = await getMetricCount('apiOperations', 'orderRetest');
|
|
565
|
+
const res = await request('POST', '/api/orderRetest/260202T0000/new', {
|
|
566
|
+
reason: 'Because the report is obsolete and needs refreshing'
|
|
567
|
+
});
|
|
568
|
+
assert.equal(res.statusCode, 200);
|
|
569
|
+
const body = jsonBody(res);
|
|
570
|
+
assert.ok(body['tool name'] || body['response content']);
|
|
571
|
+
assert.equal(await getMetricCount('apiOperations', 'orderRetest'), countBefore + 1);
|
|
572
|
+
const reportIdentifier = body['response content']['disposition of your order']['report identifier'];
|
|
573
|
+
assert.ok(reportIdentifier);
|
|
574
|
+
await fs.unlink(path.join(fixtureDBDir, 'jobs', 'queue', `${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`));
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test('POST /api/awaitTest/260101T0000/mix with an already-completed report records an API-operation metric', async () => {
|
|
578
|
+
const countBefore = await getMetricCount('apiOperations', 'awaitTest');
|
|
579
|
+
const res = await request('POST', '/api/awaitTest/260101T0000/mix', {});
|
|
580
|
+
assert.equal(res.statusCode, 200);
|
|
581
|
+
const body = jsonBody(res);
|
|
582
|
+
assert.equal(body['response content']['disposition of your wait'].outcome, 'completed');
|
|
583
|
+
assert.equal(await getMetricCount('apiOperations', 'awaitTest'), countBefore + 1);
|
|
584
|
+
});
|
|
585
|
+
|
|
547
586
|
test('POST /api/invalidService returns an error', async () => {
|
|
548
587
|
const res = await request('POST', '/api/invalidService', {data: 'test'});
|
|
549
588
|
assert.equal(res.statusCode, 400);
|
|
@@ -697,7 +736,7 @@ test('POST /requestAction.html with invalid auth code returns an error and recor
|
|
|
697
736
|
});
|
|
698
737
|
|
|
699
738
|
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML and records a managerActivity success', async () => {
|
|
700
|
-
await formRequest('POST', '/
|
|
739
|
+
await formRequest('POST', '/requestNewTest.html', {
|
|
701
740
|
description: `Reject Test Page ${uniqueStamp}`,
|
|
702
741
|
url: `https://example.com/reject-${uniqueStamp}`,
|
|
703
742
|
why: 'Because accessibility matters'
|
|
@@ -740,7 +779,7 @@ test('GET /fullReport.json/260101T0007/hid returns an error for an unavailable r
|
|
|
740
779
|
|
|
741
780
|
test('POST /requestAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
|
|
742
781
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
743
|
-
await formRequest('POST', '/
|
|
782
|
+
await formRequest('POST', '/requestNewTest.html', {
|
|
744
783
|
description: `Approval Test Page ${uniqueStamp}`,
|
|
745
784
|
url: `https://example.com/approval-${uniqueStamp}`,
|
|
746
785
|
why: 'Because accessibility matters'
|
|
@@ -899,8 +938,40 @@ test('GET /targets.html redirects to /listReports.html', async () => {
|
|
|
899
938
|
assert.equal(res.headers['location'], '/listReports.html');
|
|
900
939
|
});
|
|
901
940
|
|
|
902
|
-
test('
|
|
903
|
-
const res = await
|
|
941
|
+
test('GET /requestTest.html redirects to /requestNewTest.html', async () => {
|
|
942
|
+
const res = await request('GET', '/requestTest.html', {}, {followRedirects: false});
|
|
943
|
+
assert.equal(res.statusCode, 301);
|
|
944
|
+
assert.equal(res.headers['location'], '/requestNewTest.html');
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
test('GET /requestTestForm.html redirects to /requestNewTestForm.html', async () => {
|
|
948
|
+
const res = await request('GET', '/requestTestForm.html', {}, {followRedirects: false});
|
|
949
|
+
assert.equal(res.statusCode, 301);
|
|
950
|
+
assert.equal(res.headers['location'], '/requestNewTestForm.html');
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
test('POST /api/requestTest redirects (308, preserving method) to /api/requestNewTest', async () => {
|
|
954
|
+
const res = await request('POST', '/api/requestTest', {
|
|
955
|
+
description: `Old Path Page ${uniqueStamp}`,
|
|
956
|
+
URL: `https://example.com/old-path-${uniqueStamp}`,
|
|
957
|
+
reason: 'Because accessibility matters'
|
|
958
|
+
}, {followRedirects: false});
|
|
959
|
+
assert.equal(res.statusCode, 308);
|
|
960
|
+
assert.equal(res.headers['location'], '/api/requestNewTest');
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test('POST /api/orderTest redirects (308, preserving method) to /api/orderNewTest', async () => {
|
|
964
|
+
const res = await request('POST', '/api/orderTest', {
|
|
965
|
+
description: `Old Path Ordered Page ${uniqueStamp}`,
|
|
966
|
+
URL: `https://example.com/old-path-ordered-${uniqueStamp}`,
|
|
967
|
+
reason: 'Because accessibility matters'
|
|
968
|
+
}, {followRedirects: false});
|
|
969
|
+
assert.equal(res.statusCode, 308);
|
|
970
|
+
assert.equal(res.headers['location'], '/api/orderNewTest');
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
test('POST /requestNewTest.html with non-https URL returns an error', async () => {
|
|
974
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
904
975
|
description: 'Test Page',
|
|
905
976
|
url: 'http://example.com',
|
|
906
977
|
why: 'Because accessibility matters'
|
|
@@ -909,10 +980,10 @@ test('POST /requestTest.html with non-https URL returns an error', async () => {
|
|
|
909
980
|
assert.ok(res.body.includes('Invalid test request'));
|
|
910
981
|
});
|
|
911
982
|
|
|
912
|
-
test('POST /
|
|
983
|
+
test('POST /requestNewTest.html with a private-address URL returns an error unless internal targets are allowed', async () => {
|
|
913
984
|
delete process.env.ALLOW_INTERNAL_TARGETS;
|
|
914
985
|
try {
|
|
915
|
-
const res = await formRequest('POST', '/
|
|
986
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
916
987
|
description: 'Internal Page',
|
|
917
988
|
url: 'https://192.168.1.1/page',
|
|
918
989
|
why: 'Because accessibility matters'
|
|
@@ -1370,14 +1441,14 @@ test('GET /style.css with a read error returns an error page', async () => {
|
|
|
1370
1441
|
}
|
|
1371
1442
|
});
|
|
1372
1443
|
|
|
1373
|
-
//
|
|
1444
|
+
// requestNewTest.html and requestRetest.html are POST-only routes (see the
|
|
1374
1445
|
// `routes.POST` list above). They also match the generic '*.html*' GET pattern,
|
|
1375
1446
|
// but the generic `pageName.endsWith('.html')` branch of the GET dispatcher
|
|
1376
1447
|
// excludes any path that is POST-only, since such a path's answer handler
|
|
1377
1448
|
// expects POST's argument list and performs no GET-appropriate rendering.
|
|
1378
1449
|
|
|
1379
|
-
test('GET /
|
|
1380
|
-
const res = await request('GET', '/
|
|
1450
|
+
test('GET /requestNewTest.html without arguments is rejected as an invalid GET request', async () => {
|
|
1451
|
+
const res = await request('GET', '/requestNewTest.html');
|
|
1381
1452
|
assert.equal(res.statusCode, 400);
|
|
1382
1453
|
});
|
|
1383
1454
|
|
|
@@ -1731,18 +1802,18 @@ test('POST /mcp with a valid initialize request returns server info via SSE', as
|
|
|
1731
1802
|
|
|
1732
1803
|
// TESTS: answer error branches
|
|
1733
1804
|
|
|
1734
|
-
test('POST /
|
|
1805
|
+
test('POST /requestNewTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
1735
1806
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1736
1807
|
// First request to create the request. processTestRequest treats a request as a
|
|
1737
1808
|
// duplicate only when description, URL, AND reason all match an existing pending
|
|
1738
1809
|
// request, so the reason must be identical between the two requests here.
|
|
1739
|
-
await formRequest('POST', '/
|
|
1810
|
+
await formRequest('POST', '/requestNewTest.html', {
|
|
1740
1811
|
description: `Dup Test Page ${uniqueStamp}`,
|
|
1741
1812
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
1742
1813
|
why: 'Because accessibility matters'
|
|
1743
1814
|
});
|
|
1744
1815
|
// Second, identical request should get a duplicate error.
|
|
1745
|
-
const res = await formRequest('POST', '/
|
|
1816
|
+
const res = await formRequest('POST', '/requestNewTest.html', {
|
|
1746
1817
|
description: `Dup Test Page ${uniqueStamp}`,
|
|
1747
1818
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
1748
1819
|
why: 'Because accessibility matters'
|
|
@@ -1770,7 +1841,7 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
1770
1841
|
test('POST /requestAction.html with valid auth code approves the same target twice without error', {timeout: 500}, async () => {
|
|
1771
1842
|
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1772
1843
|
// Create a request.
|
|
1773
|
-
await formRequest('POST', '/
|
|
1844
|
+
await formRequest('POST', '/requestNewTest.html', {
|
|
1774
1845
|
description: `Action Dup Page ${uniqueStamp}`,
|
|
1775
1846
|
url: `https://example.com/action-dup-${uniqueStamp}`,
|
|
1776
1847
|
why: 'Because accessibility matters'
|
package/index.ts
CHANGED
|
@@ -67,8 +67,8 @@ import {answer as renewWCAG} from './web/renewWCAG/index.ts';
|
|
|
67
67
|
import {answer as renewWCAGForm} from './web/renewWCAGForm/index.ts';
|
|
68
68
|
import {answer as requestRetestPage} from './web/requestRetest/index.ts';
|
|
69
69
|
import {answer as requestRetestForm} from './web/requestRetestForm/index.ts';
|
|
70
|
-
import {answer as
|
|
71
|
-
import {answer as
|
|
70
|
+
import {answer as requestNewTestPage} from './web/requestNewTest/index.ts';
|
|
71
|
+
import {answer as requestNewTestForm} from './web/requestNewTestForm/index.ts';
|
|
72
72
|
import {answer as rewindReportsForm} from './web/rewindReportsForm/index.ts';
|
|
73
73
|
import {answer as showHiddenReportsForm} from './web/showHiddenReportsForm/index.ts';
|
|
74
74
|
import {answer as unhideReportForm} from './web/unhideReportForm/index.ts';
|
|
@@ -79,7 +79,10 @@ import {response as listReportsAPI} from './api/listReports.ts';
|
|
|
79
79
|
import {response as listViolatorsAPI} from './api/listViolators.ts';
|
|
80
80
|
import {response as requestFeatureAPI} from './api/requestFeature.ts';
|
|
81
81
|
import {response as requestRetestAPI} from './api/requestRetest.ts';
|
|
82
|
-
import {response as
|
|
82
|
+
import {response as requestNewTestAPI} from './api/requestNewTest.ts';
|
|
83
|
+
import {response as orderNewTestAPI} from './api/orderNewTest.ts';
|
|
84
|
+
import {response as orderRetestAPI} from './api/orderRetest.ts';
|
|
85
|
+
import {response as awaitTestAPI} from './api/awaitTest.ts';
|
|
83
86
|
|
|
84
87
|
// ENVIRONMENT
|
|
85
88
|
|
|
@@ -124,8 +127,8 @@ const answer: {
|
|
|
124
127
|
renewWCAGForm: PageHandler;
|
|
125
128
|
requestRetest: PageHandler;
|
|
126
129
|
requestRetestForm: PageHandler;
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
requestNewTest: PageHandler;
|
|
131
|
+
requestNewTestForm: PageHandler;
|
|
129
132
|
rewindReportsForm: PageHandler;
|
|
130
133
|
showHiddenReportsForm: PageHandler;
|
|
131
134
|
unhideReportForm: PageHandler;
|
|
@@ -154,8 +157,8 @@ const answer: {
|
|
|
154
157
|
renewWCAGForm,
|
|
155
158
|
requestRetest: requestRetestPage,
|
|
156
159
|
requestRetestForm,
|
|
157
|
-
|
|
158
|
-
|
|
160
|
+
requestNewTest: requestNewTestPage,
|
|
161
|
+
requestNewTestForm,
|
|
159
162
|
rewindReportsForm,
|
|
160
163
|
showHiddenReportsForm,
|
|
161
164
|
unhideReportForm
|
|
@@ -170,7 +173,10 @@ const apiRespond: {
|
|
|
170
173
|
listViolators: ApiResponder;
|
|
171
174
|
requestFeature: ApiResponder;
|
|
172
175
|
requestRetest: ApiResponder;
|
|
173
|
-
|
|
176
|
+
requestNewTest: ApiResponder;
|
|
177
|
+
orderNewTest: ApiResponder;
|
|
178
|
+
orderRetest: ApiResponder;
|
|
179
|
+
awaitTest: ApiResponder;
|
|
174
180
|
} = {
|
|
175
181
|
getReport: getReportAPI,
|
|
176
182
|
listDiagnoses: listDiagnosesAPI,
|
|
@@ -179,7 +185,10 @@ const apiRespond: {
|
|
|
179
185
|
listViolators: listViolatorsAPI,
|
|
180
186
|
requestFeature: requestFeatureAPI,
|
|
181
187
|
requestRetest: requestRetestAPI,
|
|
182
|
-
|
|
188
|
+
requestNewTest: requestNewTestAPI,
|
|
189
|
+
orderNewTest: orderNewTestAPI,
|
|
190
|
+
orderRetest: orderRetestAPI,
|
|
191
|
+
awaitTest: awaitTestAPI
|
|
183
192
|
};
|
|
184
193
|
|
|
185
194
|
// CONSTANTS
|
|
@@ -226,7 +235,7 @@ export const routes = {
|
|
|
226
235
|
'/requestAction.html',
|
|
227
236
|
'/renewWCAG.html',
|
|
228
237
|
'/requestRetest.html/*',
|
|
229
|
-
'/
|
|
238
|
+
'/requestNewTest.html',
|
|
230
239
|
'/tutorialWebComment.html',
|
|
231
240
|
'/unhideReportForm.html',
|
|
232
241
|
'/worker/job',
|
|
@@ -276,12 +285,18 @@ const noDirectGetPages = new Set(['unhideReportForm']);
|
|
|
276
285
|
// engines and other crawlers are still requesting. Redirecting them lets crawlers update
|
|
277
286
|
// their own indexes, rather than leaving them to repeat the same dead request indefinitely.
|
|
278
287
|
// TEMPORARY: retire this redirect after 2027-04-01 once crawlers have re-indexed.
|
|
288
|
+
// requestTest and requestTestForm were renamed to requestNewTest and requestNewTestForm
|
|
289
|
+
// on 2026-09-28 (see the newTest/retest naming-ambiguity fix), to distinguish the
|
|
290
|
+
// new-test-only page from bare "test" language used elsewhere.
|
|
291
|
+
// TEMPORARY: retire this redirect after 2027-03-28 once crawlers have re-indexed.
|
|
279
292
|
const renamedPagePrefixes: Record<string, string> = {
|
|
280
293
|
diagnoses: 'listDiagnoses',
|
|
281
294
|
reportIssue: 'listViolators',
|
|
282
295
|
reportIssues: 'listIssues',
|
|
283
296
|
rules: 'listRules',
|
|
284
|
-
targets: 'listReports'
|
|
297
|
+
targets: 'listReports',
|
|
298
|
+
requestTest: 'requestNewTest',
|
|
299
|
+
requestTestForm: 'requestNewTestForm'
|
|
285
300
|
};
|
|
286
301
|
const jobLock = createLock();
|
|
287
302
|
|
|
@@ -927,7 +942,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
927
942
|
await serveError({message: 'ERROR: Unreadable request body'}, response, true);
|
|
928
943
|
}
|
|
929
944
|
// If the request is a test request:
|
|
930
|
-
else if (pageName === '
|
|
945
|
+
else if (pageName === 'requestNewTest.html') {
|
|
931
946
|
const {description, url, why} = postData as {description?: string; url: string; why?: string};
|
|
932
947
|
// If the request is valid:
|
|
933
948
|
if (description && isURL(url) && why && await isAllowedTarget(url)) {
|
|
@@ -941,7 +956,7 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
941
956
|
// Serve headers for a response.
|
|
942
957
|
setHeaders('text/html', pathname, 'ultra');
|
|
943
958
|
// Get the answer data.
|
|
944
|
-
const answerData = await answer.
|
|
959
|
+
const answerData = await answer.requestNewTest(description, url, why);
|
|
945
960
|
// If they are valid:
|
|
946
961
|
if (answerData.status === 'ok') {
|
|
947
962
|
// Serve the answer page.
|
|
@@ -1226,15 +1241,26 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
1226
1241
|
// Get the segments of the path after api.
|
|
1227
1242
|
const segments = pathTail.split('/');
|
|
1228
1243
|
// If the service is to receive a test request:
|
|
1229
|
-
if (segments[0] === '
|
|
1244
|
+
if (segments[0] === 'requestNewTest') {
|
|
1230
1245
|
const {description, URL, reason} = postData as {description: string; URL: string; reason: string};
|
|
1231
1246
|
// Get the response body.
|
|
1232
|
-
const responseBody = await apiRespond.
|
|
1233
|
-
await recordPageMetric('apiOperations', '
|
|
1247
|
+
const responseBody = await apiRespond.requestNewTest([description, URL, reason]);
|
|
1248
|
+
await recordPageMetric('apiOperations', 'requestNewTest');
|
|
1234
1249
|
// Send it.
|
|
1235
1250
|
setHeaders('application/json', null, 'ultra');
|
|
1236
1251
|
response.end(JSON.stringify(responseBody));
|
|
1237
1252
|
}
|
|
1253
|
+
// Otherwise, if the request uses the old, pre-rename requestTest path name: redirect
|
|
1254
|
+
// permanently to the new path name. requestTest takes no path parameters (its
|
|
1255
|
+
// arguments are body fields), so there is no further path to preserve, unlike the
|
|
1256
|
+
// renamedPagePrefixes GET-page redirects above. 308 (not 301) is used here because
|
|
1257
|
+
// 301 is defined to let clients rewrite a POST redirect as a GET, which would drop
|
|
1258
|
+
// this request's body; 308 preserves both the method and the body.
|
|
1259
|
+
// TEMPORARY: retire this redirect after 2027-03-28 once callers have updated.
|
|
1260
|
+
else if (segments[0] === 'requestTest') {
|
|
1261
|
+
response.writeHead(308, {Location: '/api/requestNewTest'});
|
|
1262
|
+
response.end();
|
|
1263
|
+
}
|
|
1238
1264
|
// Otherwise, if the service is to receive a retest request:
|
|
1239
1265
|
else if (segments[0] === 'requestRetest') {
|
|
1240
1266
|
const {reason} = postData as {reason: string};
|
|
@@ -1255,6 +1281,44 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
1255
1281
|
setHeaders('application/json', null, 'ultra');
|
|
1256
1282
|
response.end(JSON.stringify(responseBody));
|
|
1257
1283
|
}
|
|
1284
|
+
// Otherwise, if the service is to order an immediate, automatically approved test:
|
|
1285
|
+
else if (segments[0] === 'orderNewTest') {
|
|
1286
|
+
const {description, URL, reason} = postData as {description: string; URL: string; reason: string};
|
|
1287
|
+
// Get the response body.
|
|
1288
|
+
const responseBody = await apiRespond.orderNewTest([description, URL, reason]);
|
|
1289
|
+
await recordPageMetric('apiOperations', 'orderNewTest');
|
|
1290
|
+
// Send it.
|
|
1291
|
+
setHeaders('application/json', null, 'ultra');
|
|
1292
|
+
response.end(JSON.stringify(responseBody));
|
|
1293
|
+
}
|
|
1294
|
+
// Otherwise, if the request uses the old, pre-rename orderTest path name: redirect
|
|
1295
|
+
// permanently to the new path name. orderTest likewise takes no path parameters.
|
|
1296
|
+
// See the requestTest redirect above for why 308 (not 301) is used for these
|
|
1297
|
+
// POST-only API redirects.
|
|
1298
|
+
// TEMPORARY: retire this redirect after 2027-03-28 once callers have updated.
|
|
1299
|
+
else if (segments[0] === 'orderTest') {
|
|
1300
|
+
response.writeHead(308, {Location: '/api/orderNewTest'});
|
|
1301
|
+
response.end();
|
|
1302
|
+
}
|
|
1303
|
+
// Otherwise, if the service is to order an immediate, automatically approved retest:
|
|
1304
|
+
else if (segments[0] === 'orderRetest') {
|
|
1305
|
+
const {reason} = postData as {reason: string};
|
|
1306
|
+
// Get the response body.
|
|
1307
|
+
const responseBody = await apiRespond.orderRetest(segments.slice(1).concat(reason));
|
|
1308
|
+
await recordPageMetric('apiOperations', 'orderRetest');
|
|
1309
|
+
// Send it.
|
|
1310
|
+
setHeaders('application/json', null, 'ultra');
|
|
1311
|
+
response.end(JSON.stringify(responseBody));
|
|
1312
|
+
}
|
|
1313
|
+
// Otherwise, if the service is to wait for an ordered job to complete:
|
|
1314
|
+
else if (segments[0] === 'awaitTest') {
|
|
1315
|
+
// Get the response body.
|
|
1316
|
+
const responseBody = await apiRespond.awaitTest(segments.slice(1));
|
|
1317
|
+
await recordPageMetric('apiOperations', 'awaitTest');
|
|
1318
|
+
// Send it.
|
|
1319
|
+
setHeaders('application/json', null, 'ultra');
|
|
1320
|
+
response.end(JSON.stringify(responseBody));
|
|
1321
|
+
}
|
|
1258
1322
|
// Otherwise, i.e. if the service is invalid:
|
|
1259
1323
|
else {
|
|
1260
1324
|
await serveError(
|
package/lhm.plugin.json
CHANGED
package/linkValidation.test.ts
CHANGED
|
@@ -48,8 +48,8 @@ const htmlRoutes = [
|
|
|
48
48
|
'/listViolators.html',
|
|
49
49
|
'/listRules.html',
|
|
50
50
|
'/manage.html',
|
|
51
|
-
'/
|
|
52
|
-
'/
|
|
51
|
+
'/requestNewTest.html',
|
|
52
|
+
'/requestNewTestForm.html',
|
|
53
53
|
'/requestRetest.html',
|
|
54
54
|
'/requestRetestForm.html',
|
|
55
55
|
'/reannotate.html',
|
package/llms-full.txt
CHANGED
|
@@ -63,7 +63,7 @@ Kilotest offers a comprehensive suite of capabilities to users via its web UI:
|
|
|
63
63
|
- Get statistics about issues reported in one report: `https://kilotest.com/listIssues.html/{timeStamp}/{jobID}`
|
|
64
64
|
- Get details about one issue reported in one report: `https://kilotest.com/listViolators.html/{issueID}/{timeStamp}/{jobID}`
|
|
65
65
|
- Get diagnoses by rule engines of rule violations for one HTML element exhibiting one issue in one report: `https://kilotest.com/listDiagnoses.html/{issueID}/{timeStamp}/{jobID}/{catalogIndex}`
|
|
66
|
-
- [Recommend the testing of a not-yet-tested page](https://kilotest.com/
|
|
66
|
+
- [Recommend the testing of a not-yet-tested page](https://kilotest.com/requestNewTestForm.html)
|
|
67
67
|
- Recommend the retesting of a previously tested page: `https://kilotest.com/requestRetestForm.html/{timeStamp}/{jobID}`
|
|
68
68
|
- [Get statistics about frequently reported issues across all pages](https://kilotest.com/listTopIssues.html)
|
|
69
69
|
- [Get a list of the operations that Kilotest managers are authorized to request](https://kilotest.com/manage.html)
|
|
@@ -108,10 +108,10 @@ Kilotest offers these tools:
|
|
|
108
108
|
- `timeStamp`: initial segment of report identifier
|
|
109
109
|
- `jobID`: final segment of report identifier
|
|
110
110
|
- source of parameters: response to a `listViolators` request
|
|
111
|
-
- `
|
|
111
|
+
- `requestNewTest`
|
|
112
112
|
- method: `POST`
|
|
113
113
|
- purpose: request testing of one web page about which no report is available
|
|
114
|
-
- path: `/api/
|
|
114
|
+
- path: `/api/requestNewTest`
|
|
115
115
|
- payload properties
|
|
116
116
|
- `description`: description of the page to be tested
|
|
117
117
|
- `URL`: URL of the page to be tested
|
|
@@ -132,6 +132,35 @@ Kilotest offers these tools:
|
|
|
132
132
|
- path: `/api/requestFeature`
|
|
133
133
|
- payload properties
|
|
134
134
|
- `feature`: description and motivation of the feature improvement or the new feature
|
|
135
|
+
- `orderNewTest`
|
|
136
|
+
- method: `POST`
|
|
137
|
+
- purpose: order immediate, automatically approved testing of one web page about which no report is available, instead of waiting for manual approval
|
|
138
|
+
- path: `/api/orderNewTest`
|
|
139
|
+
- payload properties
|
|
140
|
+
- `description`: description of the page to be tested
|
|
141
|
+
- `URL`: URL of the page to be tested
|
|
142
|
+
- `reason`: reason for testing the page
|
|
143
|
+
- response includes a report identifier (`timeStamp` and `jobID`) once the job is enqueued
|
|
144
|
+
- `orderRetest`
|
|
145
|
+
- method: `POST`
|
|
146
|
+
- purpose: order immediate, automatically approved retesting of one web page about which a report is available, instead of waiting for manual approval
|
|
147
|
+
- path: `/api/orderRetest/{timeStamp}/{jobID}`
|
|
148
|
+
- parameters
|
|
149
|
+
- `timeStamp`: initial segment of report identifier
|
|
150
|
+
- `jobID`: final segment of report identifier
|
|
151
|
+
- source of parameters: response to a `listIssues` request
|
|
152
|
+
- payload properties
|
|
153
|
+
- `reason`: reason for retesting the page
|
|
154
|
+
- response includes a report identifier (`timeStamp` and `jobID`) once the job is enqueued
|
|
155
|
+
- `awaitTest`
|
|
156
|
+
- method: `POST`
|
|
157
|
+
- purpose: wait for a job ordered via `orderNewTest` or `orderRetest` to complete; blocks until the report is ready, the job fails, or a maximum wait time elapses, then returns
|
|
158
|
+
- path: `/api/awaitTest/{timeStamp}/{jobID}`
|
|
159
|
+
- parameters
|
|
160
|
+
- `timeStamp`: initial segment of the report identifier returned by `orderNewTest` or `orderRetest`
|
|
161
|
+
- `jobID`: final segment of the report identifier returned by `orderNewTest` or `orderRetest`
|
|
162
|
+
- source of parameters: response to an `orderNewTest` or `orderRetest` request
|
|
163
|
+
- caveat: testing typically takes about 2 minutes and occasionally up to 4 minutes; sends no interim notices while waiting
|
|
135
164
|
|
|
136
165
|
## After a test, retest, or feature request
|
|
137
166
|
|
|
@@ -147,6 +176,8 @@ When you request a test or a retest:
|
|
|
147
176
|
- check test completion by calling `listReports` and looking for the page and completion date in the list
|
|
148
177
|
- you advise the user to check in approximately 1 day for the test results on the Kilotest website, starting at `https://kilotest.com/listReports.html`
|
|
149
178
|
|
|
179
|
+
For immediate testing without waiting for manual approval, use `orderNewTest` or `orderRetest` instead of `requestNewTest` or `requestRetest`. Each enqueues the job right away and returns a report identifier along with a question about whether to wait for completion. To wait, call `awaitTest` with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call `listIssues` with the same identifier.
|
|
180
|
+
|
|
150
181
|
An [OpenAPI specification for Kilotest](https://kilotest.com/openapi.yaml) is available.
|
|
151
182
|
|
|
152
183
|
## How to report results honestly to users
|
package/llms.txt
CHANGED
|
@@ -9,9 +9,12 @@ Kilotest helps language models and AI platforms to give thorough, accurate and t
|
|
|
9
9
|
- List all elements exhibiting one issue in one report: `/api/listViolators/{issueID}/{timeStamp}/{jobID}` (`GET`)
|
|
10
10
|
- List all diagnoses of one element exhibiting one issue in one report: `/api/listDiagnoses/{catalogIndex}/{issueID}/{timeStamp}/{jobID}` (`GET`)
|
|
11
11
|
- Get one full report in JSON: `/api/getReport/{timeStamp}/{jobID}` (`GET`)
|
|
12
|
-
- Request testing of a web page: `/api/
|
|
12
|
+
- Request testing of a web page: `/api/requestNewTest` (`POST`)
|
|
13
13
|
- Request retesting of a web page: `/api/requestRetest/{timeStamp}/{jobID}` (`POST`)
|
|
14
14
|
- Request adding or improving a feature: `/api/requestFeature` (`POST`)
|
|
15
|
+
- Order immediate, automatically approved testing of a web page: `/api/orderNewTest` (`POST`)
|
|
16
|
+
- Order immediate, automatically approved retesting of a web page: `/api/orderRetest/{timeStamp}/{jobID}` (`POST`)
|
|
17
|
+
- Wait for a job ordered via orderNewTest or orderRetest to complete: `/api/awaitTest/{timeStamp}/{jobID}` (`POST`)
|
|
15
18
|
|
|
16
19
|
## MCP Server
|
|
17
20
|
|
|
@@ -24,7 +27,7 @@ Kilotest helps language models and AI platforms to give thorough, accurate and t
|
|
|
24
27
|
- List all elements exhibiting one issue in one report: `https://kilotest.com/listViolators.html/{issueID}/{timeStamp}/{jobID}`
|
|
25
28
|
- List all diagnoses of one element exhibiting one issue in one report: `https://kilotest.com/listDiagnoses.html/{issueID}/{timeStamp}/{jobID}/{catalogIndex}`
|
|
26
29
|
- Get one full report in JSON: `https://kilotest.com/fullReport.json/{timeStamp}/{jobID}`
|
|
27
|
-
- [Request testing of a web page](https://kilotest.com/
|
|
30
|
+
- [Request testing of a web page](https://kilotest.com/requestNewTestForm.html)
|
|
28
31
|
- Request retesting of a web page: `https://kilotest.com/requestRetestForm.html/{timeStamp}/{jobID}`
|
|
29
32
|
- [List the operations that Kilotest managers are authorized to request](https://kilotest.com/manage.html)
|
|
30
33
|
- [Provide statistics about frequently reported issues across all pages](https://kilotest.com/listTopIssues.html)
|