@jrpool/kilotest 46.1.0 → 47.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/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +8 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
- package/api/{requestRetest.js → requestRetest.ts} +32 -25
- package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
- package/api/{requestTest.js → requestTest.ts} +35 -28
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +31 -11
- package/api/{util.js → util.ts} +75 -46
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +356 -0
- package/docs/SERVICE.md +144 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +462 -137
- package/{index.js → index.ts} +388 -283
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/{util.test.js → util.test.ts} +350 -257
- package/util.ts +921 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +48 -53
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +70 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +31 -0
- package/web/requestRetest/requestRetest.test.ts +116 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/{index.js → index.ts} +5 -5
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +17 -21
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -87
- package/api/requestFeature.test.js +0 -58
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -126
- package/util.js +0 -733
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestTest.test.
|
|
3
|
-
Tests for api/requestTest.
|
|
2
|
+
requestTest.test.ts
|
|
3
|
+
Tests for api/requestTest.ts using the fixture corpus.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import {test, before, beforeEach, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import fs from 'node:fs/promises';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import {fixtureDBDir} from '../test/dbFixture.ts';
|
|
12
13
|
|
|
13
14
|
// SETUP AND TEARDOWN
|
|
14
15
|
|
|
15
16
|
const savedDBDir = process.env.DB_DIR;
|
|
16
|
-
|
|
17
|
+
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
18
|
+
let logged: any[] = [];
|
|
19
|
+
const originalLog = console.log;
|
|
17
20
|
|
|
18
21
|
before(() => {
|
|
19
|
-
process.env.DB_DIR =
|
|
20
|
-
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
21
|
-
apiUtil.processTestRequest = async (testType, what, url, reason) => {
|
|
22
|
-
processTestRequestCalls.push({testType, what, url, reason});
|
|
23
|
-
};
|
|
22
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
// Reset the
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// Reset the test-requests file and capture console.log, so the alert that
|
|
26
|
+
// processTestRequest emits can be observed and tests are order-independent.
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
29
|
+
logged = [];
|
|
30
|
+
console.log = (...args) => logged.push(args.join(' '));
|
|
29
31
|
});
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
const {response} = require('./requestTest');
|
|
33
|
+
import {response} from './requestTest.ts';
|
|
33
34
|
|
|
34
35
|
after(() => {
|
|
36
|
+
console.log = originalLog;
|
|
35
37
|
if (savedDBDir !== undefined) {
|
|
36
38
|
process.env.DB_DIR = savedDBDir;
|
|
37
39
|
}
|
|
@@ -44,44 +46,47 @@ after(() => {
|
|
|
44
46
|
|
|
45
47
|
test('requestTest rejects an empty description', async () => {
|
|
46
48
|
const body = await response(['', 'https://example.com/test', 'A reason that is long enough.']);
|
|
47
|
-
assert.ok(body['response content']['details about your request'].error);
|
|
48
|
-
assert.
|
|
49
|
+
assert.ok((body['response content']['details about your request'] as any).error);
|
|
50
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
test('requestTest rejects a description longer than 100 characters', async () => {
|
|
52
54
|
const longWhat = 'x'.repeat(101);
|
|
53
55
|
const body = await response([longWhat, 'https://example.com/test', 'A reason that is long enough.']);
|
|
54
|
-
assert.ok(body['response content']['details about your request'].error);
|
|
55
|
-
assert.
|
|
56
|
+
assert.ok((body['response content']['details about your request'] as any).error);
|
|
57
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
test('requestTest rejects a URL shorter than 12 characters', async () => {
|
|
59
61
|
const body = await response(['Test Page', 'short', 'A reason that is long enough.']);
|
|
60
|
-
assert.ok(body['response content']['details about your request'].error);
|
|
61
|
-
assert.
|
|
62
|
+
assert.ok((body['response content']['details about your request'] as any).error);
|
|
63
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
test('requestTest rejects a syntactically invalid URL with the correct length', async () => {
|
|
65
67
|
const body = await response(['Test Page', 'not-a-valid-url', 'A reason that is long enough.']);
|
|
66
|
-
const details = body['response content']['details about your request'];
|
|
68
|
+
const details = body['response content']['details about your request'] as any;
|
|
67
69
|
assert.ok(details.error.includes('invalid URL'));
|
|
68
|
-
assert.
|
|
70
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
69
71
|
});
|
|
70
72
|
|
|
71
73
|
test('requestTest rejects an already-tested page', async () => {
|
|
72
74
|
const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']);
|
|
73
|
-
const details = body['response content']['details about your request'];
|
|
75
|
+
const details = body['response content']['details about your request'] as any;
|
|
74
76
|
assert.ok(details.error.includes('already been tested'));
|
|
75
|
-
assert.
|
|
77
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
test('requestTest accepts a valid new page request', async () => {
|
|
79
81
|
const body = await response(['Brand New Page', 'https://example.com/brandnew', 'A reason that is long enough.']);
|
|
80
|
-
const details = body['response content']['details about your request'];
|
|
82
|
+
const details = body['response content']['details about your request'] as any;
|
|
81
83
|
assert.equal(details.error, undefined);
|
|
82
84
|
assert.equal(details['page to be tested'].description, 'Brand New Page');
|
|
83
|
-
assert.
|
|
84
|
-
|
|
85
|
+
assert.ok(logged.some(line =>
|
|
86
|
+
line.startsWith('WARNING (Kilotest: new test request in the API)')
|
|
87
|
+
&& line.includes('Brand New Page')
|
|
88
|
+
&& line.includes('https://example.com/brandnew')
|
|
89
|
+
));
|
|
85
90
|
});
|
|
86
91
|
|
|
87
92
|
test('requestTest includes disposition information for a valid request', async () => {
|
|
@@ -1,42 +1,45 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestTest.
|
|
2
|
+
requestTest.ts
|
|
3
3
|
Processes a request to test an untested page and returns an acknowledgement.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {getResponseMetadata, getThisHost, getToolsFacts, processTestRequest} from './util.ts';
|
|
10
|
+
import {getReportExtracts, isURL} from '../util.ts';
|
|
11
|
+
import {requestTestResponseSchema} from './schemas.ts';
|
|
12
|
+
|
|
13
|
+
// TYPES
|
|
14
|
+
|
|
15
|
+
// The response content defined by the response schema.
|
|
16
|
+
type ResponseContent = z.infer<typeof requestTestResponseSchema>['response content'];
|
|
10
17
|
|
|
11
18
|
// FUNCTIONS
|
|
12
19
|
|
|
13
20
|
// Returns the response body.
|
|
14
|
-
|
|
15
|
-
const [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const whatLength = what.length;
|
|
21
|
+
export const response = async (args: string[]) => {
|
|
22
|
+
const [description = '', url = '', reason = ''] = args;
|
|
23
|
+
const thisHost = getThisHost();
|
|
24
|
+
// Initialize the response-content properties.
|
|
25
|
+
let requestDetails: ResponseContent['details about your request'];
|
|
26
|
+
let requestDisposition: ResponseContent['disposition of your request'] = null;
|
|
27
|
+
const descriptionLength = description.length;
|
|
22
28
|
// If the description is empty or too long:
|
|
23
|
-
if (!
|
|
24
|
-
|
|
25
|
-
responseContent['details about your request'] = {
|
|
29
|
+
if (!descriptionLength || descriptionLength > 100) {
|
|
30
|
+
requestDetails = {
|
|
26
31
|
error: 'request invalid: your description of the page is not between 1 and 100 characters long'
|
|
27
32
|
};
|
|
28
33
|
}
|
|
29
34
|
// Otherwise, i.e. if the URL is too short or too long::
|
|
30
35
|
else if (url.length < 12 || url.length > 300) {
|
|
31
|
-
|
|
32
|
-
responseContent['details about your request'] = {
|
|
36
|
+
requestDetails = {
|
|
33
37
|
error: 'request invalid: you specified a URL for the page that is not between 12 and 300 characters long'
|
|
34
38
|
};
|
|
35
39
|
}
|
|
36
40
|
// Otherwise, i.e. if the URL is invalid:
|
|
37
41
|
else if (!isURL(url)) {
|
|
38
|
-
|
|
39
|
-
responseContent['details about your request'] = {
|
|
42
|
+
requestDetails = {
|
|
40
43
|
error: 'request invalid: you specified an invalid URL for the page'
|
|
41
44
|
};
|
|
42
45
|
}
|
|
@@ -46,34 +49,38 @@ exports.response = async args => {
|
|
|
46
49
|
const reportExtracts = await getReportExtracts();
|
|
47
50
|
// If any report is on a page with the specified description and URL:
|
|
48
51
|
if (
|
|
49
|
-
reportExtracts.some(extract => extract.
|
|
52
|
+
reportExtracts.some(extract => extract.description === description && extract.url === url)
|
|
50
53
|
) {
|
|
51
|
-
|
|
52
|
-
responseContent['details about your request'] = {
|
|
54
|
+
requestDetails = {
|
|
53
55
|
error: 'request invalid: the page has already been tested and its report is available'
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
// Otherwise, i.e. if none is on the page:
|
|
57
59
|
else {
|
|
58
60
|
// Process the request.
|
|
59
|
-
await processTestRequest('test',
|
|
60
|
-
// Add details about the request
|
|
61
|
-
|
|
61
|
+
await processTestRequest('test', description, url, reason);
|
|
62
|
+
// Add details about the request.
|
|
63
|
+
requestDetails = {
|
|
62
64
|
'date and time received': new Date().toISOString(),
|
|
63
65
|
'page to be tested': {
|
|
64
|
-
description
|
|
66
|
+
description,
|
|
65
67
|
URL: url
|
|
66
68
|
},
|
|
67
69
|
'reason why the page should be tested': reason
|
|
68
70
|
};
|
|
69
|
-
// Add information about the disposition of the request
|
|
70
|
-
|
|
71
|
+
// Add information about the disposition of the request.
|
|
72
|
+
requestDisposition = {
|
|
71
73
|
'what happens next': 'Your request is likely to be approved and processed within 1 hour to 1 day.',
|
|
72
74
|
'how you can check for completion': 'You can call the listReports tool to learn whether the page has been tested and a report is available.',
|
|
73
75
|
'how a web user can check for completion': `A web user can visit ${thisHost}/listReports.html to learn whether the page has been tested and a report is available.`
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
// Create the response content.
|
|
80
|
+
const responseContent: ResponseContent = {
|
|
81
|
+
'details about your request': requestDetails,
|
|
82
|
+
'disposition of your request': requestDisposition
|
|
83
|
+
};
|
|
77
84
|
// Create a response body.
|
|
78
85
|
const body = {
|
|
79
86
|
'tool collection': getToolsFacts(),
|
|
@@ -83,7 +90,7 @@ exports.response = async args => {
|
|
|
83
90
|
method: 'POST',
|
|
84
91
|
URL: `${thisHost}/api/requestTest`,
|
|
85
92
|
body: {
|
|
86
|
-
description
|
|
93
|
+
description,
|
|
87
94
|
URL: url,
|
|
88
95
|
reason
|
|
89
96
|
},
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
routes.test.
|
|
3
|
-
Tests for api/routes.
|
|
2
|
+
routes.test.ts
|
|
3
|
+
Tests for api/routes.ts, verifying the structure and completeness of the OpenAPI route metadata.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import {test} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {routes} from './routes.ts';
|
|
11
11
|
|
|
12
12
|
// TESTS
|
|
13
13
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/*
|
|
2
|
-
routes.
|
|
3
|
-
Route metadata for OpenAPI generation. Pairs each Kilotest API operation with its HTTP method, path template, and the request/response schemas from schemas.
|
|
2
|
+
routes.ts
|
|
3
|
+
Route metadata for OpenAPI generation. Pairs each Kilotest API operation with its HTTP method, path template, and the request/response schemas from schemas.ts. This table supplies the facts that schemas.ts cannot express on its own (method, path, path-vs-body placement), mirroring the manual dispatch in index.ts and the // GET/POST comments in schemas.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {
|
|
10
10
|
listIssuesSchema,
|
|
11
11
|
listViolatorsSchema,
|
|
12
12
|
listDiagnosesSchema,
|
|
@@ -22,11 +22,11 @@ const {
|
|
|
22
22
|
requestTestResponseSchema,
|
|
23
23
|
requestRetestResponseSchema,
|
|
24
24
|
requestFeatureResponseSchema
|
|
25
|
-
}
|
|
25
|
+
} from './schemas.ts';
|
|
26
26
|
|
|
27
27
|
// ROUTES
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
export const routes = [
|
|
30
30
|
{
|
|
31
31
|
operationId: 'listReports',
|
|
32
32
|
method: 'get',
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
/*
|
|
2
|
-
schemas.
|
|
3
|
-
Zod input and response schemas shared by mcp.
|
|
2
|
+
schemas.ts
|
|
3
|
+
Zod input and response schemas shared by mcp.ts (MCP tool inputSchema/outputSchema), api/routes.ts, and the openapi.yaml generation script (scripts/generate-openapi.ts). Descriptions and component ids (.meta({id})) in this file are the single source of truth for parameter documentation and OpenAPI component naming and reuse.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
|
|
10
|
+
// TYPES
|
|
11
|
+
|
|
12
|
+
// A Zod schema of any shape, as accepted by the schema-composing helpers below.
|
|
13
|
+
type ZodTypeAny = import('zod').ZodTypeAny;
|
|
9
14
|
|
|
10
15
|
// REQUEST SCHEMAS
|
|
11
16
|
|
|
12
17
|
// listIssues: GET /api/listIssues/{timeStamp}/{jobID}
|
|
13
|
-
|
|
18
|
+
export const listIssuesSchema = {
|
|
14
19
|
timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
|
|
15
20
|
jobID: z.string().describe('Job identifier of the report (example: x9z)')
|
|
16
21
|
};
|
|
17
22
|
|
|
18
23
|
// listViolators: GET /api/listViolators/{issueID}/{timeStamp}/{jobID}
|
|
19
|
-
|
|
24
|
+
export const listViolatorsSchema = {
|
|
20
25
|
issueID: z.string().describe('Issue identifier (example: contrastPoor)'),
|
|
21
26
|
timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
|
|
22
27
|
jobID: z.string().describe('Job identifier of the report (example: x9z)')
|
|
23
28
|
};
|
|
24
29
|
|
|
25
30
|
// listDiagnoses: GET /api/listDiagnoses/{catalogIndex}/{issueID}/{timeStamp}/{jobID}
|
|
26
|
-
|
|
31
|
+
export const listDiagnosesSchema = {
|
|
27
32
|
catalogIndex: z.string().describe('Identifier of the issue-exhibiting element in the catalog of elements on the page (example: 372)'),
|
|
28
33
|
issueID: z.string().describe('Issue identifier (example: contrastPoor)'),
|
|
29
34
|
timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
|
|
@@ -31,31 +36,31 @@ exports.listDiagnosesSchema = {
|
|
|
31
36
|
};
|
|
32
37
|
|
|
33
38
|
// getReport: GET /api/getReport/{timeStamp}/{jobID}
|
|
34
|
-
|
|
39
|
+
export const getReportSchema = {
|
|
35
40
|
timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
|
|
36
41
|
jobID: z.string().describe('Job identifier of the report (example: x9z)')
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
// requestTest: POST /api/requestTest
|
|
40
|
-
|
|
45
|
+
export const requestTestSchema = {
|
|
41
46
|
description: z.string().describe('10- to 100-character description of the page conforming to the naming convention used in the listReports output'),
|
|
42
47
|
URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
|
|
43
48
|
reason: z.string().describe('20- to 100-character reason why the page should be tested')
|
|
44
49
|
};
|
|
45
50
|
|
|
46
51
|
// requestRetest: POST /api/requestRetest/{timeStamp}/{jobID}
|
|
47
|
-
|
|
52
|
+
export const requestRetestSchema = {
|
|
48
53
|
timeStamp: z.string().describe('Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'),
|
|
49
54
|
jobID: z.string().describe('Job identifier of the latest report about the page (example: x9z)'),
|
|
50
55
|
reason: z.string().describe('20- to 100-character reason why the page should be retested')
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
// requestFeature: POST /api/requestFeature
|
|
54
|
-
|
|
59
|
+
export const requestFeatureSchema = {
|
|
55
60
|
feature: z.string().describe('description of requested feature improvement or new feature')
|
|
56
61
|
};
|
|
57
62
|
|
|
58
|
-
// listReports takes no input; omitted (mcp.
|
|
63
|
+
// listReports takes no input; omitted (mcp.ts already uses inputSchema: {}).
|
|
59
64
|
|
|
60
65
|
// RESPONSE SCHEMAS
|
|
61
66
|
|
|
@@ -97,7 +102,7 @@ const requestReferenceSchema = z.object({
|
|
|
97
102
|
}).meta({id: 'RequestReference'});
|
|
98
103
|
|
|
99
104
|
// GET endpoints have no 'body'; POST endpoints get a request-specific body schema.
|
|
100
|
-
const thisRequestSchema = (method, bodySchema) => z.object({
|
|
105
|
+
const thisRequestSchema = (method: string, bodySchema?: ZodTypeAny) => z.object({
|
|
101
106
|
description: z.string(),
|
|
102
107
|
method: z.literal(method),
|
|
103
108
|
URL: z.string(),
|
|
@@ -105,11 +110,11 @@ const thisRequestSchema = (method, bodySchema) => z.object({
|
|
|
105
110
|
'closest ancestor request': requestReferenceSchema.nullable()
|
|
106
111
|
});
|
|
107
112
|
|
|
108
|
-
const envelope = (
|
|
109
|
-
method,
|
|
110
|
-
responseContentSchema,
|
|
111
|
-
bodySchema,
|
|
112
|
-
|
|
113
|
+
const envelope = <T extends ZodTypeAny>(
|
|
114
|
+
method: string,
|
|
115
|
+
responseContentSchema: T,
|
|
116
|
+
bodySchema?: ZodTypeAny,
|
|
117
|
+
similarWebSchema: ZodTypeAny = similarWebRequestsSchema
|
|
113
118
|
) => z.object({
|
|
114
119
|
'tool collection': toolsFactsSchema,
|
|
115
120
|
'tool name': z.string(),
|
|
@@ -133,7 +138,7 @@ const responseMetadataSchema = z.object({
|
|
|
133
138
|
const reportBasicsSchema = z.object({
|
|
134
139
|
identifier: z.string().describe('timeStamp-jobID identifier of the report.'),
|
|
135
140
|
'completion date and time': z.string(),
|
|
136
|
-
'days since the report was completed': z.number(),
|
|
141
|
+
'days since the report was completed': z.number().nullable(),
|
|
137
142
|
'tested web page': z.object({description: z.string(), URL: z.string()}),
|
|
138
143
|
'whether a later report about the same page exists': z.boolean()
|
|
139
144
|
}).meta({id: 'ReportBasics'});
|
|
@@ -161,7 +166,7 @@ const issueBasicsOrErrorSchema = z.union([
|
|
|
161
166
|
z.object({error: z.string()})
|
|
162
167
|
]).meta({id: 'IssueBasicsOrError'});
|
|
163
168
|
|
|
164
|
-
|
|
169
|
+
export const listReportsResponseSchema = envelope('GET', z.object({
|
|
165
170
|
'basics about all available reports': z.array(reportBasicsSchema.extend({
|
|
166
171
|
'how to get details about the report': z.object({method: z.literal('GET'), URL: z.string()}),
|
|
167
172
|
'web users can get details about the report at': z.string()
|
|
@@ -175,7 +180,7 @@ exports.listReportsResponseSchema = envelope('GET', z.object({
|
|
|
175
180
|
'how a web user can request that the page be tested': z.object({URL: z.string()})
|
|
176
181
|
}));
|
|
177
182
|
|
|
178
|
-
|
|
183
|
+
export const listIssuesResponseSchema = envelope('GET', z.object({
|
|
179
184
|
'basics about the report': reportBasicsOrErrorSchema,
|
|
180
185
|
'details about the report': z.object({
|
|
181
186
|
'job definition': z.object({
|
|
@@ -215,20 +220,20 @@ exports.listIssuesResponseSchema = envelope('GET', z.object({
|
|
|
215
220
|
}).nullable(),
|
|
216
221
|
'how a web user can get the full report in JSON': z.object({URL: z.string()}).nullable(),
|
|
217
222
|
'basics about all issues reported in the report': z.array(issueBasicsSchema.extend({
|
|
218
|
-
'rule engines with any violations belonging to the issue': z.array(z.string()),
|
|
223
|
+
'rule engines with any violations belonging to the issue': z.array(z.string().nullable()),
|
|
219
224
|
'how to get details about the issue': z.object({method: z.literal('GET'), URL: z.string()}),
|
|
220
225
|
'web users can get details about the issue at': z.string()
|
|
221
|
-
}))
|
|
226
|
+
})).nullable()
|
|
222
227
|
}));
|
|
223
228
|
|
|
224
|
-
|
|
229
|
+
export const listViolatorsResponseSchema = envelope('GET', z.object({
|
|
225
230
|
'basics about the report': reportBasicsOrErrorSchema,
|
|
226
231
|
'basics about the issue': issueBasicsOrErrorSchema,
|
|
227
232
|
'details about the issue': z.object({
|
|
228
233
|
'rule engines reporting violations belonging to the issue': z.array(ruleEngineFactsSchema)
|
|
229
234
|
}).nullable(),
|
|
230
|
-
'basics about all elements
|
|
231
|
-
identifier: z.string(),
|
|
235
|
+
'basics about all elements exhibiting the issue': z.array(z.object({
|
|
236
|
+
identifier: z.union([z.string(), z.number()]).describe('Catalog index of the element in the report; a string or number, copied verbatim.'),
|
|
232
237
|
'tag name': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
|
|
233
238
|
'inner text': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
|
|
234
239
|
'count of rule engines reporting that the element exhibited the issue': z.number(),
|
|
@@ -237,7 +242,7 @@ exports.listViolatorsResponseSchema = envelope('GET', z.object({
|
|
|
237
242
|
})).nullable()
|
|
238
243
|
}));
|
|
239
244
|
|
|
240
|
-
|
|
245
|
+
export const listDiagnosesResponseSchema = envelope('GET', z.object({
|
|
241
246
|
'basics about the report': reportBasicsOrErrorSchema,
|
|
242
247
|
'basics about the issue': issueBasicsOrErrorSchema,
|
|
243
248
|
'basics about the element': z.object({
|
|
@@ -255,41 +260,55 @@ exports.listDiagnosesResponseSchema = envelope('GET', z.object({
|
|
|
255
260
|
'description of the violation': z.unknown().describe('Copied from the report instance (what); type not guaranteed.'),
|
|
256
261
|
'severity of the violation on a 0-to-3 scale': z.unknown().describe('Copied from the report instance (ordinalSeverity); type not guaranteed.'),
|
|
257
262
|
'count of violations of the rule by the element': z.number()
|
|
258
|
-
})).or(z.object({error: z.string()}))
|
|
263
|
+
})).or(z.object({error: z.string()})).nullable()
|
|
259
264
|
}));
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
export const getReportResponseSchema = envelope('GET', z.object({
|
|
262
267
|
'size of the report in bytes': z.union([z.number(), z.string()]),
|
|
263
268
|
'full report': z.unknown().describe('The full raw Testaro/Testilo report JSON, copied verbatim, or an error object if it could not be retrieved.')
|
|
264
269
|
}));
|
|
265
270
|
|
|
266
|
-
|
|
271
|
+
export const requestTestResponseSchema = envelope(
|
|
267
272
|
'POST',
|
|
268
|
-
z.object({
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
273
|
+
z.object({
|
|
274
|
+
'details about your request': z.union([
|
|
275
|
+
z.object({error: z.string()}),
|
|
276
|
+
z.object({
|
|
277
|
+
'date and time received': z.string(),
|
|
278
|
+
'page to be tested': z.object({description: z.string(), URL: z.string()}),
|
|
279
|
+
'reason why the page should be tested': z.string()
|
|
280
|
+
})
|
|
281
|
+
]),
|
|
282
|
+
'disposition of your request': z.object({
|
|
283
|
+
'what happens next': z.string(),
|
|
284
|
+
'how you can check for completion': z.string(),
|
|
285
|
+
'how a web user can check for completion': z.string()
|
|
286
|
+
}).nullable()
|
|
287
|
+
}),
|
|
288
|
+
z.object(requestTestSchema)
|
|
277
289
|
);
|
|
278
290
|
|
|
279
|
-
|
|
291
|
+
export const requestRetestResponseSchema = envelope(
|
|
280
292
|
'POST',
|
|
281
|
-
z.object({
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
293
|
+
z.object({
|
|
294
|
+
'details about your request': z.union([
|
|
295
|
+
z.object({error: z.string()}),
|
|
296
|
+
z.object({
|
|
297
|
+
'date and time received': z.string(),
|
|
298
|
+
'page to be retested': z.object({description: z.string(), URL: z.string()}),
|
|
299
|
+
'reason why the page should be retested': z.string()
|
|
300
|
+
})
|
|
301
|
+
]),
|
|
302
|
+
'disposition of your request': z.object({
|
|
303
|
+
'what happens next': z.string(),
|
|
304
|
+
'how you can check for completion': z.string(),
|
|
305
|
+
'how a web user can check for completion': z.string()
|
|
306
|
+
}).nullable()
|
|
307
|
+
}),
|
|
308
|
+
z.object({reason: requestRetestSchema.reason})
|
|
290
309
|
);
|
|
291
310
|
|
|
292
|
-
|
|
311
|
+
export const requestFeatureResponseSchema = envelope(
|
|
293
312
|
'POST',
|
|
294
313
|
z.object({'details about your request': z.union([
|
|
295
314
|
z.object({error: z.string()}),
|
|
@@ -298,6 +317,6 @@ exports.requestFeatureResponseSchema = envelope(
|
|
|
298
317
|
disposition: z.string()
|
|
299
318
|
})
|
|
300
319
|
])}),
|
|
301
|
-
z.object(
|
|
320
|
+
z.object(requestFeatureSchema),
|
|
302
321
|
z.null()
|
|
303
322
|
);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/*
|
|
2
|
-
util.test.
|
|
3
|
-
Tests for api/util.
|
|
2
|
+
util.test.ts
|
|
3
|
+
Tests for api/util.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const {getReportBasics, getResponseMetadata, getRuleEngineFacts, getRuleEnginesFacts, getIssueSpec, getToolsFacts, processTestRequest} = require('./util');
|
|
8
|
+
import {test} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {getReportBasics, getResponseMetadata, getRuleEngineFacts, getRuleEnginesFacts, getIssueSpec, getToolsFacts, processTestRequest} from './util.ts';
|
|
12
11
|
|
|
13
12
|
// SETUP
|
|
14
13
|
|
|
@@ -64,8 +63,8 @@ test('getIssueSpec returns null for the ignorable issue ID', () => {
|
|
|
64
63
|
});
|
|
65
64
|
|
|
66
65
|
test('getReportBasics returns an error for a nonexistent report', async () => {
|
|
67
|
-
process.env.DB_DIR =
|
|
68
|
-
const basics = await getReportBasics('999999T9999', 'xyz');
|
|
66
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
67
|
+
const basics: any = await getReportBasics('999999T9999', 'xyz');
|
|
69
68
|
assert.ok(basics.error);
|
|
70
69
|
if (savedDBDir !== undefined) {
|
|
71
70
|
process.env.DB_DIR = savedDBDir;
|
|
@@ -75,11 +74,32 @@ test('getReportBasics returns an error for a nonexistent report', async () => {
|
|
|
75
74
|
}
|
|
76
75
|
});
|
|
77
76
|
|
|
78
|
-
test('
|
|
79
|
-
process.env.DB_DIR =
|
|
77
|
+
test('getReportBasics returns an error for a report whose extract cannot be read', async () => {
|
|
78
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
79
|
+
const fs = await import('node:fs/promises');
|
|
80
|
+
const path = await import('node:path');
|
|
81
|
+
const reportPath = path.join(process.env.DB_DIR, 'reports', '888888T8888-bad.json');
|
|
82
|
+
await fs.writeFile(reportPath, '{"malformed": true}\n');
|
|
83
|
+
try {
|
|
84
|
+
const basics: any = await getReportBasics('888888T8888', 'bad');
|
|
85
|
+
assert.ok(basics.error);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await fs.unlink(reportPath);
|
|
89
|
+
if (savedDBDir !== undefined) {
|
|
90
|
+
process.env.DB_DIR = savedDBDir;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
delete process.env.DB_DIR;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('processTestRequest returns an error for a duplicate request', async () => {
|
|
99
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
80
100
|
// Submit the same request twice; the second should be a duplicate.
|
|
81
101
|
await processTestRequest('test', 'Dup Page', 'https://example.com/dup', 'A reason that is long enough.');
|
|
82
|
-
const result = await processTestRequest('test', 'Dup Page', 'https://example.com/dup', 'A reason that is long enough.');
|
|
102
|
+
const result: any = await processTestRequest('test', 'Dup Page', 'https://example.com/dup', 'A reason that is long enough.');
|
|
83
103
|
assert.equal(result.status, 'error');
|
|
84
104
|
assert.equal(result.message, 'Duplicate request');
|
|
85
105
|
if (savedDBDir !== undefined) {
|