@jrpool/kilotest 46.0.8 → 46.0.14

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.
Files changed (60) hide show
  1. package/.devin/config.local.json +4 -1
  2. package/.github/workflows/{smoke-test.yml → ci.yml} +8 -2
  3. package/README.md +14 -8
  4. package/alerts.test.js +137 -0
  5. package/api/listDiagnoses.test.js +44 -0
  6. package/api/listIssues.js +1 -1
  7. package/api/listIssues.test.js +10 -0
  8. package/api/listReports.test.js +6 -2
  9. package/api/listViolators.test.js +35 -0
  10. package/api/requestTest.test.js +7 -0
  11. package/api/routes.test.js +63 -0
  12. package/api/util.js +9 -11
  13. package/api/util.test.js +68 -2
  14. package/apiIntegration.test.js +13 -3
  15. package/docs/AI-TOOLS.md +41 -158
  16. package/docs/{Smoke Test.json → CI.json} +4 -3
  17. package/index.html +3 -3
  18. package/index.js +64 -77
  19. package/index.test.js +1152 -0
  20. package/indexBalances.test.js +188 -0
  21. package/indexFallback.test.js +33 -0
  22. package/indexServe.test.js +59 -0
  23. package/indexWorkers.test.js +79 -0
  24. package/lhm.plugin.json +3218 -0
  25. package/llms-full.txt +6 -7
  26. package/llms.txt +1 -1
  27. package/mcp.js +2 -2
  28. package/mcp.test.js +281 -0
  29. package/openapi.yaml +1 -1
  30. package/package.json +10 -5
  31. package/routing.test.js +13 -3
  32. package/scripts/generate-openapi.js +1 -1
  33. package/test/fixtures/buildFixtures.js +83 -0
  34. package/test/fixtures/db/reports/260101T0008-mul.json +137 -0
  35. package/test/fixtures/db/reports/260101T0009-brd.json +147 -0
  36. package/test/setup.cjs +14 -0
  37. package/util.js +16 -6
  38. package/util.test.js +886 -3
  39. package/web/ai0BalanceForm/ai0BalanceForm.test.js +88 -0
  40. package/web/expungeReportsForm/expungeReportsForm.test.js +114 -0
  41. package/web/hideReportForm/hideReportForm.test.js +82 -0
  42. package/web/listDiagnoses/listDiagnoses.test.js +160 -0
  43. package/web/listIssues/listIssues.test.js +109 -0
  44. package/web/listReports/listReports.test.js +139 -3
  45. package/web/listRules/listRules.test.js +103 -0
  46. package/web/listTopIssues/listTopIssues.test.js +126 -0
  47. package/web/listViolators/index.js +1 -2
  48. package/web/listViolators/listViolators.test.js +256 -0
  49. package/web/pruneReportsForm/pruneReportsForm.test.js +86 -0
  50. package/web/reannotate/reannotate.test.js +144 -0
  51. package/web/reannotateForm/reannotateForm.test.js +134 -0
  52. package/web/renewWCAG/renewWCAG.test.js +124 -0
  53. package/web/requestRetest/requestRetest.test.js +119 -0
  54. package/web/requestRetestForm/requestRetestForm.test.js +62 -0
  55. package/web/requestTest/requestTest.test.js +115 -0
  56. package/web/requestTestForm/requestTestForm.test.js +40 -0
  57. package/web/rewindReportsForm/rewindReportsForm.test.js +86 -0
  58. package/web/tutorial/index.html +3 -3
  59. package/web/tutorial/tutorial.test.js +97 -0
  60. package/web/unhideReportForm/unhideReportForm.test.js +135 -0
package/llms-full.txt CHANGED
@@ -22,10 +22,10 @@ To provide a more inexpensive, thorough, and accurate assessment, the agent coul
22
22
 
23
23
  The tool that tests web pages for front-end quality has attributes that no other such tool has. It:
24
24
 
25
- - selects an ensemble of 10 rule engines that implement tests for, in total, more than a thousand rules for front-end quality
25
+ - selects an ensemble of 12 rule engines that implement tests for, in total, more than a thousand rules for front-end quality
26
26
  - runs the tests of the rule engines
