@jrpool/kilotest 46.0.14 → 46.1.2
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/.devin/config.local.json +24 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +2 -0
- package/api/listReports.js +4 -2
- package/api/requestFeature.test.js +6 -2
- package/api/requestRetest.test.js +6 -2
- package/api/requestTest.test.js +6 -2
- package/api/util.js +25 -21
- package/apiIntegration.test.js +1 -1
- package/capability.md +51 -0
- package/checkinfo.js +146 -0
- package/docs/AI-TOOLS.md +31 -1
- package/docs/SERVICE.md +1 -1
- package/index.html +1 -0
- package/index.js +33 -19
- package/index.test.js +130 -18
- package/llms-full.txt +1 -0
- package/llms.txt +1 -0
- package/package.json +5 -4
- package/smokeTest.js +34 -29
- package/util.js +11 -0
- package/util.test.js +2 -2
- package/web/listReports/listReports.test.js +1 -1
- package/web/reannotate/reannotate.test.js +2 -2
- package/web/requestTest/requestTest.test.js +3 -3
- package/web/rewindReportsForm/rewindReportsForm.test.js +1 -1
- package/web/tutorial/tutorial.test.js +4 -4
package/.devin/config.local.json
CHANGED
|
@@ -9,7 +9,30 @@
|
|
|
9
9
|
"Exec(git show)",
|
|
10
10
|
"Exec(git commit)",
|
|
11
11
|
"Exec(git check-ignore)",
|
|
12
|
-
"Exec(git remote)"
|
|
12
|
+
"Exec(git remote)",
|
|
13
|
+
"Exec(node)",
|
|
14
|
+
"Exec(source)",
|
|
15
|
+
"Exec(nvm use)",
|
|
16
|
+
"Exec(npm run)",
|
|
17
|
+
"Exec(lsof)",
|
|
18
|
+
"Exec(uname)",
|
|
19
|
+
"Exec(mkdir)",
|
|
20
|
+
"Exec(set)",
|
|
21
|
+
"Exec(git add)",
|
|
22
|
+
"Exec(gh)",
|
|
23
|
+
"Exec(/opt/homebrew/bin/colima start)",
|
|
24
|
+
"Exec(/opt/homebrew/bin/docker info)",
|
|
25
|
+
"Exec(/opt/homebrew/bin/docker run)",
|
|
26
|
+
"Exec($DOCKER volume)",
|
|
27
|
+
"Exec($DOCKER run)",
|
|
28
|
+
"Exec($DOCKER logs)",
|
|
29
|
+
"Exec($DOCKER exec)",
|
|
30
|
+
"Exec($DOCKER stop)",
|
|
31
|
+
"Exec(sed)",
|
|
32
|
+
"Exec(awk)",
|
|
33
|
+
"Exec(nvm ls)",
|
|
34
|
+
"Exec(.)",
|
|
35
|
+
"Exec(nvm alias)"
|
|
13
36
|
]
|
|
14
37
|
}
|
|
15
38
|
}
|
package/.github/workflows/ci.yml
CHANGED
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://registry.modelcontextprotocol.io/v0/servers?search=kilotest)
|
|
6
6
|
[](https://github.com/jrpool/kilotest/stargazers)
|
|
7
|
+
[](https://tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36)
|
|
7
8
|
|
|
8
9
|
An ensemble testing and reporting service for front-end web quality
|
|
9
10
|
|
|
@@ -63,6 +64,7 @@ There is additional documentation at:
|
|
|
63
64
|
|
|
64
65
|
- [QAI](https://kilotest.com/qai): a tutorial for allowing AI assistants to use Kilotest
|
|
65
66
|
- [AI-TOOLS.md](docs/AI-TOOLS.md): how Kilotest implements the MCP protocol
|
|
67
|
+
- [Summary of Kilotest capabilities](https://kilotest.com/capability.md)
|
|
66
68
|
- [llms.txt](llms.txt): basic documentation for LLMs
|
|
67
69
|
- [llms-full.txt](llms-full.txt): comprehensive documentation for LLMs
|
|
68
70
|
- [lhm.plugin.json](lhm.plugin.json): documentation for registration with the LobeHub MCP Market
|
package/api/listReports.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
8
|
const {getReportBasics, getResponseMetadata, getToolsFacts, thisHost} = require('./util');
|
|
9
|
-
const {getReportExtracts} = require('../util');
|
|
9
|
+
const {getLatestReportExtracts, getReportExtracts} = require('../util');
|
|
10
10
|
|
|
11
11
|
// FUNCTIONS
|
|
12
12
|
|
|
@@ -22,11 +22,13 @@ exports.response = async () => {
|
|
|
22
22
|
const reportsBasics = [];
|
|
23
23
|
// Get extracts of all available reports.
|
|
24
24
|
const reportExtracts = await getReportExtracts();
|
|
25
|
+
// Get the latest report extracts once, to avoid recomputing them for every report.
|
|
26
|
+
const latestReportExtracts = await getLatestReportExtracts();
|
|
25
27
|
// For each report:
|
|
26
28
|
for (const extract of reportExtracts) {
|
|
27
29
|
const {error, jobID, timeStamp} = extract;
|
|
28
30
|
// Get the basics about it (which may be only an error message).
|
|
29
|
-
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
31
|
+
const reportBasics = await getReportBasics(timeStamp, jobID, extract, latestReportExtracts);
|
|
30
32
|
// If this succeeded:
|
|
31
33
|
if (!error) {
|
|
32
34
|
// Add instructions for getting details to the basics.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {test, before, after} = require('node:test');
|
|
8
|
+
const {test, before, beforeEach, after} = require('node:test');
|
|
9
9
|
const assert = require('node:assert/strict');
|
|
10
10
|
const alerts = require('../alerts');
|
|
11
11
|
|
|
@@ -14,13 +14,17 @@ const alerts = require('../alerts');
|
|
|
14
14
|
let sendAlertCalls = [];
|
|
15
15
|
|
|
16
16
|
before(() => {
|
|
17
|
-
sendAlertCalls = [];
|
|
18
17
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
19
18
|
alerts.sendAlert = async (subject, body) => {
|
|
20
19
|
sendAlertCalls.push({subject, body});
|
|
21
20
|
};
|
|
22
21
|
});
|
|
23
22
|
|
|
23
|
+
// Reset the call log before each test, so tests do not depend on execution order.
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
sendAlertCalls = [];
|
|
26
|
+
});
|
|
27
|
+
|
|
24
28
|
// Require requestFeature after the mock is in place, so it captures the mocked sendAlert.
|
|
25
29
|
const {response} = require('./requestFeature');
|
|
26
30
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {test, before, after} = require('node:test');
|
|
8
|
+
const {test, before, beforeEach, after} = require('node:test');
|
|
9
9
|
const assert = require('node:assert/strict');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const apiUtil = require('./util');
|
|
@@ -17,13 +17,17 @@ let processTestRequestCalls = [];
|
|
|
17
17
|
|
|
18
18
|
before(() => {
|
|
19
19
|
process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
|
|
20
|
-
processTestRequestCalls = [];
|
|
21
20
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
22
21
|
apiUtil.processTestRequest = async (testType, what, url, reason) => {
|
|
23
22
|
processTestRequestCalls.push({testType, what, url, reason});
|
|
24
23
|
};
|
|
25
24
|
});
|
|
26
25
|
|
|
26
|
+
// Reset the call log before each test, so tests do not depend on execution order.
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
processTestRequestCalls = [];
|
|
29
|
+
});
|
|
30
|
+
|
|
27
31
|
// Require requestRetest after the mock is in place, so it captures the mocked processTestRequest.
|
|
28
32
|
const {response} = require('./requestRetest');
|
|
29
33
|
|
package/api/requestTest.test.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
const {test, before, after} = require('node:test');
|
|
8
|
+
const {test, before, beforeEach, after} = require('node:test');
|
|
9
9
|
const assert = require('node:assert/strict');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
const apiUtil = require('./util');
|
|
@@ -17,13 +17,17 @@ let processTestRequestCalls = [];
|
|
|
17
17
|
|
|
18
18
|
before(() => {
|
|
19
19
|
process.env.DB_DIR = path.join(__dirname, '..', 'test', 'fixtures', 'db');
|
|
20
|
-
processTestRequestCalls = [];
|
|
21
20
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
22
21
|
apiUtil.processTestRequest = async (testType, what, url, reason) => {
|
|
23
22
|
processTestRequestCalls.push({testType, what, url, reason});
|
|
24
23
|
};
|
|
25
24
|
});
|
|
26
25
|
|
|
26
|
+
// Reset the call log before each test, so tests do not depend on execution order.
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
processTestRequestCalls = [];
|
|
29
|
+
});
|
|
30
|
+
|
|
27
31
|
// Require requestTest after the mock is in place, so it captures the mocked processTestRequest.
|
|
28
32
|
const {response} = require('./requestTest');
|
|
29
33
|
|
package/api/util.js
CHANGED
|
@@ -35,7 +35,7 @@ exports.getResponseMetadata = () => ({
|
|
|
35
35
|
exports.getToolsFacts = () => ({
|
|
36
36
|
'name': 'Kilotest',
|
|
37
37
|
'description': {
|
|
38
|
-
'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against
|
|
38
|
+
'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) 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. Kilotest makes facts about the issues and the violations retrievable at four levels of granularity.',
|
|
39
39
|
'how to retrieve findings': {
|
|
40
40
|
'level 1': 'Use the listReports tool to get a list of available reports.',
|
|
41
41
|
'level 2': 'Use the listIssues tool to get a list of issues in one report.',
|
|
@@ -68,28 +68,32 @@ exports.getRuleEnginesFacts = ruleEngineIDSet => {
|
|
|
68
68
|
return ruleEnginesFacts;
|
|
69
69
|
};
|
|
70
70
|
// Returns the basics about a report.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// If the
|
|
75
|
-
if (!
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
// Accepts optional precomputed extract and latestReportExtracts to avoid redundant
|
|
72
|
+
// reads when called in a loop over all reports (e.g. by listReports).
|
|
73
|
+
exports.getReportBasics = async (timeStamp, jobID, extract = null, latestReportExtracts = null) => {
|
|
74
|
+
// If an extract was not provided, verify the report exists and read it.
|
|
75
|
+
if (!extract) {
|
|
76
|
+
// Get the creation time of the report.
|
|
77
|
+
const reportStats = await getReportStats(timeStamp, jobID);
|
|
78
|
+
// If the report does not exist:
|
|
79
|
+
if (!reportStats) {
|
|
80
|
+
// Log and return this.
|
|
81
|
+
console.error(`Report ${timeStamp}-${jobID} does not exist.`);
|
|
82
|
+
return {
|
|
83
|
+
error: `Report ${timeStamp}-${jobID} could not be retrieved.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// Otherwise, i.e. if it exists, get an extract of the report.
|
|
87
|
+
extract = await getReportExtract(timeStamp, jobID);
|
|
88
|
+
}
|
|
89
|
+
const {url, what, reportTime} = extract;
|
|
90
|
+
// If the latest report extracts were not provided, compute them.
|
|
91
|
+
if (!latestReportExtracts) {
|
|
92
|
+
latestReportExtracts = await getLatestReportExtracts();
|
|
81
93
|
}
|
|
82
|
-
//
|
|
83
|
-
const reportExtract = await getReportExtract(timeStamp, jobID);
|
|
84
|
-
const {url, what} = reportExtract;
|
|
85
|
-
const latestReportExtracts = await getLatestReportExtracts();
|
|
86
|
-
// Otherwise, i.e. if the report exists, get whether this report has been superseded.
|
|
94
|
+
// Get whether this report has been superseded.
|
|
87
95
|
const isSuperseded = latestReportExtracts
|
|
88
|
-
.every(
|
|
89
|
-
// Get the completion time from the report content, not the file system birth time,
|
|
90
|
-
// because the birth time depends on when the file was created on the file system
|
|
91
|
-
// and does not reflect the actual report completion time.
|
|
92
|
-
const {reportTime} = reportExtract;
|
|
96
|
+
.every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
|
|
93
97
|
// Get the basics about the report.
|
|
94
98
|
const basics = {
|
|
95
99
|
identifier: `${timeStamp}-${jobID}`,
|
package/apiIntegration.test.js
CHANGED
|
@@ -77,7 +77,7 @@ const request = (method, requestPath, body = null) => new Promise((resolve, reje
|
|
|
77
77
|
|
|
78
78
|
// TESTS
|
|
79
79
|
|
|
80
|
-
test('GET /api/listReports returns valid JSON through the HTTP routing layer', async () => {
|
|
80
|
+
test('GET /api/listReports returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
|
|
81
81
|
const {body} = await request('GET', '/api/listReports');
|
|
82
82
|
assert.equal(body['tool name'], 'listReports');
|
|
83
83
|
const reports = body['response content']['basics about all available reports'];
|
package/capability.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Kilotest capability manifest
|
|
2
|
+
|
|
3
|
+
## Capability
|
|
4
|
+
|
|
5
|
+
Web-page front-end quality audit. Kilotest tests public web pages for front-end quality, namely accessibility, usability, and standards conformity, and makes structured findings available for retrieval.
|
|
6
|
+
|
|
7
|
+
## Purpose
|
|
8
|
+
|
|
9
|
+
Language models and AI platforms often need accurate, page-specific data about the front-end quality of a web page to answer user questions. Rule engines, such as `axe-core`, `QualWeb`, and `WAVE`, provide such data, but with large coverage gaps. Orchestrating and reconciling numerous complementary rule engines would be difficult. Kilotest solves that problem by testing pages with an ensemble of numerous independent rule engines, making their results comparable, saving the results as JSON reports, mining the reports for facts at selectable levels of detail.
|
|
10
|
+
|
|
11
|
+
## Interfaces
|
|
12
|
+
|
|
13
|
+
- MCP server (Streamable HTTP): `https://kilotest.com/mcp`
|
|
14
|
+
- OpenAPI specification: `https://kilotest.com/openapi.yaml`
|
|
15
|
+
- HTTP API: see `https://kilotest.com/openapi.yaml` for endpoints
|
|
16
|
+
- Web UI: `https://kilotest.com/`
|
|
17
|
+
- LLM documentation: `https://kilotest.com/llms.txt` and `https://kilotest.com/llms-full.txt`
|
|
18
|
+
|
|
19
|
+
## Initial input
|
|
20
|
+
|
|
21
|
+
A request for a list of available reports.
|
|
22
|
+
|
|
23
|
+
## Initial output
|
|
24
|
+
|
|
25
|
+
A list of available reports.
|
|
26
|
+
|
|
27
|
+
## Subsequent inputs
|
|
28
|
+
|
|
29
|
+
- If there is no report about the wanted page, a request to test it, with a page description and its URL.
|
|
30
|
+
- If there is a report about the wanted page, requests for findings from the report at successive levels of detail.
|
|
31
|
+
|
|
32
|
+
## Subsequent outputs
|
|
33
|
+
|
|
34
|
+
- From a request to test a page: confirmation of receipt. Upon approval and execution, the list of available reports will include the new report.
|
|
35
|
+
- From a request for findings: Details about issues reported, related WCAG standards, what rules were violated, and which DOM elements violated them.
|
|
36
|
+
|
|
37
|
+
## Side effects
|
|
38
|
+
|
|
39
|
+
Kilotest reads the specified public web page during testing. It does not modify the target site. Testing is asynchronous and may take up to approximately one day before reports are available for newly requested pages.
|
|
40
|
+
|
|
41
|
+
## Cost
|
|
42
|
+
|
|
43
|
+
Free. No authentication is required for the web UI or the public MCP endpoint.
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT.
|
|
48
|
+
|
|
49
|
+
## Repository
|
|
50
|
+
|
|
51
|
+
`https://github.com/jrpool/kilotest`
|
package/checkinfo.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/*
|
|
2
|
+
checkinfo.js
|
|
3
|
+
Summarizes the validation scope of the lint, test, and test:smoke scripts by deriving file and test-case counts from their actual configurations, so the summary stays accurate as the codebase changes.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const {execSync} = require('child_process');
|
|
11
|
+
|
|
12
|
+
// CONSTANTS
|
|
13
|
+
|
|
14
|
+
const rootDir = __dirname;
|
|
15
|
+
const excludeDirs = new Set(['node_modules', '.git', 'coverage']);
|
|
16
|
+
|
|
17
|
+
// FUNCTIONS
|
|
18
|
+
|
|
19
|
+
// Recursively collects all files under a directory, skipping excluded directories.
|
|
20
|
+
const collectFiles = dir => {
|
|
21
|
+
const results = [];
|
|
22
|
+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
|
|
23
|
+
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
if (!excludeDirs.has(entry.name)) {
|
|
26
|
+
results.push(...collectFiles(fullPath));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
results.push(fullPath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return results;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Converts a glob pattern to a RegExp for matching relative paths.
|
|
37
|
+
// Supports * (non-segment), ** (any), and literal characters.
|
|
38
|
+
const globToRegExp = pattern => {
|
|
39
|
+
const escaped = pattern
|
|
40
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
41
|
+
.replace(/\*\*/g, '\x00')
|
|
42
|
+
.replace(/\*/g, '[^/]*')
|
|
43
|
+
.replace(/\x00/g, '.*');
|
|
44
|
+
return new RegExp('^' + escaped + '$');
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Returns whether a relative path matches any pattern in a list.
|
|
48
|
+
const matchesAny = (relPath, patterns) => patterns.some(p => globToRegExp(p).test(relPath));
|
|
49
|
+
|
|
50
|
+
// Extracts the top-level ignores array from eslint.config.mjs.
|
|
51
|
+
const getEslintIgnores = () => {
|
|
52
|
+
const content = fs.readFileSync(path.join(rootDir, 'eslint.config.mjs'), 'utf8');
|
|
53
|
+
const match = content.match(/ignores:\s*\[([\s\S]*?)\]/);
|
|
54
|
+
if (!match) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
return [...match[1].matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Parses .markdownlint-cli2.jsonc and returns its ignores array.
|
|
61
|
+
const getMarkdownlintIgnores = () => {
|
|
62
|
+
const content = fs.readFileSync(path.join(rootDir, '.markdownlint-cli2.jsonc'), 'utf8');
|
|
63
|
+
const stripped = content.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
64
|
+
const match = stripped.match(/"ignores"\s*:\s*\[([\s\S]*?)\]/);
|
|
65
|
+
if (!match) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
return [...match[1].matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Runs the Node test runner with the TAP reporter and parses the test count, so dynamically generated tests (e.g., from for-loops) are counted accurately.
|
|
72
|
+
const getTestCaseCount = () => {
|
|
73
|
+
const output = execSync(
|
|
74
|
+
'node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=100 --test-reporter=tap',
|
|
75
|
+
{cwd: rootDir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe']}
|
|
76
|
+
);
|
|
77
|
+
const match = output.match(/^# tests (\d+)$/m);
|
|
78
|
+
return match ? Number(match[1]) : 0;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// MAIN
|
|
82
|
+
|
|
83
|
+
const allFiles = collectFiles(rootDir);
|
|
84
|
+
const relFiles = allFiles.map(f => path.relative(rootDir, f));
|
|
85
|
+
|
|
86
|
+
const eslintIgnores = getEslintIgnores();
|
|
87
|
+
const markdownlintIgnores = getMarkdownlintIgnores();
|
|
88
|
+
|
|
89
|
+
// ESLint file counts by extension, excluding ignored files.
|
|
90
|
+
const eslintExts = {js: 0, json: 0, md: 0, css: 0};
|
|
91
|
+
for (const rel of relFiles) {
|
|
92
|
+
if (matchesAny(rel, eslintIgnores)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (/\.(js|mjs|cjs)$/.test(rel)) {
|
|
96
|
+
eslintExts.js++;
|
|
97
|
+
}
|
|
98
|
+
else if (/\.json$/.test(rel)) {
|
|
99
|
+
eslintExts.json++;
|
|
100
|
+
}
|
|
101
|
+
else if (/\.md$/.test(rel)) {
|
|
102
|
+
eslintExts.md++;
|
|
103
|
+
}
|
|
104
|
+
else if (/\.css$/.test(rel)) {
|
|
105
|
+
eslintExts.css++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const eslintTotal = Object.values(eslintExts).reduce((a, b) => a + b, 0);
|
|
109
|
+
|
|
110
|
+
// markdownlint file count (markdown files not ignored).
|
|
111
|
+
const markdownlintCount = relFiles.filter(
|
|
112
|
+
rel => rel.endsWith('.md') && !matchesAny(rel, markdownlintIgnores)
|
|
113
|
+
).length;
|
|
114
|
+
|
|
115
|
+
// Test file and test-case counts.
|
|
116
|
+
const testFiles = relFiles.filter(rel => /\.test\.js$/.test(rel));
|
|
117
|
+
const testCaseCount = getTestCaseCount();
|
|
118
|
+
|
|
119
|
+
// Smoke-test path counts from the routes table in index.js.
|
|
120
|
+
const {routes} = require('./index');
|
|
121
|
+
const smokeGetCount = routes.GET.length;
|
|
122
|
+
const smokePostCount = routes.POST.length;
|
|
123
|
+
const smokeTotal = smokeGetCount + smokePostCount;
|
|
124
|
+
|
|
125
|
+
// OUTPUT
|
|
126
|
+
|
|
127
|
+
const parts = [
|
|
128
|
+
['lint', [
|
|
129
|
+
`ESLint: ${eslintTotal} files (${eslintExts.js} JS, ${eslintExts.json} JSON, ${eslintExts.md} MD, ${eslintExts.css} CSS)`,
|
|
130
|
+
`markdownlint: ${markdownlintCount} files`
|
|
131
|
+
]],
|
|
132
|
+
['test', [
|
|
133
|
+
`${testFiles.length} test files, ${testCaseCount} test cases`
|
|
134
|
+
]],
|
|
135
|
+
['test:smoke', [
|
|
136
|
+
`${smokeTotal} paths (${smokeGetCount} GET, ${smokePostCount} POST)`
|
|
137
|
+
]]
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
console.log('Validation summary:');
|
|
141
|
+
for (const [name, lines] of parts) {
|
|
142
|
+
console.log(` ${name}:`);
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
console.log(` ${line}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/docs/AI-TOOLS.md
CHANGED
|
@@ -27,6 +27,7 @@ The internal features that make Kilotest a collection of tools for language mode
|
|
|
27
27
|
- A generated `openapi.yaml` file, documenting the Kilotest API, conforming to the [OpenAPI specification](https://spec.openapis.org/oas/v3.1.0).
|
|
28
28
|
- An `mcp.js` file, providing an MCP server for Kilotest.
|
|
29
29
|
- A `server.json` file, conforming to the [MCP server schema](https://modelcontextprotocol.io/), and a GitHub Actions workflow (`publish-mcp.yml`) that authenticates via GitHub OIDC and publishes that file to the official MCP Registry whenever `server.json` or `api/version.js` changes on the `main` branch.
|
|
30
|
+
- A `capability.md` file with a URL, summarizing what Kilotest tools can do.
|
|
30
31
|
- `mcp`, `mcp-server`, and `modelcontextprotocol` keywords in `package.json`, supporting discovery of the `@jrpool/kilotest` npm package by tools and humans searching the npm registry for MCP servers.
|
|
31
32
|
- An `lhm.plugin.json` file, conforming to the [LobeHub plugin manifest format](https://market.lobehub.com/s/publish-mcp/references/manifest), declaring Kilotest as a plugin in the [LobeHub MCP Market](https://lobehub.com/mcp). Publishing and updating are non-automated (`lhm plugin publish`/`update`), because LobeHub requires browser-based OAuth and does not support machine-to-machine publishing.
|
|
32
33
|
- A `sitemap.xml` file.
|
|
@@ -44,7 +45,7 @@ The external features that support the use of Kilotest as a collection of AI too
|
|
|
44
45
|
- Registration of Kilotest as an active server in the [official MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=kilotest) (`io.github.jrpool/kilotest`), maintained by the [Model Context Protocol](https://modelcontextprotocol.io/) project. The [PulseMCP](https://www.pulsemcp.com/servers) directory and the VS Code MCP gallery both crawl this registry, so Kilotest is discoverable through them with no separate submission.
|
|
45
46
|
- Registration of Kilotest with the [Smithery](https://smithery.ai/servers/pool/kilotest) MCP server registry.
|
|
46
47
|
- Registration of Kilotest with the [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest) MCP server registry.
|
|
47
|
-
-
|
|
48
|
+
- Registration of Kilotest with [Awesome MCP Servers](https://www.tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36).
|
|
48
49
|
- Registration of Kilotest with the [RapidAPI](https://rapidapi.com/jrpool/api/kilotest/playground/apiendpoint_0f03577a-ff9a-472a-a0ed-533bd198981a) Hub.
|
|
49
50
|
- Registration of Kilotest with the [LobeHub MCP Market](https://lobehub.com/mcp) (identifier: `jrpool-kilotest`), enabled with an `lhm.plugin.json` file.
|
|
50
51
|
- Deployment of an MCP server in HTTP mode on the Kilotest service host.
|
|
@@ -99,3 +100,32 @@ Other future work:
|
|
|
99
100
|
|
|
100
101
|
- Add to QAI, or replace much of the current QAI content with, an [Add Custom Connector link](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Kilotest&connectorUrl=https%3A%2F%2Fkilotest.com%2Fmcp).
|
|
101
102
|
- Consider subscribing to a `Claude.ai` Team plan and submitting Kilotest to the [Claude Connectors Directory](https://claude.com/docs/connectors/building/submission). Details provided by GLM-5.2 High: “If accepted, Kilotest would get a permanent listing URL like `https://claude.ai/directory/connectors/kilotest` with a Connect button, and would appear in Claude’s in-product browse/search and ‘Suggested Connectors’. This is the highest-value listing for your use cases, because it puts Kilotest directly in front of Claude users who have not explicitly searched for it.”
|
|
103
|
+
- Recommended by GLM-5.2 High: Submit a Kilotest-appropriate request to one or more AI platforms and discover whether the model discovers Kilotest and, if so, either uses it or offers to use it in the creation of a response.
|
|
104
|
+
- Recommended by ChatGPT: A general capability manifest that says, in one machine-readable place: Here are the capabilities Kilotest provides; here are all the interfaces through which you can invoke them; here are their requirements and effects. For example:
|
|
105
|
+
|
|
106
|
+
```markdown
|
|
107
|
+
Capability: web-page-quality-audit
|
|
108
|
+
|
|
109
|
+
Purpose:
|
|
110
|
+
Test a public web page for accessibility,
|
|
111
|
+
usability, HTML and web-standards problems.
|
|
112
|
+
|
|
113
|
+
Interfaces:
|
|
114
|
+
MCP: https://kilotest.com/mcp
|
|
115
|
+
OpenAPI: https://kilotest.com/openapi.yaml
|
|
116
|
+
Web UI: https://kilotest.com/
|
|
117
|
+
|
|
118
|
+
Input:
|
|
119
|
+
Public URL
|
|
120
|
+
|
|
121
|
+
Output:
|
|
122
|
+
Structured findings, affected elements,
|
|
123
|
+
rule-engine diagnoses, WCAG mappings
|
|
124
|
+
|
|
125
|
+
Side effects:
|
|
126
|
+
Reads the specified public web page.
|
|
127
|
+
Does not modify the target site.
|
|
128
|
+
|
|
129
|
+
Cost:
|
|
130
|
+
...
|
|
131
|
+
```
|
package/docs/SERVICE.md
CHANGED
|
@@ -199,7 +199,7 @@ kilotest.com {
|
|
|
199
199
|
# Specify the only paths of forwardable requests.
|
|
200
200
|
@allowedGET {
|
|
201
201
|
method GET
|
|
202
|
-
path /mcp / /index.html /robots.txt /openapi.yaml /openapi.json /swagger.yaml /swagger.json /api-docs /llms.txt /llms-full.txt *.html* /fullReport.json/* /api/* /tutorial/images/* /favicon.* /style.css /sitemap.xml
|
|
202
|
+
path /mcp / /index.html /robots.txt /openapi.yaml /openapi.json /swagger.yaml /swagger.json /api-docs /llms.txt /llms-full.txt /capability.md *.html* /fullReport.json/* /api/* /tutorial/images/* /favicon.* /style.css /sitemap.xml
|
|
203
203
|
}
|
|
204
204
|
@allowedPOST {
|
|
205
205
|
method POST
|
package/index.html
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
<p>The rule engines perform about 1,300 tests in total. <a href="https://medium.com/cvs-health-tech-blog/how-to-run-a-thousand-accessibility-tests-63692ad120c3">Open-source software</a> developed initially at CVS Health drives the rule engines, tries to weed out invalid results, and distills the findings into about 380 <q>issues</q>.</p>
|
|
72
72
|
<h2>Features</h2>
|
|
73
73
|
<p>With Kilotest, you can get results of already performed tests and propose web pages for testing. (Immediate testing on request may become available in a future version.)</p>
|
|
74
|
+
<p>There is a <a href="/capability.md">capability document</a> that summarizes what Kilotest can do for human and AI-agent users.</p>
|
|
74
75
|
<h2>How to use Kilotest</h2>
|
|
75
76
|
<h3>If you are a human user on the web</h3>
|
|
76
77
|
<p>You can use Kilotest starting on this page.</p>
|
package/index.js
CHANGED
|
@@ -66,36 +66,37 @@ const answer = {
|
|
|
66
66
|
// Paths that the application is authorized to handle, by method, as glob-style patterns where * matches any sequence of characters.
|
|
67
67
|
const routes = exports.routes = {
|
|
68
68
|
GET: [
|
|
69
|
-
'
|
|
69
|
+
'*.html*',
|
|
70
70
|
'/',
|
|
71
|
-
'/index.html',
|
|
72
|
-
'/robots.txt',
|
|
73
|
-
'/openapi.yaml',
|
|
74
|
-
'/openapi.json',
|
|
75
|
-
'/swagger.yaml',
|
|
76
|
-
'/swagger.json',
|
|
77
71
|
'/api-docs',
|
|
78
|
-
'/
|
|
72
|
+
'/api/*',
|
|
73
|
+
'/capability.md',
|
|
74
|
+
'/favicon.*',
|
|
75
|
+
'/fullReport.json/*',
|
|
76
|
+
'/index.html',
|
|
79
77
|
'/llms-full.txt',
|
|
78
|
+
'/llms.txt',
|
|
79
|
+
'/mcp',
|
|
80
|
+
'/openapi.json',
|
|
81
|
+
'/openapi.yaml',
|
|
82
|
+
'/robots.txt',
|
|
80
83
|
'/sitemap.xml',
|
|
81
84
|
'/style.css',
|
|
82
|
-
'/
|
|
83
|
-
'/
|
|
84
|
-
'/tutorial/images/*'
|
|
85
|
-
'/favicon.*',
|
|
86
|
-
'*.html*'
|
|
85
|
+
'/swagger.json',
|
|
86
|
+
'/swagger.yaml',
|
|
87
|
+
'/tutorial/images/*'
|
|
87
88
|
],
|
|
88
89
|
POST: [
|
|
90
|
+
'/api/*',
|
|
89
91
|
'/mcp',
|
|
90
|
-
'/requestTest.html',
|
|
91
|
-
'/requestRetest.html/*',
|
|
92
|
-
'/recAction.html',
|
|
93
92
|
'/reannotate.html',
|
|
93
|
+
'/recAction.html',
|
|
94
94
|
'/renewWCAG.html',
|
|
95
|
+
'/requestRetest.html/*',
|
|
96
|
+
'/requestTest.html',
|
|
97
|
+
'/tutorialComment.html',
|
|
95
98
|
'/worker/job',
|
|
96
|
-
'/worker/report'
|
|
97
|
-
'/api/*',
|
|
98
|
-
'/tutorialComment.html'
|
|
99
|
+
'/worker/report'
|
|
99
100
|
]
|
|
100
101
|
};
|
|
101
102
|
const protocol = process.env.PROTOCOL || 'http';
|
|
@@ -359,6 +360,12 @@ const requestHandler = async (request, response) => {
|
|
|
359
360
|
const {pathname, search} = requestURL;
|
|
360
361
|
const pageName = pathname.split('/')[1];
|
|
361
362
|
const pathTail = pathname.split('/').slice(2).join('/');
|
|
363
|
+
// If the request is a smoke-test probe, respond perfunctorily without executing a handler.
|
|
364
|
+
if (request.headers['x-kilotest-smoke']) {
|
|
365
|
+
response.setHeader('content-type', 'application/json; charset=utf-8');
|
|
366
|
+
response.end('{}');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
362
369
|
// If the request is a GET request:
|
|
363
370
|
if (method === 'GET') {
|
|
364
371
|
// If the path is not authorized for GET requests:
|
|
@@ -412,6 +419,13 @@ const requestHandler = async (request, response) => {
|
|
|
412
419
|
setHeaders('text/plain', '/llms-full.txt', 'medium');
|
|
413
420
|
response.end(llmsfull);
|
|
414
421
|
}
|
|
422
|
+
// Otherwise, if it is for the summary of capabilities:
|
|
423
|
+
else if (pageName === 'capability.md') {
|
|
424
|
+
const capabilityDoc = await fs.readFile('capability.md', 'utf8');
|
|
425
|
+
// Serve it.
|
|
426
|
+
setHeaders('text/markdown', '/capability.md', 'medium');
|
|
427
|
+
response.end(capabilityDoc);
|
|
428
|
+
}
|
|
415
429
|
// Otherwise, if it is for the XML sitemap:
|
|
416
430
|
else if (pageName === 'sitemap.xml') {
|
|
417
431
|
const sitemap = await fs.readFile('sitemap.xml', 'utf8');
|
package/index.test.js
CHANGED
|
@@ -18,7 +18,7 @@ process.env.TESTARO_WORKERS = JSON.stringify({
|
|
|
18
18
|
|
|
19
19
|
// IMPORTS
|
|
20
20
|
|
|
21
|
-
const {test, before, after} = require('node:test');
|
|
21
|
+
const {test, before, beforeEach, after} = require('node:test');
|
|
22
22
|
const assert = require('node:assert/strict');
|
|
23
23
|
const http = require('node:http');
|
|
24
24
|
const fs = require('node:fs/promises');
|
|
@@ -35,7 +35,12 @@ const recsPath = path.join(fixtureDBDir, 'jobs', 'recs.json');
|
|
|
35
35
|
let server;
|
|
36
36
|
|
|
37
37
|
before(async () => {
|
|
38
|
-
|
|
38
|
+
server = http.createServer(requestHandler);
|
|
39
|
+
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Restore recs.json and clean job directories before each test, so tests do not depend on execution order.
|
|
43
|
+
beforeEach(async () => {
|
|
39
44
|
await fs.writeFile(recsPath, '{}\n');
|
|
40
45
|
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
41
46
|
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
@@ -45,8 +50,6 @@ before(async () => {
|
|
|
45
50
|
await fs.unlink(path.join(dir, file)).catch(() => {});
|
|
46
51
|
}
|
|
47
52
|
}
|
|
48
|
-
server = http.createServer(requestHandler);
|
|
49
|
-
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
after(async () => {
|
|
@@ -144,6 +147,26 @@ test('routes has GET and POST arrays', () => {
|
|
|
144
147
|
assert.ok(Array.isArray(routes.POST));
|
|
145
148
|
});
|
|
146
149
|
|
|
150
|
+
// TESTS: smoke-test short-circuit
|
|
151
|
+
|
|
152
|
+
test('GET with x-kilotest-smoke header returns a perfunctory 200', async () => {
|
|
153
|
+
const res = await request('GET', '/api/listReports', null, {'x-kilotest-smoke': '1'});
|
|
154
|
+
assert.equal(res.statusCode, 200);
|
|
155
|
+
assert.equal(res.body, '{}');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('POST with x-kilotest-smoke header returns a perfunctory 200', async () => {
|
|
159
|
+
const res = await request('POST', '/api/requestFeature', {}, {'x-kilotest-smoke': '1'});
|
|
160
|
+
assert.equal(res.statusCode, 200);
|
|
161
|
+
assert.equal(res.body, '{}');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('GET without x-kilotest-smoke header executes the handler', async () => {
|
|
165
|
+
const res = await request('GET', '/api/listReports');
|
|
166
|
+
assert.equal(res.statusCode, 200);
|
|
167
|
+
assert.notEqual(res.body, '{}');
|
|
168
|
+
});
|
|
169
|
+
|
|
147
170
|
// TESTS: GET routes
|
|
148
171
|
|
|
149
172
|
test('GET / serves the home page as HTML', async () => {
|
|
@@ -211,6 +234,13 @@ test('GET /llms-full.txt serves the LLM detailed guide as text', async () => {
|
|
|
211
234
|
assert.ok(res.body.length > 0);
|
|
212
235
|
});
|
|
213
236
|
|
|
237
|
+
test('GET /capability.md serves the capability manifest as markdown', async () => {
|
|
238
|
+
const res = await request('GET', '/capability.md');
|
|
239
|
+
assert.equal(res.statusCode, 200);
|
|
240
|
+
assert.ok(res.headers['content-type'].includes('text/markdown'));
|
|
241
|
+
assert.ok(res.body.length > 0);
|
|
242
|
+
});
|
|
243
|
+
|
|
214
244
|
test('GET /sitemap.xml serves the sitemap as XML', async () => {
|
|
215
245
|
const res = await request('GET', '/sitemap.xml');
|
|
216
246
|
assert.equal(res.statusCode, 200);
|
|
@@ -251,7 +281,7 @@ test('GET /api/listViolators/linkNoText/260101T0000/mix returns JSON', async ()
|
|
|
251
281
|
assert.equal(body['tool name'], 'listViolators');
|
|
252
282
|
});
|
|
253
283
|
|
|
254
|
-
test('GET /api/listDiagnoses/0/linkNoText/260101T0000/mix returns JSON', async () => {
|
|
284
|
+
test('GET /api/listDiagnoses/0/linkNoText/260101T0000/mix returns JSON', {timeout: 500}, async () => {
|
|
255
285
|
const res = await request('GET', '/api/listDiagnoses/0/linkNoText/260101T0000/mix');
|
|
256
286
|
assert.equal(res.statusCode, 200);
|
|
257
287
|
const body = jsonBody(res);
|
|
@@ -265,7 +295,7 @@ test('GET /api/getReport/260101T0000/mix returns JSON with the report', async ()
|
|
|
265
295
|
assert.equal(body['tool name'], 'getReport');
|
|
266
296
|
});
|
|
267
297
|
|
|
268
|
-
test('GET /api/invalidService returns an error', async () => {
|
|
298
|
+
test('GET /api/invalidService returns an error', {timeout: 500}, async () => {
|
|
269
299
|
const res = await request('GET', '/api/invalidService');
|
|
270
300
|
assert.equal(res.statusCode, 400);
|
|
271
301
|
const body = jsonBody(res);
|
|
@@ -281,7 +311,7 @@ test('GET /fullReport.json/260101T0000/mix downloads the report as JSON', async
|
|
|
281
311
|
assert.ok(body.target);
|
|
282
312
|
});
|
|
283
313
|
|
|
284
|
-
test('GET /fullReport.json/invalid/invalid returns an error page', async () => {
|
|
314
|
+
test('GET /fullReport.json/invalid/invalid returns an error page', {timeout: 500}, async () => {
|
|
285
315
|
const res = await request('GET', '/fullReport.json/invalid/invalid');
|
|
286
316
|
assert.equal(res.statusCode, 400);
|
|
287
317
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
@@ -382,7 +412,7 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
382
412
|
assert.ok(res.body.includes('Invalid retest recommendation'));
|
|
383
413
|
});
|
|
384
414
|
|
|
385
|
-
test('POST /api/requestTest with valid JSON returns a JSON response', async () => {
|
|
415
|
+
test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
386
416
|
const res = await request('POST', '/api/requestTest', {
|
|
387
417
|
description: `API Test Page ${uniqueStamp}`,
|
|
388
418
|
URL: `https://example.com/api-test-${uniqueStamp}`,
|
|
@@ -502,7 +532,7 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
502
532
|
|
|
503
533
|
// TESTS: recAction.html
|
|
504
534
|
|
|
505
|
-
test('POST /recAction.html with invalid auth code returns an error', async () => {
|
|
535
|
+
test('POST /recAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
|
|
506
536
|
const res = await formRequest('POST', '/recAction.html', {
|
|
507
537
|
target: 'https://example.com\tTest Page',
|
|
508
538
|
authCode: 'wrong-code'
|
|
@@ -542,7 +572,7 @@ test('GET /fullReport.json/260101T0007/hid returns an abuse error for a hidden r
|
|
|
542
572
|
assert.ok(res.body.includes('Invalid request'));
|
|
543
573
|
});
|
|
544
574
|
|
|
545
|
-
test('POST /recAction.html with valid auth code and approval returns HTML', async () => {
|
|
575
|
+
test('POST /recAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
|
|
546
576
|
await fs.writeFile(recsPath, '{}\n');
|
|
547
577
|
await formRequest('POST', '/requestTest.html', {
|
|
548
578
|
what: `Approval Test Page ${uniqueStamp}`,
|
|
@@ -619,7 +649,7 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
619
649
|
}
|
|
620
650
|
});
|
|
621
651
|
|
|
622
|
-
test('POST /tutorialComment.html with empty content returns a JSON error', async () => {
|
|
652
|
+
test('POST /tutorialComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
623
653
|
const res = await request('POST', '/tutorialComment.html', {
|
|
624
654
|
content: ''
|
|
625
655
|
});
|
|
@@ -691,7 +721,7 @@ test('POST /worker/job with no colon in decoded credentials returns 401', async
|
|
|
691
721
|
|
|
692
722
|
// TESTS: processJobRequest branches
|
|
693
723
|
|
|
694
|
-
test('POST /worker/job with a claimed job assigned to the worker returns an error and reclassifies the job', async () => {
|
|
724
|
+
test('POST /worker/job with a claimed job assigned to the worker returns an error and reclassifies the job', {timeout: 500}, async () => {
|
|
695
725
|
// Create a claimed job assigned to Worker One.
|
|
696
726
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
697
727
|
const failedDir = path.join(fixtureDBDir, 'jobs', 'failed');
|
|
@@ -712,6 +742,8 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
712
742
|
// The worker should get an error about the incomplete job.
|
|
713
743
|
const body = jsonBody(res);
|
|
714
744
|
assert.ok(body.error.message.includes('has not completed job'));
|
|
745
|
+
// Wait for the async rename to complete.
|
|
746
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
715
747
|
// The job should have been moved to failed.
|
|
716
748
|
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
717
749
|
assert.ok(failedExists, 'Job should be moved to failed directory');
|
|
@@ -758,7 +790,7 @@ test('POST /worker/job with a queued job assigns it to the worker', {timeout: 50
|
|
|
758
790
|
|
|
759
791
|
// TESTS: worker/report valid submission
|
|
760
792
|
|
|
761
|
-
test('POST /worker/report with valid authentication and valid claimed job processes the report', async () => {
|
|
793
|
+
test('POST /worker/report with valid authentication and valid claimed job processes the report', {timeout: 500}, async () => {
|
|
762
794
|
// Use a unique job ID that does not conflict with existing fixtures.
|
|
763
795
|
const jobID = '990101T0000-tst';
|
|
764
796
|
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
@@ -851,6 +883,18 @@ for (const pagePath of htmlPagePaths) {
|
|
|
851
883
|
});
|
|
852
884
|
}
|
|
853
885
|
|
|
886
|
+
test('GET /enqueueForm.html shows recommendations when recs.json has entries', async () => {
|
|
887
|
+
await fs.writeFile(recsPath, JSON.stringify({
|
|
888
|
+
'https://example.com/enqueue-test': [
|
|
889
|
+
{what: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
|
|
890
|
+
]
|
|
891
|
+
}));
|
|
892
|
+
const res = await request('GET', '/enqueueForm.html');
|
|
893
|
+
assert.equal(res.statusCode, 200);
|
|
894
|
+
assert.ok(res.body.includes('https://example.com/enqueue-test'));
|
|
895
|
+
assert.ok(res.body.includes('Enqueue Test Page'));
|
|
896
|
+
});
|
|
897
|
+
|
|
854
898
|
test('GET /listRules.html returns an error page when called without arguments', async () => {
|
|
855
899
|
const res = await request('GET', '/listRules.html');
|
|
856
900
|
assert.equal(res.statusCode, 400);
|
|
@@ -891,7 +935,7 @@ test('POST /mcp returns a response from the MCP handler', async () => {
|
|
|
891
935
|
|
|
892
936
|
// TESTS: answer error branches
|
|
893
937
|
|
|
894
|
-
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', async () => {
|
|
938
|
+
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
895
939
|
await fs.writeFile(recsPath, '{}\n');
|
|
896
940
|
// First request to create the recommendation.
|
|
897
941
|
await formRequest('POST', '/requestTest.html', {
|
|
@@ -909,7 +953,7 @@ test('POST /requestTest.html with valid format but duplicate URL returns an answ
|
|
|
909
953
|
assert.ok(res.body.includes('Duplicate recommendation'));
|
|
910
954
|
});
|
|
911
955
|
|
|
912
|
-
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', async () => {
|
|
956
|
+
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
913
957
|
await fs.writeFile(recsPath, '{}\n');
|
|
914
958
|
// First retest to create the recommendation.
|
|
915
959
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
@@ -923,7 +967,7 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
923
967
|
assert.ok(res.body.includes('Duplicate recommendation'));
|
|
924
968
|
});
|
|
925
969
|
|
|
926
|
-
test('POST /recAction.html with valid auth code and approval of a duplicate returns an error', async () => {
|
|
970
|
+
test('POST /recAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
|
|
927
971
|
await fs.writeFile(recsPath, '{}\n');
|
|
928
972
|
// Create a recommendation.
|
|
929
973
|
await formRequest('POST', '/requestTest.html', {
|
|
@@ -1026,12 +1070,21 @@ test('getAbuseError uses unknown IP when no forwarding header or remote address'
|
|
|
1026
1070
|
|
|
1027
1071
|
// UNIT TESTS FOR startServer
|
|
1028
1072
|
|
|
1029
|
-
test('startServer starts an HTTP server when protocol is http', async () => {
|
|
1073
|
+
test('startServer starts an HTTP server when protocol is http', {timeout: 500}, async () => {
|
|
1030
1074
|
// Delete PROTOCOL to exercise the 'http' fallback in startServer.
|
|
1031
1075
|
const savedProtocol = process.env.PROTOCOL;
|
|
1076
|
+
// Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
|
|
1077
|
+
const savedPort = process.env.PORT;
|
|
1078
|
+
process.env.PORT = '0';
|
|
1032
1079
|
delete process.env.PROTOCOL;
|
|
1033
1080
|
const server = await startServer();
|
|
1034
1081
|
process.env.PROTOCOL = savedProtocol;
|
|
1082
|
+
if (savedPort !== undefined) {
|
|
1083
|
+
process.env.PORT = savedPort;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
delete process.env.PORT;
|
|
1087
|
+
}
|
|
1035
1088
|
try {
|
|
1036
1089
|
assert.ok(server);
|
|
1037
1090
|
assert.equal(typeof server.listen, 'function');
|
|
@@ -1054,7 +1107,57 @@ test('startServer starts an HTTP server when protocol is http', async () => {
|
|
|
1054
1107
|
}
|
|
1055
1108
|
});
|
|
1056
1109
|
|
|
1057
|
-
test('
|
|
1110
|
+
test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, async () => {
|
|
1111
|
+
// Exercise the '|| 3000' fallback branch in serve by deleting PORT and calling
|
|
1112
|
+
// serve directly. The server will attempt to bind to port 3000. On a system
|
|
1113
|
+
// where port 3000 is free, it succeeds and we close it. Where it is occupied,
|
|
1114
|
+
// the error event fires and we accept that as proof the fallback was reached.
|
|
1115
|
+
const savedPort = process.env.PORT;
|
|
1116
|
+
delete process.env.PORT;
|
|
1117
|
+
try {
|
|
1118
|
+
const server = await indexModule.serve(http, {});
|
|
1119
|
+
let bindError = null;
|
|
1120
|
+
server.on('error', error => {
|
|
1121
|
+
bindError = error;
|
|
1122
|
+
});
|
|
1123
|
+
// Wait briefly for either successful binding or an EADDRINUSE error.
|
|
1124
|
+
await new Promise(resolve => {
|
|
1125
|
+
const timer = setTimeout(resolve, 200);
|
|
1126
|
+
server.on('listening', () => {
|
|
1127
|
+
clearTimeout(timer);
|
|
1128
|
+
resolve();
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
if (bindError) {
|
|
1132
|
+
assert.equal(bindError.code, 'EADDRINUSE', `Expected EADDRINUSE, got: ${bindError.message}`);
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
const address = server.address();
|
|
1136
|
+
assert.ok(typeof address === 'object' && address.port === 3000);
|
|
1137
|
+
}
|
|
1138
|
+
server.closeAllConnections?.();
|
|
1139
|
+
await new Promise(resolve => {
|
|
1140
|
+
const timer = setTimeout(() => {
|
|
1141
|
+
server.closeAllConnections?.();
|
|
1142
|
+
resolve();
|
|
1143
|
+
}, 1000);
|
|
1144
|
+
server.close(() => {
|
|
1145
|
+
clearTimeout(timer);
|
|
1146
|
+
resolve();
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
finally {
|
|
1151
|
+
if (savedPort !== undefined) {
|
|
1152
|
+
process.env.PORT = savedPort;
|
|
1153
|
+
}
|
|
1154
|
+
else {
|
|
1155
|
+
delete process.env.PORT;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
|
|
1058
1161
|
// Generate a self-signed certificate for the test using openssl.
|
|
1059
1162
|
const {execSync} = require('node:child_process');
|
|
1060
1163
|
const os = require('node:os');
|
|
@@ -1070,6 +1173,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 500}
|
|
|
1070
1173
|
const savedProtocol = process.env.PROTOCOL;
|
|
1071
1174
|
const savedKey = process.env.KEY;
|
|
1072
1175
|
const savedCert = process.env.CERT;
|
|
1176
|
+
// Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
|
|
1177
|
+
const savedPort = process.env.PORT;
|
|
1178
|
+
process.env.PORT = '0';
|
|
1073
1179
|
process.env.PROTOCOL = 'https';
|
|
1074
1180
|
process.env.KEY = keyPath;
|
|
1075
1181
|
process.env.CERT = certPath;
|
|
@@ -1110,6 +1216,12 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 500}
|
|
|
1110
1216
|
else {
|
|
1111
1217
|
delete process.env.CERT;
|
|
1112
1218
|
}
|
|
1219
|
+
if (savedPort !== undefined) {
|
|
1220
|
+
process.env.PORT = savedPort;
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
delete process.env.PORT;
|
|
1224
|
+
}
|
|
1113
1225
|
fsSync.rmSync(tmpDir, {recursive: true, force: true});
|
|
1114
1226
|
}
|
|
1115
1227
|
});
|
package/llms-full.txt
CHANGED
|
@@ -172,6 +172,7 @@ More information about Kilotest features and internals:
|
|
|
172
172
|
- [Tutorial, “Accessibility testing strategies”](https://kilotest.com/tutorial.html)
|
|
173
173
|
- [Tutorial on authorizing language models to use Kilotest](https://kilotest.com/qai/)
|
|
174
174
|
- [README file](https://github.com/jrpool/kilotest/blob/main/README.md)
|
|
175
|
+
- [Summary of Kilotest capabilities](https://kilotest.com/capability.md)
|
|
175
176
|
- [Testaro rule-engine orchestrator](https://github.com/YRA-Tech/testaro)
|
|
176
177
|
- [Testaro rule classifier](https://github.com/YRA-Tech/testaro-issues)
|
|
177
178
|
- [Research report on Testaro](https://arxiv.org/abs/2309.10167)
|
package/llms.txt
CHANGED
|
@@ -33,6 +33,7 @@ Kilotest helps language models and AI platforms to give thorough, accurate and t
|
|
|
33
33
|
## More documents
|
|
34
34
|
|
|
35
35
|
- [Detailed explanation of Kilotest and its capabilities for LLMs](https://kilotest.com/llms-full.txt)
|
|
36
|
+
- [Summary of Kilotest capabilities](https://kilotest.com/capability.md)
|
|
36
37
|
- [OpenAPI specification for Kilotest](https://kilotest.com/openapi.yaml)
|
|
37
38
|
- [Kilotest as connector to and collection of AI Tools](https://github.com/jrpool/kilotest/blob/main/docs/AI-TOOLS.md)
|
|
38
39
|
- [Tutorial on authorizing language models to use Kilotest](https://kilotest.com/qai/)
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jrpool/kilotest",
|
|
3
|
-
"version": "46.
|
|
3
|
+
"version": "46.1.2",
|
|
4
4
|
"description": "Ensemble testing for front-end web quality",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"engines": {
|
|
7
|
-
"node": ">=
|
|
7
|
+
"node": ">=24.16.0"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node index",
|
|
11
|
-
"lint": "npx eslint && npx markdownlint-cli2",
|
|
12
|
-
"test": "c8 node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=
|
|
11
|
+
"lint": "npx eslint && out=$(npx markdownlint-cli2 2>&1); code=$?; if [ $code -ne 0 ]; then echo \"$out\"; exit $code; fi",
|
|
12
|
+
"test": "c8 node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=100 --test-randomize",
|
|
13
13
|
"test:smoke": "node smokeTest",
|
|
14
|
+
"checkinfo": "node checkinfo",
|
|
14
15
|
"generate:openapi": "node scripts/generate-openapi"
|
|
15
16
|
},
|
|
16
17
|
"repository": {
|
package/smokeTest.js
CHANGED
|
@@ -14,50 +14,53 @@ const host = process.env.SMOKE_HOST || 'kilotest.com';
|
|
|
14
14
|
// Concrete paths matching each wildcard pattern in the routes table, for smoke testing.
|
|
15
15
|
const concretePaths = {
|
|
16
16
|
GET: {
|
|
17
|
-
'
|
|
17
|
+
'*.html*': '/listReports.html',
|
|
18
18
|
'/': '/',
|
|
19
|
-
'/index.html': '/index.html',
|
|
20
|
-
'/robots.txt': '/robots.txt',
|
|
21
|
-
'/openapi.yaml': '/openapi.yaml',
|
|
22
|
-
'/openapi.json': '/openapi.json',
|
|
23
|
-
'/swagger.yaml': '/swagger.yaml',
|
|
24
|
-
'/swagger.json': '/swagger.json',
|
|
25
19
|
'/api-docs': '/api-docs',
|
|
26
|
-
'/
|
|
20
|
+
'/api/*': '/api/listReports',
|
|
21
|
+
'/capability.md': '/capability.md',
|
|
22
|
+
'/favicon.*': '/favicon.ico',
|
|
23
|
+
'/fullReport.json/*': '/fullReport.json/260101T0000/mix',
|
|
24
|
+
'/index.html': '/index.html',
|
|
27
25
|
'/llms-full.txt': '/llms-full.txt',
|
|
26
|
+
'/llms.txt': '/llms.txt',
|
|
27
|
+
'/mcp': '/mcp',
|
|
28
|
+
'/openapi.json': '/openapi.json',
|
|
29
|
+
'/openapi.yaml': '/openapi.yaml',
|
|
30
|
+
'/robots.txt': '/robots.txt',
|
|
28
31
|
'/sitemap.xml': '/sitemap.xml',
|
|
29
32
|
'/style.css': '/style.css',
|
|
30
|
-
'/
|
|
31
|
-
'/
|
|
32
|
-
'/tutorial/images/*': '/tutorial/images/diagram.png'
|
|
33
|
-
'/favicon.*': '/favicon.ico',
|
|
34
|
-
'*.html*': '/listReports.html'
|
|
33
|
+
'/swagger.json': '/swagger.json',
|
|
34
|
+
'/swagger.yaml': '/swagger.yaml',
|
|
35
|
+
'/tutorial/images/*': '/tutorial/images/diagram.png'
|
|
35
36
|
},
|
|
36
37
|
POST: {
|
|
38
|
+
'/api/*': '/api/requestFeature',
|
|
37
39
|
'/mcp': '/mcp',
|
|
38
|
-
'/requestTest.html': '/requestTest.html',
|
|
39
|
-
'/requestRetest.html/*': '/requestRetest.html/260101T0001/ct',
|
|
40
|
-
'/recAction.html': '/recAction.html',
|
|
41
40
|
'/reannotate.html': '/reannotate.html',
|
|
41
|
+
'/recAction.html': '/recAction.html',
|
|
42
42
|
'/renewWCAG.html': '/renewWCAG.html',
|
|
43
|
+
'/requestRetest.html/*': '/requestRetest.html/260101T0001/ct',
|
|
44
|
+
'/requestTest.html': '/requestTest.html',
|
|
45
|
+
'/tutorialComment.html': '/tutorialComment.html',
|
|
43
46
|
'/worker/job': '/worker/job',
|
|
44
|
-
'/worker/report': '/worker/report'
|
|
45
|
-
'/api/*': '/api/requestFeature',
|
|
46
|
-
'/tutorialComment.html': '/tutorialComment.html'
|
|
47
|
+
'/worker/report': '/worker/report'
|
|
47
48
|
}
|
|
48
49
|
};
|
|
49
50
|
// Minimal POST bodies for paths that require them.
|
|
50
51
|
const postBodies = {
|
|
51
52
|
'/api/requestFeature': {feature: 'smoke test'},
|
|
52
|
-
'/
|
|
53
|
-
'/requestRetest.html/260101T0001/ct': {why: 'smoke test'},
|
|
54
|
-
'/recAction.html': {target: 'https://smoketest.example.com\tSmoke Test Page', authCode: 'invalid'},
|
|
53
|
+
'/mcp': {},
|
|
55
54
|
'/reannotate.html': {authCode: 'invalid'},
|
|
55
|
+
'/recAction.html': {
|
|
56
|
+
target: 'https://smoketest.example.com\tSmoke Test Page', authCode: 'invalid'
|
|
57
|
+
},
|
|
56
58
|
'/renewWCAG.html': {authCode: 'invalid'},
|
|
57
|
-
'/
|
|
58
|
-
'/
|
|
59
|
+
'/requestRetest.html/260101T0001/ct': {why: 'smoke test'},
|
|
60
|
+
'/requestTest.html': {what: 'Smoke Test Page', url: 'https://smoketest.example.com', why: 'smoke test'},
|
|
59
61
|
'/tutorialComment.html': {content: 'smoke test'},
|
|
60
|
-
'/
|
|
62
|
+
'/worker/job': {},
|
|
63
|
+
'/worker/report': {}
|
|
61
64
|
};
|
|
62
65
|
|
|
63
66
|
// FUNCTIONS
|
|
@@ -65,14 +68,16 @@ const postBodies = {
|
|
|
65
68
|
// Sends an HTTPS request and returns the status code and body length.
|
|
66
69
|
const sendRequest = (method, requestPath) => new Promise((resolve, reject) => {
|
|
67
70
|
const body = method === 'POST' ? JSON.stringify(postBodies[requestPath] || {}) : null;
|
|
71
|
+
const headers = {'x-kilotest-smoke': '1'};
|
|
72
|
+
if (body) {
|
|
73
|
+
headers['content-type'] = 'application/json; charset=utf-8';
|
|
74
|
+
headers['content-length'] = Buffer.byteLength(body);
|
|
75
|
+
}
|
|
68
76
|
const options = {
|
|
69
77
|
method,
|
|
70
78
|
host,
|
|
71
79
|
path: requestPath,
|
|
72
|
-
headers
|
|
73
|
-
'content-type': 'application/json; charset=utf-8',
|
|
74
|
-
'content-length': Buffer.byteLength(body)
|
|
75
|
-
} : {}
|
|
80
|
+
headers
|
|
76
81
|
};
|
|
77
82
|
const req = https.request(options, response => {
|
|
78
83
|
const chunks = [];
|
package/util.js
CHANGED
|
@@ -673,12 +673,14 @@ exports.isHidden = async (timeStamp, jobID) => {
|
|
|
673
673
|
// Returns an extract of an available report, or an error object if it cannot be read or parsed.
|
|
674
674
|
const getReportExtract = exports.getReportExtract = async (timeStamp, jobID) => {
|
|
675
675
|
try {
|
|
676
|
+
// Get the report.
|
|
676
677
|
const reportJSON = await fs.readFile(
|
|
677
678
|
path.join(reportsPath(), `${timeStamp}-${jobID}.json`), 'utf8'
|
|
678
679
|
);
|
|
679
680
|
const report = JSON.parse(reportJSON);
|
|
680
681
|
const {target, jobData} = report;
|
|
681
682
|
const {what, url} = target;
|
|
683
|
+
// Return an extract of it.
|
|
682
684
|
return {
|
|
683
685
|
timeStamp,
|
|
684
686
|
jobID,
|
|
@@ -695,12 +697,17 @@ const getReportExtract = exports.getReportExtract = async (timeStamp, jobID) =>
|
|
|
695
697
|
};
|
|
696
698
|
// Returns extracts of all available reports.
|
|
697
699
|
const getReportExtracts = exports.getReportExtracts = async () => {
|
|
700
|
+
// Get the names of the available report files.
|
|
698
701
|
const reportFileNames = await fs.readdir(reportsPath());
|
|
702
|
+
// Initialize an array of extracts.
|
|
699
703
|
const extracts = [];
|
|
704
|
+
// For each one:
|
|
700
705
|
for (const reportFileName of reportFileNames) {
|
|
701
706
|
const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
|
|
707
|
+
// Get an extract of it.
|
|
702
708
|
const extract = await getReportExtract(timeStamp, jobID);
|
|
703
709
|
if (!extract.error) {
|
|
710
|
+
// Add the extract to the array.
|
|
704
711
|
extracts.push(extract);
|
|
705
712
|
}
|
|
706
713
|
}
|
|
@@ -715,11 +722,15 @@ exports.isReportAvailable = async (what, url) => {
|
|
|
715
722
|
};
|
|
716
723
|
// Gets extracts of the latest available reports for all page descriptions.
|
|
717
724
|
exports.getLatestReportExtracts = async () => {
|
|
725
|
+
// Get extracts of all available reports.
|
|
718
726
|
const reportExtracts = await getReportExtracts();
|
|
727
|
+
// Sort them by page description and, secondarily, completion time.
|
|
719
728
|
objectSort(reportExtracts, 'reportTime', 'alpha');
|
|
720
729
|
objectSort(reportExtracts, 'what', 'alpha');
|
|
730
|
+
// Get those that are not superseded.
|
|
721
731
|
const latestReportExtracts = reportExtracts
|
|
722
732
|
.filter((extract, index) => extract.what !== reportExtracts[index + 1]?.what);
|
|
733
|
+
// Return them.
|
|
723
734
|
return latestReportExtracts;
|
|
724
735
|
};
|
|
725
736
|
// Gets the descriptions of multi-report pages.
|
package/util.test.js
CHANGED
|
@@ -398,7 +398,7 @@ test('processTestRequest returns an error for a mismatched directory name', asyn
|
|
|
398
398
|
assert.equal(result.message, 'Invalid recommendation');
|
|
399
399
|
});
|
|
400
400
|
|
|
401
|
-
test('processTestRequest succeeds and populates the template for a valid request', async () => {
|
|
401
|
+
test('processTestRequest succeeds and populates the template for a valid request', {timeout: 500}, async () => {
|
|
402
402
|
// Reset recs.json to empty before the test.
|
|
403
403
|
await fs.writeFile(recsPath(), '{}\n');
|
|
404
404
|
const result = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
|
|
@@ -409,7 +409,7 @@ test('processTestRequest succeeds and populates the template for a valid request
|
|
|
409
409
|
await fs.writeFile(recsPath(), '{}\n');
|
|
410
410
|
});
|
|
411
411
|
|
|
412
|
-
test('processTestRequest returns a duplicate error for a repeated request', async () => {
|
|
412
|
+
test('processTestRequest returns a duplicate error for a repeated request', {timeout: 500}, async () => {
|
|
413
413
|
// Reset recs.json to empty, then make a successful request.
|
|
414
414
|
await fs.writeFile(recsPath(), '{}\n');
|
|
415
415
|
await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
|
|
@@ -88,7 +88,7 @@ test('listReports includes a link to request testing a new page', async () => {
|
|
|
88
88
|
assert.ok(testLink);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
test('listReports shows recommendations when recs.json has entries', async () => {
|
|
91
|
+
test('listReports shows recommendations when recs.json has entries', {timeout: 500}, async () => {
|
|
92
92
|
const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
|
|
93
93
|
const recsPath = path.join(dbDir, 'jobs', 'recs.json');
|
|
94
94
|
const originalRecs = await fs.readFile(recsPath, 'utf8');
|
|
@@ -87,7 +87,7 @@ test('answer returns an error when no reports are available', async () => {
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test('answer returns an error when a report fails annotation', async () => {
|
|
90
|
+
test('answer returns an error when a report fails annotation', {timeout: 2000}, async () => {
|
|
91
91
|
// Back up all fixture reports, because annotateReport modifies good reports
|
|
92
92
|
// in place before it reaches the bad one and returns an error.
|
|
93
93
|
const backups = await backupReports();
|
|
@@ -122,7 +122,7 @@ test('answer returns an error when a report fails annotation', async () => {
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
test('answer returns ok with an answer page when all reports annotate successfully', async () => {
|
|
125
|
+
test('answer returns ok with an answer page when all reports annotate successfully', {timeout: 2000}, async () => {
|
|
126
126
|
// Back up all fixture reports, because annotateReport modifies them in place.
|
|
127
127
|
const backups = await backupReports();
|
|
128
128
|
try {
|
|
@@ -61,7 +61,7 @@ after(async () => {
|
|
|
61
61
|
|
|
62
62
|
// TESTS
|
|
63
63
|
|
|
64
|
-
test('answer returns ok with a populated answer page for a valid test request', async () => {
|
|
64
|
+
test('answer returns ok with a populated answer page for a valid test request', {timeout: 500}, async () => {
|
|
65
65
|
// Require the module after DB_DIR is set.
|
|
66
66
|
const {answer} = require('./index');
|
|
67
67
|
const result = await answer(
|
|
@@ -81,7 +81,7 @@ test('answer returns ok with a populated answer page for a valid test request',
|
|
|
81
81
|
assert.ok(h1.textContent.includes('Test Page'));
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
test('answer returns an error when the URL is already queued', async () => {
|
|
84
|
+
test('answer returns an error when the URL is already queued', {timeout: 500}, async () => {
|
|
85
85
|
const {answer} = require('./index');
|
|
86
86
|
// Create a queued job with the target URL.
|
|
87
87
|
const queueDir = path.join(fixtureDBDir, 'jobs', 'queue');
|
|
@@ -107,7 +107,7 @@ test('answer returns an error when the URL is already queued', async () => {
|
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
test('answer returns an error for an invalid recommendation', async () => {
|
|
110
|
+
test('answer returns an error for an invalid recommendation', {timeout: 500}, async () => {
|
|
111
111
|
const {answer} = require('./index');
|
|
112
112
|
const result = await answer('', 'not-a-url', 'why');
|
|
113
113
|
assert.equal(result.status, 'error');
|
|
@@ -72,7 +72,7 @@ test('rewindReportsForm deletes a latest superseding report with valid auth code
|
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
test('rewindReportsForm returns an error when a report file is corrupt', async () => {
|
|
75
|
+
test('rewindReportsForm returns an error when a report file is corrupt', {timeout: 500}, async () => {
|
|
76
76
|
const reportPath = path.join(reportsPath(), '260101T0001-ct.json');
|
|
77
77
|
const backup = await fs.readFile(reportPath, 'utf8');
|
|
78
78
|
try {
|
|
@@ -59,7 +59,7 @@ test('handleComment returns an error for content that is empty after sanitizatio
|
|
|
59
59
|
assert.equal(result.message, 'Comment is empty after sanitization');
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
test('handleComment saves a sanitized comment and returns ok', async () => {
|
|
62
|
+
test('handleComment saves a sanitized comment and returns ok', {timeout: 500}, async () => {
|
|
63
63
|
// Replace comments with an empty array for a clean test.
|
|
64
64
|
await fs.writeFile(commentsPath, '[]\n');
|
|
65
65
|
const result = await handleComment('This is a <b>test</b> comment.');
|
|
@@ -70,7 +70,7 @@ test('handleComment saves a sanitized comment and returns ok', async () => {
|
|
|
70
70
|
assert.ok(comments[0].timeStamp);
|
|
71
71
|
});
|
|
72
72
|
|
|
73
|
-
test('handleComment strips HTML tags and control characters', async () => {
|
|
73
|
+
test('handleComment strips HTML tags and control characters', {timeout: 500}, async () => {
|
|
74
74
|
await fs.writeFile(commentsPath, '[]\n');
|
|
75
75
|
const result = await handleComment('<img src=x onerror=alert(1)>\x00\x07Hello');
|
|
76
76
|
assert.equal(result.status, 'ok');
|
|
@@ -78,7 +78,7 @@ test('handleComment strips HTML tags and control characters', async () => {
|
|
|
78
78
|
assert.equal(comments[0].content, 'Hello');
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
test('handleComment truncates content to 500 characters', async () => {
|
|
81
|
+
test('handleComment truncates content to 500 characters', {timeout: 500}, async () => {
|
|
82
82
|
await fs.writeFile(commentsPath, '[]\n');
|
|
83
83
|
const longComment = 'x'.repeat(600);
|
|
84
84
|
const result = await handleComment(longComment);
|
|
@@ -87,7 +87,7 @@ test('handleComment truncates content to 500 characters', async () => {
|
|
|
87
87
|
assert.equal(comments[0].content.length, 500);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
-
test('handleComment creates comments.json when it does not exist', async () => {
|
|
90
|
+
test('handleComment creates comments.json when it does not exist', {timeout: 500}, async () => {
|
|
91
91
|
await fs.unlink(commentsPath).catch(() => {});
|
|
92
92
|
const result = await handleComment('Test comment for missing file');
|
|
93
93
|
assert.equal(result.status, 'ok');
|