@jrpool/kilotest 46.0.14 → 46.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.devin/config.local.json +21 -1
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish-mcp.yml +1 -1
- package/README.md +2 -0
- 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 +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 +27 -19
- package/index.test.js +41 -17
- package/llms-full.txt +1 -0
- package/llms.txt +1 -0
- package/package.json +5 -4
- package/smokeTest.js +28 -25
- 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,27 @@
|
|
|
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)"
|
|
13
33
|
]
|
|
14
34
|
}
|
|
15
35
|
}
|
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
|
|
@@ -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.',
|
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';
|
|
@@ -412,6 +413,13 @@ const requestHandler = async (request, response) => {
|
|
|
412
413
|
setHeaders('text/plain', '/llms-full.txt', 'medium');
|
|
413
414
|
response.end(llmsfull);
|
|
414
415
|
}
|
|
416
|
+
// Otherwise, if it is for the summary of capabilities:
|
|
417
|
+
else if (pageName === 'capability.md') {
|
|
418
|
+
const capabilityDoc = await fs.readFile('capability.md', 'utf8');
|
|
419
|
+
// Serve it.
|
|
420
|
+
setHeaders('text/markdown', '/capability.md', 'medium');
|
|
421
|
+
response.end(capabilityDoc);
|
|
422
|
+
}
|
|
415
423
|
// Otherwise, if it is for the XML sitemap:
|
|
416
424
|
else if (pageName === 'sitemap.xml') {
|
|
417
425
|
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 () => {
|
|
@@ -211,6 +214,13 @@ test('GET /llms-full.txt serves the LLM detailed guide as text', async () => {
|
|
|
211
214
|
assert.ok(res.body.length > 0);
|
|
212
215
|
});
|
|
213
216
|
|
|
217
|
+
test('GET /capability.md serves the capability manifest as markdown', async () => {
|
|
218
|
+
const res = await request('GET', '/capability.md');
|
|
219
|
+
assert.equal(res.statusCode, 200);
|
|
220
|
+
assert.ok(res.headers['content-type'].includes('text/markdown'));
|
|
221
|
+
assert.ok(res.body.length > 0);
|
|
222
|
+
});
|
|
223
|
+
|
|
214
224
|
test('GET /sitemap.xml serves the sitemap as XML', async () => {
|
|
215
225
|
const res = await request('GET', '/sitemap.xml');
|
|
216
226
|
assert.equal(res.statusCode, 200);
|
|
@@ -251,7 +261,7 @@ test('GET /api/listViolators/linkNoText/260101T0000/mix returns JSON', async ()
|
|
|
251
261
|
assert.equal(body['tool name'], 'listViolators');
|
|
252
262
|
});
|
|
253
263
|
|
|
254
|
-
test('GET /api/listDiagnoses/0/linkNoText/260101T0000/mix returns JSON', async () => {
|
|
264
|
+
test('GET /api/listDiagnoses/0/linkNoText/260101T0000/mix returns JSON', {timeout: 500}, async () => {
|
|
255
265
|
const res = await request('GET', '/api/listDiagnoses/0/linkNoText/260101T0000/mix');
|
|
256
266
|
assert.equal(res.statusCode, 200);
|
|
257
267
|
const body = jsonBody(res);
|
|
@@ -265,7 +275,7 @@ test('GET /api/getReport/260101T0000/mix returns JSON with the report', async ()
|
|
|
265
275
|
assert.equal(body['tool name'], 'getReport');
|
|
266
276
|
});
|
|
267
277
|
|
|
268
|
-
test('GET /api/invalidService returns an error', async () => {
|
|
278
|
+
test('GET /api/invalidService returns an error', {timeout: 500}, async () => {
|
|
269
279
|
const res = await request('GET', '/api/invalidService');
|
|
270
280
|
assert.equal(res.statusCode, 400);
|
|
271
281
|
const body = jsonBody(res);
|
|
@@ -281,7 +291,7 @@ test('GET /fullReport.json/260101T0000/mix downloads the report as JSON', async
|
|
|
281
291
|
assert.ok(body.target);
|
|
282
292
|
});
|
|
283
293
|
|
|
284
|
-
test('GET /fullReport.json/invalid/invalid returns an error page', async () => {
|
|
294
|
+
test('GET /fullReport.json/invalid/invalid returns an error page', {timeout: 500}, async () => {
|
|
285
295
|
const res = await request('GET', '/fullReport.json/invalid/invalid');
|
|
286
296
|
assert.equal(res.statusCode, 400);
|
|
287
297
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
@@ -382,7 +392,7 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
382
392
|
assert.ok(res.body.includes('Invalid retest recommendation'));
|
|
383
393
|
});
|
|
384
394
|
|
|
385
|
-
test('POST /api/requestTest with valid JSON returns a JSON response', async () => {
|
|
395
|
+
test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
386
396
|
const res = await request('POST', '/api/requestTest', {
|
|
387
397
|
description: `API Test Page ${uniqueStamp}`,
|
|
388
398
|
URL: `https://example.com/api-test-${uniqueStamp}`,
|
|
@@ -502,7 +512,7 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
502
512
|
|
|
503
513
|
// TESTS: recAction.html
|
|
504
514
|
|
|
505
|
-
test('POST /recAction.html with invalid auth code returns an error', async () => {
|
|
515
|
+
test('POST /recAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
|
|
506
516
|
const res = await formRequest('POST', '/recAction.html', {
|
|
507
517
|
target: 'https://example.com\tTest Page',
|
|
508
518
|
authCode: 'wrong-code'
|
|
@@ -542,7 +552,7 @@ test('GET /fullReport.json/260101T0007/hid returns an abuse error for a hidden r
|
|
|
542
552
|
assert.ok(res.body.includes('Invalid request'));
|
|
543
553
|
});
|
|
544
554
|
|
|
545
|
-
test('POST /recAction.html with valid auth code and approval returns HTML', async () => {
|
|
555
|
+
test('POST /recAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
|
|
546
556
|
await fs.writeFile(recsPath, '{}\n');
|
|
547
557
|
await formRequest('POST', '/requestTest.html', {
|
|
548
558
|
what: `Approval Test Page ${uniqueStamp}`,
|
|
@@ -619,7 +629,7 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
619
629
|
}
|
|
620
630
|
});
|
|
621
631
|
|
|
622
|
-
test('POST /tutorialComment.html with empty content returns a JSON error', async () => {
|
|
632
|
+
test('POST /tutorialComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
623
633
|
const res = await request('POST', '/tutorialComment.html', {
|
|
624
634
|
content: ''
|
|
625
635
|
});
|
|
@@ -691,7 +701,7 @@ test('POST /worker/job with no colon in decoded credentials returns 401', async
|
|
|
691
701
|
|
|
692
702
|
// TESTS: processJobRequest branches
|
|
693
703
|
|
|
694
|
-
test('POST /worker/job with a claimed job assigned to the worker returns an error and reclassifies the job', async () => {
|
|
704
|
+
test('POST /worker/job with a claimed job assigned to the worker returns an error and reclassifies the job', {timeout: 500}, async () => {
|
|
695
705
|
// Create a claimed job assigned to Worker One.
|
|
696
706
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
697
707
|
const failedDir = path.join(fixtureDBDir, 'jobs', 'failed');
|
|
@@ -712,6 +722,8 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
712
722
|
// The worker should get an error about the incomplete job.
|
|
713
723
|
const body = jsonBody(res);
|
|
714
724
|
assert.ok(body.error.message.includes('has not completed job'));
|
|
725
|
+
// Wait for the async rename to complete.
|
|
726
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
715
727
|
// The job should have been moved to failed.
|
|
716
728
|
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
717
729
|
assert.ok(failedExists, 'Job should be moved to failed directory');
|
|
@@ -758,7 +770,7 @@ test('POST /worker/job with a queued job assigns it to the worker', {timeout: 50
|
|
|
758
770
|
|
|
759
771
|
// TESTS: worker/report valid submission
|
|
760
772
|
|
|
761
|
-
test('POST /worker/report with valid authentication and valid claimed job processes the report', async () => {
|
|
773
|
+
test('POST /worker/report with valid authentication and valid claimed job processes the report', {timeout: 500}, async () => {
|
|
762
774
|
// Use a unique job ID that does not conflict with existing fixtures.
|
|
763
775
|
const jobID = '990101T0000-tst';
|
|
764
776
|
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
@@ -851,6 +863,18 @@ for (const pagePath of htmlPagePaths) {
|
|
|
851
863
|
});
|
|
852
864
|
}
|
|
853
865
|
|
|
866
|
+
test('GET /enqueueForm.html shows recommendations when recs.json has entries', async () => {
|
|
867
|
+
await fs.writeFile(recsPath, JSON.stringify({
|
|
868
|
+
'https://example.com/enqueue-test': [
|
|
869
|
+
{what: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
|
|
870
|
+
]
|
|
871
|
+
}));
|
|
872
|
+
const res = await request('GET', '/enqueueForm.html');
|
|
873
|
+
assert.equal(res.statusCode, 200);
|
|
874
|
+
assert.ok(res.body.includes('https://example.com/enqueue-test'));
|
|
875
|
+
assert.ok(res.body.includes('Enqueue Test Page'));
|
|
876
|
+
});
|
|
877
|
+
|
|
854
878
|
test('GET /listRules.html returns an error page when called without arguments', async () => {
|
|
855
879
|
const res = await request('GET', '/listRules.html');
|
|
856
880
|
assert.equal(res.statusCode, 400);
|
|
@@ -891,7 +915,7 @@ test('POST /mcp returns a response from the MCP handler', async () => {
|
|
|
891
915
|
|
|
892
916
|
// TESTS: answer error branches
|
|
893
917
|
|
|
894
|
-
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', async () => {
|
|
918
|
+
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
895
919
|
await fs.writeFile(recsPath, '{}\n');
|
|
896
920
|
// First request to create the recommendation.
|
|
897
921
|
await formRequest('POST', '/requestTest.html', {
|
|
@@ -909,7 +933,7 @@ test('POST /requestTest.html with valid format but duplicate URL returns an answ
|
|
|
909
933
|
assert.ok(res.body.includes('Duplicate recommendation'));
|
|
910
934
|
});
|
|
911
935
|
|
|
912
|
-
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', async () => {
|
|
936
|
+
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
913
937
|
await fs.writeFile(recsPath, '{}\n');
|
|
914
938
|
// First retest to create the recommendation.
|
|
915
939
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
@@ -923,7 +947,7 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
923
947
|
assert.ok(res.body.includes('Duplicate recommendation'));
|
|
924
948
|
});
|
|
925
949
|
|
|
926
|
-
test('POST /recAction.html with valid auth code and approval of a duplicate returns an error', async () => {
|
|
950
|
+
test('POST /recAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
|
|
927
951
|
await fs.writeFile(recsPath, '{}\n');
|
|
928
952
|
// Create a recommendation.
|
|
929
953
|
await formRequest('POST', '/requestTest.html', {
|
|
@@ -1054,7 +1078,7 @@ test('startServer starts an HTTP server when protocol is http', async () => {
|
|
|
1054
1078
|
}
|
|
1055
1079
|
});
|
|
1056
1080
|
|
|
1057
|
-
test('startServer starts an HTTPS server when protocol is https', {timeout:
|
|
1081
|
+
test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
|
|
1058
1082
|
// Generate a self-signed certificate for the test using openssl.
|
|
1059
1083
|
const {execSync} = require('node:child_process');
|
|
1060
1084
|
const os = require('node:os');
|
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.0
|
|
3
|
+
"version": "46.1.0",
|
|
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
|
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');
|