27
27
  - combines the reports of the rule engines into a single integrated report
28
- - consolidates the 1000+ rules having 10 different naming systems into 300+ “issues” with a uniform naming system
28
+ - consolidates the 1000+ rules having 12 different naming systems into 300+ “issues” with a uniform naming system
29
29
 
30
30
  The tools that report test results extract from reports statistics and details at various levels of granularity, as required by an agent to fulfill particular user requests.
31
31
 
@@ -46,9 +46,9 @@ The rule engines in the Kilotest ensemble take responsibility for determining wh
46
46
 
47
47
  ## Kilotest versus other MCP servers
48
48
 
49
- In the marketplace for MCP servers that test for front-end quality, all servers other than that of Kilotest leverage at most 1 rule engine, most often `axe-core`. The Kilotest testing tool leverages `axe-core` plus 9 other rule engines. This has these main effects:
49
+ In the marketplace for MCP servers that test for front-end quality, all servers other than that of Kilotest leverage at most 1 rule engine, most often `axe-core`. The Kilotest testing tool leverages `axe-core` plus 11 other rule engines. This has these main effects:
50
50
 
51
- - For web pages that have not yet been tested, a single-rule-engine MCP can provide faster results than Kilotest. A tool running only the `axe-core` tests can complete its work in about 5 seconds or less. The testing tool of Kilotest usually completes its work in 2 to 3 minutes, because all 10 rule engines are run, and some of the tests involve navigation and interaction with the page and LLM inference. Moreover, Kilotest currently allows users (both humans and models) to **recommend** new pages for testing but authorizes only Kilotest managers to make the tool proceed with the testing. The wait for manager action can take up to a day. A feature permitting immediate testing ordered by AI agents is planned, but, until it is implemented, Kilotest will be useful for not-yet-tested pages only in long-running (multi-day) workflows.
51
+ - For web pages that have not yet been tested, a single-rule-engine MCP can provide faster results than Kilotest. A tool running only the `axe-core` tests can complete its work in about 5 seconds or less. The testing tool of Kilotest usually completes its work in 2 to 3 minutes, because all 12 rule engines are run, and some of the tests involve navigation and interaction with the page and LLM inference. Moreover, Kilotest currently allows users (both humans and models) to **recommend** new pages for testing but authorizes only Kilotest managers to make the tool proceed with the testing. The wait for manager action can take up to a day. A feature permitting immediate testing ordered by AI agents is planned, but, until it is implemented, Kilotest will be useful for not-yet-tested pages only in long-running (multi-day) workflows.
52
52
  - For web pages about which reports are already available, Kilotest can provide faster results than a single-rule-engine MCP server, because Kilotest stores test results for subsequent retrieval. A retrieval from a Kilotest reporting tool can be completed in less than 2 seconds.
