@jrpool/kilotest 60.9.1 → 61.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/api/listPages.test.ts +17 -16
- package/api/listPages.ts +13 -35
- package/api/schemas.ts +8 -5
- package/api/util.ts +10 -8
- package/index.test.ts +40 -10
- package/index.ts +43 -32
- package/lhm.plugin.json +35 -66
- package/llms-full.txt +2 -2
- package/openapi-3.0.yaml +29 -49
- package/openapi.yaml +28 -49
- package/package.json +1 -1
- package/robots.txt +3 -0
- package/server.json +1 -1
package/api/listPages.test.ts
CHANGED
|
@@ -31,7 +31,7 @@ after(() => {
|
|
|
31
31
|
test('listPages returns one entry per tested page, sorted by page description', async () => {
|
|
32
32
|
const body = await response();
|
|
33
33
|
const pagesBasics = body['response content']['basics about all tested pages'];
|
|
34
|
-
const descriptions = pagesBasics.map(b => b
|
|
34
|
+
const descriptions = pagesBasics.map(b => b.description);
|
|
35
35
|
assert.deepEqual(
|
|
36
36
|
descriptions,
|
|
37
37
|
[
|
|
@@ -49,31 +49,32 @@ test('listPages returns one entry per tested page, sorted by page description',
|
|
|
49
49
|
test('listPages does not include the page of the hidden report', async () => {
|
|
50
50
|
const body = await response();
|
|
51
51
|
const pagesBasics = body['response content']['basics about all tested pages'];
|
|
52
|
-
const ids = pagesBasics.map(b => b
|
|
52
|
+
const ids = pagesBasics.map(b => b.identifier);
|
|
53
53
|
assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listPages');
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
test('listPages
|
|
56
|
+
test('listPages identifies a multi-report page by its latest report', async () => {
|
|
57
57
|
const body = await response();
|
|
58
58
|
const pagesBasics = body['response content']['basics about all tested pages'];
|
|
59
|
-
const mixed = pagesBasics.find(b => b
|
|
60
|
-
assert.equal(mixed
|
|
61
|
-
|
|
62
|
-
assert.equal(latest.identifier, '260202T0000-new');
|
|
63
|
-
assert.equal(latest['completion date and time'], '2026-02-02T00:10:00.000Z');
|
|
64
|
-
assert.equal(typeof latest['days since the report was completed'], 'number');
|
|
65
|
-
assert.equal(mixed['how to get details about the latest report'].method, 'GET');
|
|
66
|
-
assert.ok(mixed['how to get details about the latest report'].URL.endsWith('/api/listIssues/260202T0000-new'));
|
|
67
|
-
assert.ok(mixed['web users can get details about the latest report at'].endsWith('/listIssues.html/260202T0000-new'));
|
|
59
|
+
const mixed = pagesBasics.find(b => b.description === 'Mixed Outcomes')!;
|
|
60
|
+
assert.equal(mixed.identifier, '260202T0000-new');
|
|
61
|
+
assert.equal(mixed.URL, 'https://example.com/mixed');
|
|
68
62
|
});
|
|
69
63
|
|
|
70
64
|
test('listPages describes a single-report page', async () => {
|
|
71
65
|
const body = await response();
|
|
72
66
|
const pagesBasics = body['response content']['basics about all tested pages'];
|
|
73
|
-
const canttell = pagesBasics.find(b => b
|
|
74
|
-
assert.equal(canttell
|
|
75
|
-
assert.equal(canttell
|
|
76
|
-
|
|
67
|
+
const canttell = pagesBasics.find(b => b.description === 'All CantTell Page')!;
|
|
68
|
+
assert.equal(canttell.URL, 'https://example.com/canttell');
|
|
69
|
+
assert.equal(canttell.identifier, '260101T0001-ct');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('listPages includes result-retrieval instructions with an identifier placeholder', async () => {
|
|
73
|
+
const body = await response();
|
|
74
|
+
const content = body['response content'];
|
|
75
|
+
assert.equal(content['how to get test results about a page'].method, 'GET');
|
|
76
|
+
assert.ok(content['how to get test results about a page'].URL.endsWith('/api/listIssues/{identifier}'));
|
|
77
|
+
assert.ok(content['web users can get test results about a page at'].endsWith('/listIssues.html/{identifier}'));
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
test('listPages includes request-test instructions', async () => {
|
package/api/listPages.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
9
|
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
-
import {
|
|
10
|
+
import {getReportExtracts} from '../util.ts';
|
|
11
11
|
import {listPagesResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -20,42 +20,20 @@ type ResponseContent = z.infer<typeof listPagesResponseSchema>['response content
|
|
|
20
20
|
// Returns the response body.
|
|
21
21
|
export const response = async () => {
|
|
22
22
|
const thisHost = getThisHost();
|
|
23
|
-
// Get extracts of all
|
|
24
|
-
const reportExtracts = await getReportExtracts();
|
|
25
|
-
// Get the
|
|
26
|
-
|
|
27
|
-
reportExtracts.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// Initialize an array of basics about the pages.
|
|
31
|
-
const pagesBasics: ResponseContent['basics about all tested pages'] = [];
|
|
32
|
-
// For each report:
|
|
33
|
-
reportExtracts.forEach(extract => {
|
|
34
|
-
const {description, id, reportTime, superseded, url} = extract;
|
|
35
|
-
// If it is the latest report about its page:
|
|
36
|
-
if (!superseded) {
|
|
37
|
-
// Add basics about the page, with instructions for getting details, to the array.
|
|
38
|
-
pagesBasics.push({
|
|
39
|
-
'tested web page': {
|
|
40
|
-
description,
|
|
41
|
-
URL: url
|
|
42
|
-
},
|
|
43
|
-
'number of reports about the page': reportCounts.get(description)!,
|
|
44
|
-
'basics about the latest report': {
|
|
45
|
-
identifier: id,
|
|
46
|
-
'completion date and time': reportTime,
|
|
47
|
-
'days since the report was completed': getAgoDays(new Date(reportTime))
|
|
48
|
-
},
|
|
49
|
-
'how to get details about the latest report': {
|
|
50
|
-
method: 'GET',
|
|
51
|
-
URL: `${thisHost}/api/listIssues/${id}`
|
|
52
|
-
},
|
|
53
|
-
'web users can get details about the latest report at': `${thisHost}/listIssues.html/${id}`
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
});
|
|
23
|
+
// Get extracts of the latest reports about all tested pages, sorted by page description.
|
|
24
|
+
const reportExtracts = await getReportExtracts(true);
|
|
25
|
+
// Get the basics about each tested page: its description, URL, and the identifier of
|
|
26
|
+
// its latest report, which the result-retrieval instructions below use as a placeholder.
|
|
27
|
+
const pagesBasics: ResponseContent['basics about all tested pages'] = reportExtracts.map(
|
|
28
|
+
({description, id, url}) => ({description, URL: url, identifier: id})
|
|
29
|
+
);
|
|
57
30
|
// Create the response content.
|
|
58
31
|
const responseContent: ResponseContent = {
|
|
32
|
+
'how to get test results about a page': {
|
|
33
|
+
method: 'GET',
|
|
34
|
+
URL: `${thisHost}/api/listIssues/{identifier}`
|
|
35
|
+
},
|
|
36
|
+
'web users can get test results about a page at': `${thisHost}/listIssues.html/{identifier}`,
|
|
59
37
|
'basics about all tested pages': pagesBasics,
|
|
60
38
|
'how to request that a page with no report be tested': {
|
|
61
39
|
method: 'POST',
|
package/api/schemas.ts
CHANGED
|
@@ -195,12 +195,15 @@ const reportTimeSchema = z.object({
|
|
|
195
195
|
});
|
|
196
196
|
|
|
197
197
|
export const listPagesResponseSchema = envelope('GET', z.object({
|
|
198
|
+
'how to get test results about a page': z.object({
|
|
199
|
+
method: z.literal('GET'),
|
|
200
|
+
URL: z.string().describe('URL template; replace {identifier} with the identifier of the latest report about the page.')
|
|
201
|
+
}),
|
|
202
|
+
'web users can get test results about a page at': z.string().describe('URL template; replace {identifier} with the identifier of the latest report about the page.'),
|
|
198
203
|
'basics about all tested pages': z.array(z.object({
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
'
|
|
202
|
-
'how to get details about the latest report': z.object({method: z.literal('GET'), URL: z.string()}),
|
|
203
|
-
'web users can get details about the latest report at': z.string()
|
|
204
|
+
description: z.string(),
|
|
205
|
+
URL: z.string(),
|
|
206
|
+
identifier: z.string().describe('Identifier of the latest report about the page (example: 260503T0432-x9z).')
|
|
204
207
|
})),
|
|
205
208
|
'how to request that a page with no report be tested': z.object({
|
|
206
209
|
method: z.literal('POST'),
|
package/api/util.ts
CHANGED
|
@@ -51,18 +51,20 @@ export const getResponseMetadata = () => ({
|
|
|
51
51
|
export const getToolsFacts = () => ({
|
|
52
52
|
'name': 'Kilotest',
|
|
53
53
|
'description': {
|
|
54
|
-
'what Kilotest does': 'Kilotest tools generate
|
|
54
|
+
'what Kilotest does': 'Kilotest tools generate findings about the front-end quality (i.e. accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG of W3C) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues and then saves the report for public retrieval. Kilotest makes facts about the tested pages, the issues, and the violations retrievable at four levels of granularity.',
|
|
55
55
|
'how to retrieve findings': {
|
|
56
|
-
'level 1': 'Use the listPages tool to get a list of tested pages, each with
|
|
57
|
-
'level 2': 'Use the listIssues tool to get a list of issues in one report.',
|
|
58
|
-
'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue.',
|
|
56
|
+
'level 1': 'Use the listPages tool to get a list of tested pages, each with the identifier of its latest report, for use with the listIssues tool.',
|
|
57
|
+
'level 2': 'Use the listIssues tool to get a list of issues in one report, each with a link to the level-3 results for that issue. The response also includes a history of reports about the page, each with a link to its own level-2 results.',
|
|
58
|
+
'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue, each with a link to the level-4 results for that violator.',
|
|
59
59
|
'level 4': 'Use the listDiagnoses tool to get a list of diagnoses of how one element on one page exhibited one issue in one report.',
|
|
60
60
|
},
|
|
61
61
|
'how to generate more findings': {
|
|
62
|
-
'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.',
|
|
63
|
-
'retesting': 'If
|
|
64
|
-
'
|
|
65
|
-
'
|
|
62
|
+
'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested, or the orderNewTest tool for immediate testing.',
|
|
63
|
+
'retesting': 'If you believe that the latest report about a page is obsolete (e.g., because the page has been revised), use the requestRetest tool to request that the page be retested, or the orderRetest tool for immediate retesting.',
|
|
64
|
+
'approval': 'Requests are manager-reviewed, usually in 1 hour to one day. Orders are automatically reviewed. Reviews result in approval or rejection.',
|
|
65
|
+
'latency': 'An approved test typically finishes in 2 to 4 minutes.',
|
|
66
|
+
'request confirmation': 'After submitting a request and waiting for manager review, you can use the listPages tool to determine whether the request has been fulfilled: a new-test request adds a page entry linking to its results, and a retest request adds a later report about the page.',
|
|
67
|
+
'order confirmation': 'After ordering a test or retest, you will receive a rejection or a report identifier, which you can use to call the awaitTest tool. That tool blocks until the test completes, fails, or a maximum wait time elapses, and then returns the outcome. If you do not call awaitTest, you can wait 2 to 4 minutes and then, if the test is complete, the listIssues tool called with the report identifier will succeed.'
|
|
66
68
|
}
|
|
67
69
|
},
|
|
68
70
|
'URL': `${getThisHost()}/mcp`,
|
package/index.test.ts
CHANGED
|
@@ -981,12 +981,12 @@ test('GET /qai/comments redirects to /tutorialAI.html', async () => {
|
|
|
981
981
|
assert.equal(res.headers['location'], '/tutorialAI.html');
|
|
982
982
|
});
|
|
983
983
|
|
|
984
|
-
test('GET /diagnoses.html/<
|
|
984
|
+
test('GET /diagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> with a query string redirects in one step to the canonical /listDiagnoses.html URL', async () => {
|
|
985
985
|
const res = await request(
|
|
986
|
-
'GET', '/diagnoses.html/
|
|
986
|
+
'GET', '/diagnoses.html/linkNoText/260101T0000/mix/0?pathID=/html/body', {}, {followRedirects: false}
|
|
987
987
|
);
|
|
988
988
|
assert.equal(res.statusCode, 301);
|
|
989
|
-
assert.equal(res.headers['location'], '/listDiagnoses.html/
|
|
989
|
+
assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
|
|
990
990
|
});
|
|
991
991
|
|
|
992
992
|
test('GET /reportIssues.html/<reportID> redirects to the equivalent /listIssues.html/<reportID>', async () => {
|
|
@@ -1015,12 +1015,12 @@ test('GET /listViolators.html/<issueID>/<timeStamp>/<jobID> redirects to the sin
|
|
|
1015
1015
|
assert.equal(res.headers['location'], '/listViolators.html/linkNoText/260101T0000-mix');
|
|
1016
1016
|
});
|
|
1017
1017
|
|
|
1018
|
-
test('GET /listDiagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> redirects to the single-reportID URL
|
|
1018
|
+
test('GET /listDiagnoses.html/<issueID>/<timeStamp>/<jobID>/<catalogIndex> with a query string redirects in one step to the canonical single-reportID URL', async () => {
|
|
1019
1019
|
const res = await request(
|
|
1020
1020
|
'GET', '/listDiagnoses.html/linkNoText/260101T0000/mix/0?pathID=/html/body', {}, {followRedirects: false}
|
|
1021
1021
|
);
|
|
1022
1022
|
assert.equal(res.statusCode, 301);
|
|
1023
|
-
assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0
|
|
1023
|
+
assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
|
|
1024
1024
|
});
|
|
1025
1025
|
|
|
1026
1026
|
test('GET /requestRetestForm.html/<timeStamp>/<jobID> redirects to the single-reportID URL', async () => {
|
|
@@ -1077,6 +1077,33 @@ test('GET /api/listReports redirects to /api/listPages', async () => {
|
|
|
1077
1077
|
assert.equal(res.headers['location'], '/api/listPages');
|
|
1078
1078
|
});
|
|
1079
1079
|
|
|
1080
|
+
// TESTS: query-string canonicalization
|
|
1081
|
+
// No GET page handler reads the query string, so a request with a query returns a
|
|
1082
|
+
// duplicate of the queryless page. These verify that such requests permanently
|
|
1083
|
+
// redirect to the queryless URL.
|
|
1084
|
+
|
|
1085
|
+
test('GET /listDiagnoses.html/<segments> with a query string redirects to the queryless URL', async () => {
|
|
1086
|
+
const res = await request(
|
|
1087
|
+
'GET', '/listDiagnoses.html/linkNoText/260101T0000-mix/0?pathID=/html/body', {}, {followRedirects: false}
|
|
1088
|
+
);
|
|
1089
|
+
assert.equal(res.statusCode, 301);
|
|
1090
|
+
assert.equal(res.headers['location'], '/listDiagnoses.html/linkNoText/260101T0000-mix/0');
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
test('GET /listIssues.html/<reportID> with a query string redirects to the queryless URL', async () => {
|
|
1094
|
+
const res = await request(
|
|
1095
|
+
'GET', '/listIssues.html/260101T0000-mix?foo=bar', {}, {followRedirects: false}
|
|
1096
|
+
);
|
|
1097
|
+
assert.equal(res.statusCode, 301);
|
|
1098
|
+
assert.equal(res.headers['location'], '/listIssues.html/260101T0000-mix');
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test('GET / with a query string redirects to /', async () => {
|
|
1102
|
+
const res = await request('GET', '/?utm_source=example', {}, {followRedirects: false});
|
|
1103
|
+
assert.equal(res.statusCode, 301);
|
|
1104
|
+
assert.equal(res.headers['location'], '/');
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1080
1107
|
test('GET /requestTest.html redirects to /requestNewTest.html', async () => {
|
|
1081
1108
|
const res = await request('GET', '/requestTest.html', {}, {followRedirects: false});
|
|
1082
1109
|
assert.equal(res.statusCode, 301);
|
|
@@ -1647,12 +1674,15 @@ test('POST /metrics.html with a valid authCode sets a metrics-exclusion cookie',
|
|
|
1647
1674
|
assert.ok(setCookie.includes('HttpOnly'));
|
|
1648
1675
|
});
|
|
1649
1676
|
|
|
1650
|
-
test('GET /metrics.html with a valid authCode in the query string does not set a cookie or show the counts', async () => {
|
|
1677
|
+
test('GET /metrics.html with a valid authCode in the query string redirects to the queryless URL, which does not set a cookie or show the counts', async () => {
|
|
1651
1678
|
const res = await request('GET', '/metrics.html?authCode=test-auth-code');
|
|
1652
|
-
assert.equal(res.statusCode,
|
|
1653
|
-
assert.equal(res.headers['
|
|
1654
|
-
|
|
1655
|
-
assert.
|
|
1679
|
+
assert.equal(res.statusCode, 301);
|
|
1680
|
+
assert.equal(res.headers['location'], '/metrics.html');
|
|
1681
|
+
const redirected = await request('GET', res.headers['location']);
|
|
1682
|
+
assert.equal(redirected.statusCode, 200);
|
|
1683
|
+
assert.equal(redirected.headers['set-cookie'], undefined);
|
|
1684
|
+
assert.ok(redirected.body.includes('Authorization code'));
|
|
1685
|
+
assert.ok(!redirected.body.includes('Web page views'));
|
|
1656
1686
|
});
|
|
1657
1687
|
|
|
1658
1688
|
test('a request carrying the metrics-exclusion cookie does not record a pageViews or apiOperations metric', async () => {
|
package/index.ts
CHANGED
|
@@ -347,9 +347,10 @@ export const isPathAllowed = (method: string, pathname: string): boolean => {
|
|
|
347
347
|
const patterns = (routes as Record<string, string[]>)[method] || [];
|
|
348
348
|
return patterns.some(pattern => matchPath(pattern, pathname));
|
|
349
349
|
};
|
|
350
|
-
// Returns the single-reportID
|
|
351
|
-
//
|
|
352
|
-
//
|
|
350
|
+
// Returns the single-reportID path that a legacy two-segment report path tail (one
|
|
351
|
+
// identifying a report with separate timeStamp and jobID segments) canonicalizes to
|
|
352
|
+
// for a page under its current name, or null if the page has no legacy form or the
|
|
353
|
+
// tail lacks its shape.
|
|
353
354
|
const getLegacyReportURL = (topic: string, pathTail: string): string | null => {
|
|
354
355
|
const legacy = legacyReportTails[topic];
|
|
355
356
|
if (!legacy) {
|
|
@@ -681,12 +682,21 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
681
682
|
}
|
|
682
683
|
// Otherwise, if it is for the home page:
|
|
683
684
|
else if (['/', '/index.html'].includes(pathname)) {
|
|
684
|
-
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
685
|
+
// If the URL has a query string, redirect the request permanently to the
|
|
686
|
+
// queryless URL (see the same redirect for .html pages below).
|
|
687
|
+
if (search) {
|
|
688
|
+
response.writeHead(301, {Location: pathname});
|
|
689
|
+
response.end();
|
|
690
|
+
}
|
|
691
|
+
// Otherwise, i.e. if the URL has no query string:
|
|
692
|
+
else {
|
|
693
|
+
await recordPageMetric('pageViews', 'index');
|
|
694
|
+
// Get the home page.
|
|
695
|
+
const {answerPage} = await home();
|
|
696
|
+
// Serve it.
|
|
697
|
+
setHeaders('text/html', '/index.html', 'medium');
|
|
698
|
+
response.end(answerPage);
|
|
699
|
+
}
|
|
690
700
|
}
|
|
691
701
|
// Otherwise, if it is for the crawler specification:
|
|
692
702
|
else if (pageName === 'robots.txt') {
|
|
@@ -766,18 +776,15 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
766
776
|
// If this failed:
|
|
767
777
|
if (isReportError(report)) {
|
|
768
778
|
// This is not necessarily abuse: the link may have been generated (e.g. by
|
|
769
|
-
// listIssues.html) before the report was pruned or rewound, or the
|
|
770
|
-
// may have
|
|
771
|
-
// So tell the requester only that the request was
|
|
772
|
-
//
|
|
773
|
-
//
|
|
779
|
+
// listIssues.html) before the report was pruned or rewound, or the identifier
|
|
780
|
+
// may have been scraped from the API documentation's examples, none of which
|
|
781
|
+
// is the requester's fault. So tell the requester only that the request was
|
|
782
|
+
// invalid, without accusing them. No manager alert is sent: a missing report
|
|
783
|
+
// is a routine miss, and a report that exists but is unreadable or unusable
|
|
784
|
+
// has already been quarantined and alerted on by getReport.
|
|
774
785
|
console.error(
|
|
775
786
|
`Full report ${pathTail} requested but unavailable (${report.error})`
|
|
776
787
|
);
|
|
777
|
-
await sendAlert(
|
|
778
|
-
'Kilotest: requested full report unavailable',
|
|
779
|
-
`Full report ${pathTail} was requested but could not be retrieved: ${report.error}`
|
|
780
|
-
);
|
|
781
788
|
await serveError({message: 'ERROR: Invalid request'}, response, true);
|
|
782
789
|
}
|
|
783
790
|
// Otherwise, i.e. if it succeeded:
|
|
@@ -800,20 +807,24 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
|
|
|
800
807
|
// Otherwise, if it is for an HTML page other than the home page:
|
|
801
808
|
else if (pageName.endsWith('.html')) {
|
|
802
809
|
const topic = pageName.slice(0, -5);
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
810
|
+
// Compute the path in the current request signature: the current name if the
|
|
811
|
+
// page name is a pre-rename one, and the single-reportID tail if the tail
|
|
812
|
+
// identifies a report with the legacy two-segment form (a timeStamp segment
|
|
813
|
+
// and a jobID segment).
|
|
814
|
+
const canonicalTopic = renamedPagePrefixes[topic] ?? topic;
|
|
815
|
+
const canonicalPath = getLegacyReportURL(canonicalTopic, pathTail) ??
|
|
816
|
+
`/${canonicalTopic}.html${pathTail ? `/${pathTail}` : ''}`;
|
|
817
|
+
// If the request signature contains any obsolete element (a pre-rename page
|
|
818
|
+
// name, a legacy two-segment report tail, or a query string, which no GET
|
|
819
|
+
// page handler reads, since the search argument passed to answer() is
|
|
820
|
+
// consumed only for POST requests, where it is reconstructed from the
|
|
821
|
+
// request body), convert all of them at once by redirecting the request
|
|
822
|
+
// permanently to the canonical path with no query string. A request with a
|
|
823
|
+
// query otherwise returns a byte-identical duplicate of the queryless page,
|
|
824
|
+
// which search engines report as a duplicate without a user-selected
|
|
825
|
+
// canonical (e.g. the pathID queries still requested on listDiagnoses URLs).
|
|
826
|
+
if (canonicalPath !== pathname || search) {
|
|
827
|
+
response.writeHead(301, {Location: canonicalPath});
|
|
817
828
|
response.end();
|
|
818
829
|
}
|
|
819
830
|
// Otherwise, if the page can be generated and is not POST-only (a POST-only path also matches
|
package/lhm.plugin.json
CHANGED
|
@@ -96,81 +96,48 @@
|
|
|
96
96
|
"response content": {
|
|
97
97
|
"type": "object",
|
|
98
98
|
"properties": {
|
|
99
|
+
"how to get test results about a page": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"method": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"const": "GET"
|
|
105
|
+
},
|
|
106
|
+
"URL": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "URL template; replace {identifier} with the identifier of the latest report about the page."
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"required": [
|
|
112
|
+
"method",
|
|
113
|
+
"URL"
|
|
114
|
+
],
|
|
115
|
+
"additionalProperties": false
|
|
116
|
+
},
|
|
117
|
+
"web users can get test results about a page at": {
|
|
118
|
+
"type": "string",
|
|
119
|
+
"description": "URL template; replace {identifier} with the identifier of the latest report about the page."
|
|
120
|
+
},
|
|
99
121
|
"basics about all tested pages": {
|
|
100
122
|
"type": "array",
|
|
101
123
|
"items": {
|
|
102
124
|
"type": "object",
|
|
103
125
|
"properties": {
|
|
104
|
-
"
|
|
105
|
-
"type": "
|
|
106
|
-
"properties": {
|
|
107
|
-
"description": {
|
|
108
|
-
"type": "string"
|
|
109
|
-
},
|
|
110
|
-
"URL": {
|
|
111
|
-
"type": "string"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
"required": [
|
|
115
|
-
"description",
|
|
116
|
-
"URL"
|
|
117
|
-
],
|
|
118
|
-
"additionalProperties": false
|
|
119
|
-
},
|
|
120
|
-
"number of reports about the page": {
|
|
121
|
-
"type": "number"
|
|
122
|
-
},
|
|
123
|
-
"basics about the latest report": {
|
|
124
|
-
"type": "object",
|
|
125
|
-
"properties": {
|
|
126
|
-
"identifier": {
|
|
127
|
-
"type": "string",
|
|
128
|
-
"description": "Identifier of the report (example: 260503T0432-x9z)."
|
|
129
|
-
},
|
|
130
|
-
"completion date and time": {
|
|
131
|
-
"type": "string"
|
|
132
|
-
},
|
|
133
|
-
"days since the report was completed": {
|
|
134
|
-
"type": [
|
|
135
|
-
"number",
|
|
136
|
-
"null"
|
|
137
|
-
]
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
"required": [
|
|
141
|
-
"identifier",
|
|
142
|
-
"completion date and time",
|
|
143
|
-
"days since the report was completed"
|
|
144
|
-
],
|
|
145
|
-
"additionalProperties": false
|
|
146
|
-
},
|
|
147
|
-
"how to get details about the latest report": {
|
|
148
|
-
"type": "object",
|
|
149
|
-
"properties": {
|
|
150
|
-
"method": {
|
|
151
|
-
"type": "string",
|
|
152
|
-
"const": "GET"
|
|
153
|
-
},
|
|
154
|
-
"URL": {
|
|
155
|
-
"type": "string"
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
"required": [
|
|
159
|
-
"method",
|
|
160
|
-
"URL"
|
|
161
|
-
],
|
|
162
|
-
"additionalProperties": false
|
|
126
|
+
"description": {
|
|
127
|
+
"type": "string"
|
|
163
128
|
},
|
|
164
|
-
"
|
|
129
|
+
"URL": {
|
|
165
130
|
"type": "string"
|
|
131
|
+
},
|
|
132
|
+
"identifier": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "Identifier of the latest report about the page (example: 260503T0432-x9z)."
|
|
166
135
|
}
|
|
167
136
|
},
|
|
168
137
|
"required": [
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"how to get details about the latest report",
|
|
173
|
-
"web users can get details about the latest report at"
|
|
138
|
+
"description",
|
|
139
|
+
"URL",
|
|
140
|
+
"identifier"
|
|
174
141
|
],
|
|
175
142
|
"additionalProperties": false
|
|
176
143
|
}
|
|
@@ -231,6 +198,8 @@
|
|
|
231
198
|
}
|
|
232
199
|
},
|
|
233
200
|
"required": [
|
|
201
|
+
"how to get test results about a page",
|
|
202
|
+
"web users can get test results about a page at",
|
|
234
203
|
"basics about all tested pages",
|
|
235
204
|
"how to request that a page with no report be tested",
|
|
236
205
|
"how a web user can request that the page be tested"
|
|
@@ -4423,5 +4392,5 @@
|
|
|
4423
4392
|
}
|
|
4424
4393
|
}
|
|
4425
4394
|
],
|
|
4426
|
-
"version": "
|
|
4395
|
+
"version": "61.0.0"
|
|
4427
4396
|
}
|
package/llms-full.txt
CHANGED
|
@@ -74,7 +74,7 @@ Kilotest offers these tools:
|
|
|
74
74
|
|
|
75
75
|
- `listPages`
|
|
76
76
|
- method: `GET`
|
|
77
|
-
- purpose: list all tested pages, each with
|
|
77
|
+
- purpose: list all tested pages, each with the identifier of its latest report
|
|
78
78
|
- path: `/api/listPages`
|
|
79
79
|
- caveat: Cached results are likely to be stale and should not be used
|
|
80
80
|
- `listIssues`
|
|
@@ -163,7 +163,7 @@ When you request a test or a retest:
|
|
|
163
163
|
- approval followed by the requested testing
|
|
164
164
|
- rejection
|
|
165
165
|
- how to confirm approval
|
|
166
|
-
- check test completion by calling `listPages` and looking for the page
|
|
166
|
+
- check test completion by calling `listPages` and looking for the page in the list
|
|
167
167
|
- you advise the user to check in approximately 1 day for the test results on the Kilotest website, starting at `https://kilotest.com/listPages.html`
|
|
168
168
|
|
|
169
169
|
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.
|
package/openapi-3.0.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.0.3
|
|
|
2
2
|
info:
|
|
3
3
|
title: Kilotest API
|
|
4
4
|
description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
|
|
5
|
-
version:
|
|
5
|
+
version: 61.0.0
|
|
6
6
|
license:
|
|
7
7
|
name: MIT
|
|
8
8
|
url: https://github.com/jrpool/kilotest/blob/main/LICENSE
|
|
@@ -60,61 +60,39 @@ paths:
|
|
|
60
60
|
response content:
|
|
61
61
|
type: object
|
|
62
62
|
properties:
|
|
63
|
+
how to get test results about a page:
|
|
64
|
+
type: object
|
|
65
|
+
properties:
|
|
66
|
+
method:
|
|
67
|
+
type: string
|
|
68
|
+
enum:
|
|
69
|
+
- GET
|
|
70
|
+
URL:
|
|
71
|
+
type: string
|
|
72
|
+
description: URL template; replace {identifier} with the identifier of the latest report about the page.
|
|
73
|
+
required:
|
|
74
|
+
- method
|
|
75
|
+
- URL
|
|
76
|
+
additionalProperties: false
|
|
77
|
+
web users can get test results about a page at:
|
|
78
|
+
type: string
|
|
79
|
+
description: URL template; replace {identifier} with the identifier of the latest report about the page.
|
|
63
80
|
basics about all tested pages:
|
|
64
81
|
type: array
|
|
65
82
|
items:
|
|
66
83
|
type: object
|
|
67
84
|
properties:
|
|
68
|
-
|
|
69
|
-
type:
|
|
70
|
-
|
|
71
|
-
description:
|
|
72
|
-
type: string
|
|
73
|
-
URL:
|
|
74
|
-
type: string
|
|
75
|
-
required:
|
|
76
|
-
- description
|
|
77
|
-
- URL
|
|
78
|
-
additionalProperties: false
|
|
79
|
-
number of reports about the page:
|
|
80
|
-
type: number
|
|
81
|
-
basics about the latest report:
|
|
82
|
-
type: object
|
|
83
|
-
properties:
|
|
84
|
-
identifier:
|
|
85
|
-
type: string
|
|
86
|
-
description: 'Identifier of the report (example: 260503T0432-x9z).'
|
|
87
|
-
completion date and time:
|
|
88
|
-
type: string
|
|
89
|
-
days since the report was completed:
|
|
90
|
-
type: number
|
|
91
|
-
nullable: true
|
|
92
|
-
required:
|
|
93
|
-
- identifier
|
|
94
|
-
- completion date and time
|
|
95
|
-
- days since the report was completed
|
|
96
|
-
additionalProperties: false
|
|
97
|
-
how to get details about the latest report:
|
|
98
|
-
type: object
|
|
99
|
-
properties:
|
|
100
|
-
method:
|
|
101
|
-
type: string
|
|
102
|
-
enum:
|
|
103
|
-
- GET
|
|
104
|
-
URL:
|
|
105
|
-
type: string
|
|
106
|
-
required:
|
|
107
|
-
- method
|
|
108
|
-
- URL
|
|
109
|
-
additionalProperties: false
|
|
110
|
-
web users can get details about the latest report at:
|
|
85
|
+
description:
|
|
86
|
+
type: string
|
|
87
|
+
URL:
|
|
111
88
|
type: string
|
|
89
|
+
identifier:
|
|
90
|
+
type: string
|
|
91
|
+
description: 'Identifier of the latest report about the page (example: 260503T0432-x9z).'
|
|
112
92
|
required:
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
- how to get details about the latest report
|
|
117
|
-
- web users can get details about the latest report at
|
|
93
|
+
- description
|
|
94
|
+
- URL
|
|
95
|
+
- identifier
|
|
118
96
|
additionalProperties: false
|
|
119
97
|
how to request that a page with no report be tested:
|
|
120
98
|
type: object
|
|
@@ -156,6 +134,8 @@ paths:
|
|
|
156
134
|
- URL
|
|
157
135
|
additionalProperties: false
|
|
158
136
|
required:
|
|
137
|
+
- how to get test results about a page
|
|
138
|
+
- web users can get test results about a page at
|
|
159
139
|
- basics about all tested pages
|
|
160
140
|
- how to request that a page with no report be tested
|
|
161
141
|
- how a web user can request that the page be tested
|
package/openapi.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.1.0
|
|
|
2
2
|
info:
|
|
3
3
|
title: Kilotest API
|
|
4
4
|
description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
|
|
5
|
-
version:
|
|
5
|
+
version: 61.0.0
|
|
6
6
|
license:
|
|
7
7
|
name: MIT
|
|
8
8
|
identifier: MIT
|
|
@@ -58,61 +58,38 @@ paths:
|
|
|
58
58
|
response content:
|
|
59
59
|
type: object
|
|
60
60
|
properties:
|
|
61
|
+
how to get test results about a page:
|
|
62
|
+
type: object
|
|
63
|
+
properties:
|
|
64
|
+
method:
|
|
65
|
+
type: string
|
|
66
|
+
const: GET
|
|
67
|
+
URL:
|
|
68
|
+
type: string
|
|
69
|
+
description: URL template; replace {identifier} with the identifier of the latest report about the page.
|
|
70
|
+
required:
|
|
71
|
+
- method
|
|
72
|
+
- URL
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
web users can get test results about a page at:
|
|
75
|
+
type: string
|
|
76
|
+
description: URL template; replace {identifier} with the identifier of the latest report about the page.
|
|
61
77
|
basics about all tested pages:
|
|
62
78
|
type: array
|
|
63
79
|
items:
|
|
64
80
|
type: object
|
|
65
81
|
properties:
|
|
66
|
-
|
|
67
|
-
type:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
URL:
|
|
72
|
-
type: string
|
|
73
|
-
required:
|
|
74
|
-
- description
|
|
75
|
-
- URL
|
|
76
|
-
additionalProperties: false
|
|
77
|
-
number of reports about the page:
|
|
78
|
-
type: number
|
|
79
|
-
basics about the latest report:
|
|
80
|
-
type: object
|
|
81
|
-
properties:
|
|
82
|
-
identifier:
|
|
83
|
-
type: string
|
|
84
|
-
description: 'Identifier of the report (example: 260503T0432-x9z).'
|
|
85
|
-
completion date and time:
|
|
86
|
-
type: string
|
|
87
|
-
days since the report was completed:
|
|
88
|
-
type:
|
|
89
|
-
- number
|
|
90
|
-
- 'null'
|
|
91
|
-
required:
|
|
92
|
-
- identifier
|
|
93
|
-
- completion date and time
|
|
94
|
-
- days since the report was completed
|
|
95
|
-
additionalProperties: false
|
|
96
|
-
how to get details about the latest report:
|
|
97
|
-
type: object
|
|
98
|
-
properties:
|
|
99
|
-
method:
|
|
100
|
-
type: string
|
|
101
|
-
const: GET
|
|
102
|
-
URL:
|
|
103
|
-
type: string
|
|
104
|
-
required:
|
|
105
|
-
- method
|
|
106
|
-
- URL
|
|
107
|
-
additionalProperties: false
|
|
108
|
-
web users can get details about the latest report at:
|
|
82
|
+
description:
|
|
83
|
+
type: string
|
|
84
|
+
URL:
|
|
85
|
+
type: string
|
|
86
|
+
identifier:
|
|
109
87
|
type: string
|
|
88
|
+
description: 'Identifier of the latest report about the page (example: 260503T0432-x9z).'
|
|
110
89
|
required:
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
-
-
|
|
114
|
-
- how to get details about the latest report
|
|
115
|
-
- web users can get details about the latest report at
|
|
90
|
+
- description
|
|
91
|
+
- URL
|
|
92
|
+
- identifier
|
|
116
93
|
additionalProperties: false
|
|
117
94
|
how to request that a page with no report be tested:
|
|
118
95
|
type: object
|
|
@@ -153,6 +130,8 @@ paths:
|
|
|
153
130
|
- URL
|
|
154
131
|
additionalProperties: false
|
|
155
132
|
required:
|
|
133
|
+
- how to get test results about a page
|
|
134
|
+
- web users can get test results about a page at
|
|
156
135
|
- basics about all tested pages
|
|
157
136
|
- how to request that a page with no report be tested
|
|
158
137
|
- how a web user can request that the page be tested
|
package/package.json
CHANGED
package/robots.txt
CHANGED
|
@@ -14,4 +14,7 @@ Disallow: /rewindReportsForm.html
|
|
|
14
14
|
Disallow: /expungeReportsForm.html
|
|
15
15
|
Disallow: /renewWCAGForm.html
|
|
16
16
|
Disallow: /renewWCAG.html
|
|
17
|
+
# JSON endpoints, whose contents are not indexable pages; crawling them is pointless.
|
|
18
|
+
Disallow: /api/
|
|
19
|
+
Disallow: /fullReport.json
|
|
17
20
|
Sitemap: https://kilotest.com/sitemap.xml
|