53
53
  - Kilotest results are more comprehensive than single-rule-engine MCP server results. Every rule engine provides limited coverage of front-end quality, so false negatives (missed defects) are more common with single-rule-engine MCP servers. This difference [has been documented in research](https://arxiv.org/pdf/2304.07591).
54
54
 
@@ -165,7 +165,6 @@ More information about Kilotest features and internals:
165
165
 
166
166
  - [Kilotest as connector to and collection of AI Tools](https://github.com/jrpool/kilotest/blob/main/docs/AI-TOOLS.md)
167
167
  - [OpenAPI specification for Kilotest](https://kilotest.com/openapi.yaml)
168
- - [Tool caller for testing](https://github.com/jrpool/kilotest/blob/main/api/testSuite.js)
169
168
 
170
169
  ### General
171
170
 
@@ -173,7 +172,7 @@ More information about Kilotest features and internals:
173
172
  - [Tutorial, “Accessibility testing strategies”](https://kilotest.com/tutorial.html)
174
173
  - [Tutorial on authorizing language models to use Kilotest](https://kilotest.com/qai/)
175
174
  - [README file](https://github.com/jrpool/kilotest/blob/main/README.md)
176
- - [Testaro rule-engine orchestrator](https://github.com/jrpool/testaro)
177
- - [Testaro rule classifier](https://github.com/jrpool/testaro-issues)
175
+ - [Testaro rule-engine orchestrator](https://github.com/YRA-Tech/testaro)
176
+ - [Testaro rule classifier](https://github.com/YRA-Tech/testaro-issues)
178
177
  - [Research report on Testaro](https://arxiv.org/abs/2309.10167)
179
178
  - [Blog post on the Kilotest use case](https://medium.com/cvs-health-tech-blog/how-to-run-a-thousand-accessibility-tests-63692ad120c3)
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Kilotest
2
2
 
3
- Kilotest helps language models and AI platforms to give thorough, accurate and truthful answers when users ask about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. Kilotest integrates an ensemble often independent rule engines to test any public web page. Kilotest creates a structured report of the results and saves the report for future reference. Kilotest API endpoints and MCP tools can provide multipage, single-page, and element-level test results.
3
+ Kilotest helps language models and AI platforms to give thorough, accurate and truthful answers when users ask about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. Kilotest integrates an ensemble of twelve independent rule engines to test any public web page. Kilotest creates a structured report of the results and saves the report for future reference. Kilotest API endpoints and MCP tools can provide multipage, single-page, and element-level test results.
4
4
 
5
5
  ## API
6
6
 
package/mcp.js CHANGED
@@ -44,7 +44,7 @@ exports.mcpPath = '/mcp';
44
44
  // FUNCTIONS
45
45
 
46
46
  // Creates and returns an McpServer with Kilotest tools registered.
47
- const createMCPServer = () => {
47
+ exports.createMCPServer = () => {
48
48
  const server = new McpServer({
49
49
  name: 'Kilotest',
50
50
  version,
@@ -210,7 +210,7 @@ const createMCPServer = () => {
210
210
  // Handles an MCP request.
211
211
  exports.handleMCP = async (request, response) => {
212
212
  const transport = new StreamableHTTPServerTransport({sessionIdGenerator: undefined});
213
- const server = createMCPServer();
213
+ const server = exports.createMCPServer();
214
214
  await server.connect(transport);
215
215
  await transport.handleRequest(request, response);
216
216
  };
package/mcp.test.js ADDED
@@ -0,0 +1,281 @@
1
+ /*
2
+ mcp.test.js
3
+ Tests for mcp.js, covering tool registration and handler behavior.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {test} = require('node:test');
9
+ const assert = require('node:assert/strict');
10
+ const path = require('node:path');
11
+ const http = require('node:http');
12
+ const {mcpPath, createMCPServer} = require('./mcp');
13
+
14
+ // CONSTANTS
15
+
16
+ // Set DB_DIR to the fixture database for all tests.
17
+ process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
18
+
19
+ // TESTS
20
+
21
+ test('mcpPath is /mcp', () => {
22
+ assert.equal(mcpPath, '/mcp');
23
+ });
24
+
25
+ test('createMCPServer registers all 8 tools', () => {
26
+ const server = createMCPServer();
27
+ const toolNames = Object.keys(server._registeredTools);
28
+ assert.equal(toolNames.length, 8);
29
+ assert.deepEqual(toolNames, [
30
+ 'listReports',
31
+ 'listIssues',
32
+ 'listViolators',
33
+ 'listDiagnoses',
34
+ 'getReport',
35
+ 'requestTest',
36
+ 'requestRetest',
37
+ 'requestFeature'
38
+ ]);
39
+ });
40
+
41
+ test('each tool has a description and a handler function', () => {
42
+ const server = createMCPServer();
43
+ const tools = server._registeredTools;
44
+ for (const [name, tool] of Object.entries(tools)) {
45
+ assert.ok(tool.description, `${name} has a description`);
46
+ assert.equal(typeof tool.handler, 'function', `${name} has a handler function`);
47
+ }
48
+ });
49
+
50
+ test('listReports handler returns content and structuredContent', async () => {
51
+ const server = createMCPServer();
52
+ const result = await server._registeredTools.listReports.handler({});
53
+ assert.ok(result.content);
54
+ assert.equal(result.content[0].type, 'text');
55
+ assert.ok(result.structuredContent);
56
+ });
57
+
58
+ test('listIssues handler returns content and structuredContent for a valid report', async () => {
59
+ const server = createMCPServer();
60
+ const result = await server._registeredTools.listIssues.handler({
61
+ timeStamp: '260101T0000',
62
+ jobID: 'mix'
63
+ });
64
+ assert.ok(result.content);
65
+ assert.equal(result.content[0].type, 'text');
66
+ assert.ok(result.structuredContent);
67
+ });
68
+
69
+ test('listViolators handler returns content and structuredContent for a valid issue', async () => {
70
+ const server = createMCPServer();
71
+ const result = await server._registeredTools.listViolators.handler({
72
+ issueID: 'linkNoText',
73
+ timeStamp: '260101T0000',
74
+ jobID: 'mix'
75
+ });
76
+ assert.ok(result.content);
77
+ assert.equal(result.content[0].type, 'text');
78
+ assert.ok(result.structuredContent);
79
+ });
80
+
81
+ test('listDiagnoses handler returns content and structuredContent for a valid diagnosis', async () => {
82
+ const server = createMCPServer();
83
+ const result = await server._registeredTools.listDiagnoses.handler({
84
+ catalogIndex: '0',
85
+ issueID: 'linkNoText',
86
+ timeStamp: '260101T0000',
87
+ jobID: 'mix'
88
+ });
89
+ assert.ok(result.content);
90
+ assert.equal(result.content[0].type, 'text');
91
+ assert.ok(result.structuredContent);
92
+ });
93
+
94
+ test('getReport handler returns content and structuredContent for a valid report', async () => {
95
+ const server = createMCPServer();
96
+ const result = await server._registeredTools.getReport.handler({
97
+ timeStamp: '260101T0000',
98
+ jobID: 'mix'
99
+ });
100
+ assert.ok(result.content);
101
+ assert.equal(result.content[0].type, 'text');
102
+ assert.ok(result.structuredContent);
103
+ });
104
+
105
+ test('requestTest handler returns content and structuredContent', async () => {
106
+ const server = createMCPServer();
107
+ const result = await server._registeredTools.requestTest.handler({
108
+ description: 'Test Page',
109
+ URL: 'https://example.com/test',
110
+ reason: 'Because accessibility matters'
111
+ });
112
+ assert.ok(result.content);
113
+ assert.equal(result.content[0].type, 'text');
114
+ assert.ok(result.structuredContent);
115
+ });
116
+
117
+ test('requestRetest handler returns content and structuredContent for a valid report', async () => {
118
+ const server = createMCPServer();
119
+ const result = await server._registeredTools.requestRetest.handler({
120
+ timeStamp: '260101T0000',
121
+ jobID: 'mix',
122
+ reason: 'Because the report is obsolete'
123
+ });
124
+ assert.ok(result.content);
125
+ assert.equal(result.content[0].type, 'text');
126
+ assert.ok(result.structuredContent);
127
+ });
128
+
129
+ test('requestFeature handler returns content and structuredContent', async () => {
130
+ const server = createMCPServer();
131
+ const result = await server._registeredTools.requestFeature.handler({
132
+ feature: 'A new feature idea'
133
+ });
134
+ assert.ok(result.content);
135
+ assert.equal(result.content[0].type, 'text');
136
+ assert.ok(result.structuredContent);
137
+ });
138
+
139
+ test('listIssues handler returns an error for a nonexistent report', async () => {
140
+ const server = createMCPServer();
141
+ const result = await server._registeredTools.listIssues.handler({
142
+ timeStamp: '990101T0000',
143
+ jobID: 'xxx'
144
+ });
145
+ const reportBasics = result.structuredContent['response content']['basics about the report'];
146
+ assert.ok(reportBasics.error);
147
+ });
148
+
149
+ test('listViolators handler returns an error for an unknown issue', async () => {
150
+ const server = createMCPServer();
151
+ const result = await server._registeredTools.listViolators.handler({
152
+ issueID: 'nonexistentIssue',
153
+ timeStamp: '260101T0000',
154
+ jobID: 'mix'
155
+ });
156
+ const issueBasics = result.structuredContent['response content']['basics about the issue'];
157
+ assert.ok(issueBasics.error);
158
+ });
159
+
160
+ // INTEGRATION TESTS FOR handleMCP
161
+
162
+ // Helper: sends a single MCP JSON-RPC request to a local server and returns the parsed SSE response.
163
+ const sendMCPRequest = (port, method, params, id) => new Promise((resolve, reject) => {
164
+ const body = JSON.stringify({jsonrpc: '2.0', method, params, id});
165
+ const req = http.request({
166
+ port,
167
+ method: 'POST',
168
+ path: '/mcp',
169
+ headers: {
170
+ 'content-type': 'application/json',
171
+ 'accept': 'application/json, text/event-stream',
172
+ 'content-length': Buffer.byteLength(body)
173
+ }
174
+ }, res => {
175
+ let data = '';
176
+ res.on('data', chunk => {
177
+ data += chunk;
178
+ });
179
+ res.on('end', () => {
180
+ resolve({statusCode: res.statusCode, headers: res.headers, body: data});
181
+ });
182
+ });
183
+ req.on('error', reject);
184
+ req.write(body);
185
+ req.end();
186
+ });
187
+
188
+ // Helper: parses the JSON-RPC result from an SSE response body.
189
+ const parseSSEResult = body => {
190
+ const jsonLine = body.split('\n').find(line => line.startsWith('data: '));
191
+ if (!jsonLine) {
192
+ throw new Error('No data line in SSE response');
193
+ }
194
+ return JSON.parse(jsonLine.slice(6));
195
+ };
196
+
197
+ // Helper: starts a local HTTP server with handleMCP and returns it.
198
+ const startMCPServer = () => new Promise(resolve => {
199
+ const {handleMCP} = require('./mcp');
200
+ const server = http.createServer((req, res) => handleMCP(req, res));
201
+ server.listen(0, () => resolve(server));
202
+ });
203
+
204
+ // Helper: closes a server with a timeout fallback.
205
+ const closeMCPServer = server => new Promise(resolve => {
206
+ if (!server) {
207
+ resolve();
208
+ return;
209
+ }
210
+ server.closeAllConnections?.();
211
+ const timer = setTimeout(() => {
212
+ server.closeAllConnections?.();
213
+ resolve();
214
+ }, 1000);
215
+ server.close(() => {
216
+ clearTimeout(timer);
217
+ resolve();
218
+ });
219
+ });
220
+
221
+ test('handleMCP responds to initialize with server info', async () => {
222
+ const server = await startMCPServer();
223
+ try {
224
+ const port = server.address().port;
225
+ const res = await sendMCPRequest(port, 'initialize', {
226
+ protocolVersion: '2025-06-18',
227
+ capabilities: {},
228
+ clientInfo: {name: 'test', version: '1.0'}
229
+ }, 1);
230
+ assert.equal(res.statusCode, 200);
231
+ const result = parseSSEResult(res.body);
232
+ assert.equal(result.result.serverInfo.name, 'Kilotest');
233
+ assert.equal(result.result.protocolVersion, '2025-06-18');
234
+ }
235
+ finally {
236
+ await closeMCPServer(server);
237
+ }
238
+ });
239
+
240
+ test('handleMCP lists all 8 tools via tools/list', async () => {
241
+ const server = await startMCPServer();
242
+ try {
243
+ const port = server.address().port;
244
+ const res = await sendMCPRequest(port, 'tools/list', {}, 2);
245
+ assert.equal(res.statusCode, 200);
246
+ const result = parseSSEResult(res.body);
247
+ const toolNames = result.result.tools.map(t => t.name);
248
+ assert.equal(toolNames.length, 8);
249
+ assert.deepEqual(toolNames, [
250
+ 'listReports',
251
+ 'listIssues',
252
+ 'listViolators',
253
+ 'listDiagnoses',
254
+ 'getReport',
255
+ 'requestTest',
256
+ 'requestRetest',
257
+ 'requestFeature'
258
+ ]);
259
+ }
260
+ finally {
261
+ await closeMCPServer(server);
262
+ }
263
+ });
264
+
265
+ test('handleMCP executes listReports tool via tools/call', async () => {
266
+ const server = await startMCPServer();
267
+ try {
268
+ const port = server.address().port;
269
+ const res = await sendMCPRequest(port, 'tools/call', {
270
+ name: 'listReports',
271
+ arguments: {}
272
+ }, 3);
273
+ assert.equal(res.statusCode, 200);
274
+ const result = parseSSEResult(res.body);
275
+ assert.ok(result.result.content);
276
+ assert.equal(result.result.content[0].type, 'text');
277
+ }
278
+ finally {
279
+ await closeMCPServer(server);
280
+ }
281
+ });
package/openapi.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
- description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 10 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
4
+ description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
5
  version: 2.1.0
6
6
  license:
7
7
  name: MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "46.0.8",
3
+ "version": "46.0.14",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "start": "node index",
11
11
  "lint": "npx eslint && npx markdownlint-cli2",
12
- "test": "c8 node --test",
12
+ "test": "c8 node --require ./test/setup.cjs --test --test-concurrency=1 --test-timeout=70",
13
13
  "test:smoke": "node smokeTest",
14
14
  "generate:openapi": "node scripts/generate-openapi"
15
15
  },
@@ -66,7 +66,8 @@
66
66
  "mcp.js",
67
67
  "alerts.js",
68
68
  "util.js",
69
- "api/**/*.js"
69
+ "api/**/*.js",
70
+ "web/**/*.js"
70
71
  ],
71
72
  "exclude": [
72
73
  "coverage/**",
@@ -74,8 +75,12 @@
74
75
  "test{,-*}.{js,cjs,mjs}",
75
76
  "**/*{.,-}test.{js,cjs,mjs}",
76
77
  "**/__tests__/**",
77
- "web/**",
78
78
  "scripts/**"
79
- ]
79
+ ],
80
+ "check-coverage": true,
81
+ "lines": 100,
82
+ "statements": 100,
83
+ "functions": 100,
84
+ "branches": 100
80
85
  }
81
86
  }
package/routing.test.js CHANGED
@@ -23,7 +23,7 @@ let server;
23
23
  before(async () => {
24
24
  process.env.DB_DIR = path.join(__dirname, 'test', 'fixtures', 'db');
25
25
  server = http.createServer(requestHandler);
26
- await new Promise(resolve => server.listen(port, resolve));
26
+ await new Promise(resolve => server.listen(port, () => resolve()));
27
27
  });
28
28
 
29
29
  after(async () => {
@@ -33,7 +33,17 @@ after(async () => {
33
33
  else {
34
34
  delete process.env.DB_DIR;
35
35
  }
36
- await new Promise(resolve => server.close(() => resolve()));
36
+ server.closeAllConnections?.();
37
+ await new Promise(resolve => {
38
+ const timer = setTimeout(() => {
39
+ server.closeAllConnections?.();
40
+ resolve();
41
+ }, 1000);
42
+ server.close(() => {
43
+ clearTimeout(timer);
44
+ resolve();
45
+ });
46
+ });
37
47
  });
38
48
 
39
49
  // HELPERS
@@ -73,5 +83,5 @@ test('GET /api/listReports returns a valid JSON response', async () => {
73
83
  const body = JSON.parse(result.body);
74
84
  assert.equal(body['tool name'], 'listReports');
75
85
  const reports = body['response content']['basics about all available reports'];
76
- assert.equal(reports.length, 6);
86
+ assert.equal(reports.length, 8);
77
87
  });
@@ -58,7 +58,7 @@ const generate = () => {
58
58
  openapi: '3.1.0',
59
59
  info: {
60
60
  title: 'Kilotest API',
61
- description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 10 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.',
61
+ description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.',
62
62
  version,
63
63
  license: {
64
64
  name: license,
@@ -233,6 +233,89 @@ const main = async () => {
233
233
  hiddenReport
234
234
  );
235
235
 
236
+ // Fixture 8: multiViolator. Three violators for the same issue: two with 1 reporter each (exercising the catalogIndex tiebreaker) and one with 2 reporters (exercising the reporter-count descending sort).
237
+ const multiViolatorCatalog = {
238
+ '0': catalogItem('A', 'Home', '/html/body/a[1]', '10:10:40:20'),
239
+ '1': catalogItem('A', 'Contact', '/html/body/a[2]', '10:40:40:20'),
240
+ '2': catalogItem('A', 'Help', '/html/body/a[3]', '10:70:40:20')
241
+ };
242
+ const multiViolatorActs = [
243
+ testAct(engineAxe, [
244
+ instance('r11', 'The link does not have an accessible name', 'failed',
245
+ issueLinkNoText, 0, 2, 1),
246
+ instance('r11', 'The link does not have an accessible name', 'failed',
247
+ issueLinkNoText, 2, 2, 1)
248
+ ]),
249
+ testAct(engineAlfa, [
250
+ instance('r11', 'The link does not have an accessible name', 'failed',
251
+ issueLinkNoText, 1, 2, 1),
252
+ instance('r11', 'The link does not have an accessible name', 'failed',
253
+ issueLinkNoText, 2, 2, 1)
254
+ ])
255
+ ];
256
+ await writeJSON(
257
+ path.join(reportsDir, '260101T0008-mul.json'),
258
+ report('260101T0008-mul', 'Multi Violator Page',
259
+ 'https://example.com/multiviolator', multiViolatorActs, multiViolatorCatalog)
260
+ );
261
+
262
+ // Fixture 9: branchCoverage. Covers remaining branch gaps in listDiagnoses and listIssues: an issue with a short WCAG code (guideline layer), a catalog item with no tagName or text, an instance where ruleID equals what and count is missing, a test act with null instances, and an instance with no issueID.
263
+ const issueDuplicateID = 'duplicateID';
264
+ const branchCatalog = {
265
+ '0': catalogItem('DIV', '', '/html/body/div[1]', '0:0:100:50'),
266
+ '1': {id: '', startTag: '<div>', text: null, textLinkable: false, boxID: '5:5:90:40', pathID: '/html/body/div[2]', headingIndex: '', checkpoint: 0},
267
+ '2': {id: '', text: 'No tags', textLinkable: false, headingIndex: '', checkpoint: 0}
268
+ };
269
+ const branchActs = [
270
+ testAct(engineAxe, [
271
+ instance('r99', 'r99', 'failed',
272
+ issueDuplicateID, 0, 2, 1),
273
+ {
274
+ ruleID: 'r100',
275
+ what: 'Element has no role',
276
+ ordinalSeverity: 1,
277
+ outcome: 'failed',
278
+ catalogIndex: '1',
279
+ checkpoint: 0,
280
+ issueID: issueDuplicateID
281
+ },
282
+ instance('r101', 'Duplicate ID found', 'failed',
283
+ issueDuplicateID, 2, 2, 1),
284
+ instance('r102', 'Duplicate ID in orphan', 'failed',
285
+ issueDuplicateID, 3, 2, 1)
286
+ ]),
287
+ {
288
+ type: 'test',
289
+ which: engineAlfa,
290
+ startTime: '26-01-01T00:00',
291
+ endTime: '26-01-01T00:01',
292
+ result: {
293
+ standardResult: {}
294
+ }
295
+ },
296
+ testAct(engineIbm, [
297
+ {
298
+ ruleID: 'r50',
299
+ what: 'Link has no text',
300
+ ordinalSeverity: 2,
301
+ outcome: 'failed',
302
+ catalogIndex: '0',
303
+ checkpoint: 0
304
+ }
305
+ ])
306
+ ];
307
+ await writeJSON(
308
+ path.join(reportsDir, '260101T0009-brd.json'),
309
+ report('260101T0009-brd', 'Branch Coverage Page',
310
+ 'https://example.com/branch', branchActs, branchCatalog)
311
+ );
312
+ // Remove preventions from the brd fixture to cover the preventions ?? {} branch in listIssues.
313
+ const brdReport = JSON.parse(
314
+ await fs.readFile(path.join(reportsDir, '260101T0009-brd.json'), 'utf8')
315
+ );
316
+ delete brdReport.jobData.preventions;
317
+ await writeJSON(path.join(reportsDir, '260101T0009-brd.json'), brdReport);
318
+
236
319
  // Write an empty recs.json so getRecs does not try to create one.
237
320
  await writeJSON(path.join(targetDir, 'jobs', 'recs.json'), {});
238
321
 
@@ -0,0 +1,137 @@
1
+ {
2
+ "id": "260101T0008-mul",
3
+ "what": "Multi Violator Page",
4
+ "strict": false,
5
+ "standard": "only",
6
+ "device": {
7
+ "id": "default"
8
+ },
9
+ "browserID": "chromium",
10
+ "creationTimeStamp": "260101T0008",
11
+ "executionTimeStamp": "260101T0008",
12
+ "target": {
13
+ "what": "Multi Violator Page",
14
+ "url": "https://example.com/multiviolator"
15
+ },
16
+ "sources": {
17
+ "worker": "test-worker"
18
+ },
19
+ "acts": [
20
+ {
21
+ "type": "test",
22
+ "which": "axe",
23
+ "startTime": "26-01-01T00:00",
24
+ "endTime": "26-01-01T00:01",
25
+ "result": {
26
+ "standardResult": {
27
+ "instances": [
28
+ {
29
+ "ruleID": "r11",
30
+ "what": "The link does not have an accessible name",
31
+ "ordinalSeverity": 2,
32
+ "outcome": "failed",
33
+ "count": 1,
34
+ "catalogIndex": "0",
35
+ "checkpoint": 0,
36
+ "issueID": "linkNoText"
37
+ },
38
+ {
39
+ "ruleID": "r11",
40
+ "what": "The link does not have an accessible name",
41
+ "ordinalSeverity": 2,
42
+ "outcome": "failed",
43
+ "count": 1,
44
+ "catalogIndex": "2",
45
+ "checkpoint": 0,
46
+ "issueID": "linkNoText"
47
+ }
48
+ ],
49
+ "outcomeTotals": {
50
+ "failed": 2,
51
+ "cantTell": 0
52
+ }
53
+ }
54
+ }
55
+ },
56
+ {
57
+ "type": "test",
58
+ "which": "alfa",
59
+ "startTime": "26-01-01T00:00",
60
+ "endTime": "26-01-01T00:01",
61
+ "result": {
62
+ "standardResult": {
63
+ "instances": [
64
+ {
65
+ "ruleID": "r11",
66
+ "what": "The link does not have an accessible name",
67
+ "ordinalSeverity": 2,
68
+ "outcome": "failed",
69
+ "count": 1,
70
+ "catalogIndex": "1",
71
+ "checkpoint": 0,
72
+ "issueID": "linkNoText"
73
+ },
74
+ {
75
+ "ruleID": "r11",
76
+ "what": "The link does not have an accessible name",
77
+ "ordinalSeverity": 2,
78
+ "outcome": "failed",
79
+ "count": 1,
80
+ "catalogIndex": "2",
81
+ "checkpoint": 0,
82
+ "issueID": "linkNoText"
83
+ }
84
+ ],
85
+ "outcomeTotals": {
86
+ "failed": 2,
87
+ "cantTell": 0
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ],
93
+ "jobData": {
94
+ "startTime": "26-01-01T00:00",
95
+ "endTime": "26-01-01T00:10",
96
+ "elapsedSeconds": 600,
97
+ "preventions": {},
98
+ "issuelessRules": []
99
+ },
100
+ "catalog": {
101
+ "0": {
102
+ "tagName": "A",
103
+ "id": "",
104
+ "startTag": "<a>",
105
+ "text": "Home",
106
+ "textLinkable": false,
107
+ "boxID": "10:10:40:20",
108
+ "pathID": "/html/body/a[1]",
109
+ "headingIndex": "",
110
+ "checkpoint": 0
111
+ },
112
+ "1": {
113
+ "tagName": "A",
114
+ "id": "",
115
+ "startTag": "<a>",
116
+ "text": "Contact",
117
+ "textLinkable": false,
118
+ "boxID": "10:40:40:20",
119
+ "pathID": "/html/body/a[2]",
120
+ "headingIndex": "",
121
+ "checkpoint": 0
122
+ },
123
+ "2": {
124
+ "tagName": "A",
125
+ "id": "",
126
+ "startTag": "<a>",
127
+ "text": "Help",
128
+ "textLinkable": false,
129
+ "boxID": "10:70:40:20",
130
+ "pathID": "/html/body/a[3]",
131
+ "headingIndex": "",
132
+ "checkpoint": 0
133
+ }
134
+ },
135
+ "images": {},
136
+ "checkpoints": []
137
+ }