@jrpool/kilotest 38.1.0 → 39.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.markdownlint-cli2.jsonc +12 -0
  2. package/.vscode/settings.json +3 -0
  3. package/CLAUDE.md +5 -0
  4. package/README.md +6 -4
  5. package/ai0BalanceForm/index.html +1 -1
  6. package/alerts.js +3 -1
  7. package/api/getReport.js +66 -0
  8. package/api/listDiagnoses.js +157 -0
  9. package/api/listIssues.js +225 -0
  10. package/api/listReports.js +87 -0
  11. package/api/listViolators.js +164 -0
  12. package/api/requestFeature.js +56 -0
  13. package/api/requestRetest.js +102 -0
  14. package/api/requestTest.js +106 -0
  15. package/api/routes.js +97 -0
  16. package/api/schemas.js +297 -0
  17. package/api/testSuite.js +294 -0
  18. package/api/util.js +149 -0
  19. package/diagnoses/index.js +2 -2
  20. package/docs/{AI-TOOL.md → AI-TOOLS.md} +54 -26
  21. package/docs/SERVICE.md +4 -1
  22. package/docs/decisions/001-api-design.md +59 -49
  23. package/eslint.config.mjs +13 -3
  24. package/index.html +1 -1
  25. package/index.js +202 -188
  26. package/issues/index.js +13 -21
  27. package/jsconfig.json +8 -0
  28. package/llms-full.txt +2 -2
  29. package/llms.txt +16 -11
  30. package/mcp.js +129 -65
  31. package/openapi.yaml +1234 -572
  32. package/package.json +18 -11
  33. package/reannotate/index.js +26 -15
  34. package/reannotateForm/index.js +15 -13
  35. package/redocly.yaml +4 -0
  36. package/reportHideForm/index.html +1 -1
  37. package/reportHideForm/index.js +24 -38
  38. package/reportIssue/index.js +3 -3
  39. package/reportIssues/index.js +128 -1
  40. package/reportUnhideForm/index.js +36 -44
  41. package/reportsExpungeForm/index.js +22 -16
  42. package/reportsPruneForm/index.js +22 -12
  43. package/reportsRewindForm/index.js +17 -9
  44. package/retestRec/index.js +8 -4
  45. package/retestRecForm/index.js +18 -6
  46. package/scripts/generate-openapi.js +83 -0
  47. package/targets/index.js +18 -13
  48. package/testOrder/index.js +11 -8
  49. package/tutorial/index.html +12 -12
  50. package/util.js +581 -539
  51. package/api/reportList.js +0 -83
  52. package/api/reportSummary.js +0 -181
  53. package/reportIssue/api.js +0 -183
  54. package/reportIssues/util.js +0 -138
  55. package/researchAgent.js +0 -206
  56. package/target/api.js +0 -126
  57. package/testRecForm/api.js +0 -55
package/api/schemas.js ADDED
@@ -0,0 +1,297 @@
1
+ /*
2
+ schemas.js
3
+ Zod input and response schemas shared by mcp.js (MCP tool inputSchema/outputSchema), api/routes.js, and the openapi.yaml generation script (scripts/generate-openapi.js). Descriptions and component ids (.meta({id})) in this file are the single source of truth for parameter documentation and OpenAPI component naming and reuse.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ const {z} = require('zod');
9
+
10
+ // REQUEST SCHEMAS
11
+
12
+ // listIssues: GET /api/listIssues/{timeStamp}/{jobID}
13
+ exports.listIssuesSchema = {
14
+ timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
15
+ jobID: z.string().describe('Job identifier of the report (example: x9z)')
16
+ };
17
+
18
+ // listViolators: GET /api/listViolators/{issueID}/{timeStamp}/{jobID}
19
+ exports.listViolatorsSchema = {
20
+ issueID: z.string().describe('Issue identifier (example: contrastPoor)'),
21
+ timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
22
+ jobID: z.string().describe('Job identifier of the report (example: x9z)')
23
+ };
24
+
25
+ // listDiagnoses: GET /api/listDiagnoses/{catalogIndex}/{issueID}/{timeStamp}/{jobID}
26
+ exports.listDiagnosesSchema = {
27
+ catalogIndex: z.string().describe('Identifier of the issue-exhibiting element in the catalog of elements on the page (example: 372)'),
28
+ issueID: z.string().describe('Issue identifier (example: contrastPoor)'),
29
+ timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
30
+ jobID: z.string().describe('Job identifier of the report (example: x9z)')
31
+ };
32
+
33
+ // getReport: GET /api/getReport/{timeStamp}/{jobID}
34
+ exports.getReportSchema = {
35
+ timeStamp: z.string().describe('Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'),
36
+ jobID: z.string().describe('Job identifier of the report (example: x9z)')
37
+ };
38
+
39
+ // requestTest: POST /api/requestTest
40
+ exports.requestTestSchema = {
41
+ description: z.string().describe('10- to 100-character description of the page conforming to the naming convention used in the listReports output'),
42
+ URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
43
+ reason: z.string().describe('20- to 100-character reason why the page should be tested')
44
+ };
45
+
46
+ // requestRetest: POST /api/requestRetest/{timeStamp}/{jobID}
47
+ exports.requestRetestSchema = {
48
+ timeStamp: z.string().describe('Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'),
49
+ jobID: z.string().describe('Job identifier of the latest report about the page (example: x9z)'),
50
+ reason: z.string().describe('20- to 100-character reason why the page should be retested')
51
+ };
52
+
53
+ // requestFeature: POST /api/requestFeature
54
+ exports.requestFeatureSchema = {
55
+ request: z.string().describe('description of requested new feature or feature improvement')
56
+ };
57
+
58
+ // listReports takes no input; omitted (mcp.js already uses inputSchema: {}).
59
+
60
+ // RESPONSE SCHEMAS
61
+
62
+ // Facts about a rule engine, as constructed by Kilotest's own getRuleEngineFacts (never copied from a Testaro report).
63
+ const ruleEngineFactsSchema = z.object({
64
+ identifier: z.string(),
65
+ name: z.string().nullable(),
66
+ sponsor: z.string().nullable()
67
+ }).meta({id: 'RuleEngineFacts'});
68
+
69
+ // Facts about the Kilotest tool collection, included in every response.
70
+ const toolsFactsSchema = z.object({
71
+ name: z.string(),
72
+ description: z.object({
73
+ 'what Kilotest does': z.string(),
74
+ 'how to retrieve findings': z.object({
75
+ 'level 1': z.string(),
76
+ 'level 2': z.string(),
77
+ 'level 3': z.string(),
78
+ 'level 4': z.string()
79
+ }),
80
+ 'how to generate more findings': z.object({
81
+ 'new testing': z.string(),
82
+ 'retesting': z.string(),
83
+ 'latency': z.string(),
84
+ 'confirmation': z.string()
85
+ })
86
+ }).describe('Explanation of what Kilotest does and how to use its tools.'),
87
+ URL: z.string().describe('URL of the Kilotest MCP server.'),
88
+ 'web users can obtain similar functionalities at': z.string()
89
+ }).meta({id: 'ToolsFacts'});
90
+
91
+ // Identifies a related request (e.g., the closest ancestor request in the drill-down hierarchy).
92
+ const requestReferenceSchema = z.object({
93
+ 'tool name': z.string(),
94
+ description: z.string(),
95
+ method: z.literal('GET'),
96
+ URL: z.string()
97
+ }).meta({id: 'RequestReference'});
98
+
99
+ // GET endpoints have no 'body'; POST endpoints get a request-specific body schema.
100
+ const thisRequestSchema = (method, bodySchema) => z.object({
101
+ description: z.string(),
102
+ method: z.literal(method),
103
+ URL: z.string(),
104
+ ...(bodySchema ? {body: bodySchema} : {}),
105
+ 'closest ancestor request': requestReferenceSchema.nullable()
106
+ });
107
+
108
+ const envelope = (method, responseContentSchema, bodySchema) => z.object({
109
+ 'tool collection': toolsFactsSchema,
110
+ 'tool name': z.string(),
111
+ 'this request': thisRequestSchema(method, bodySchema),
112
+ 'URLs of similar requests for web users': similarWebRequestsSchema,
113
+ 'response metadata': responseMetadataSchema,
114
+ 'response content': responseContentSchema
115
+ });
116
+
117
+ const similarWebRequestsSchema = z.object({
118
+ 'this request': z.string().describe('URL of the equivalent web UI page for this request.'),
119
+ 'closest ancestor request': z.string().nullable()
120
+ }).meta({id: 'SimilarWebRequests'});
121
+
122
+ const responseMetadataSchema = z.object({
123
+ identifier: z.string().describe('Unique identifier of this response (timestamp and random suffix).'),
124
+ 'date and time': z.string().describe('UTC date and time when the response was generated, in ISO 8601 format.')
125
+ }).meta({id: 'ResponseMetadata'});
126
+
127
+ // Facts about a report, as constructed by Kilotest's own getReportBasics (never copied from a Testaro report).
128
+ const reportBasicsSchema = z.object({
129
+ identifier: z.string().describe('timeStamp-jobID identifier of the report.'),
130
+ 'completion date and time': z.string(),
131
+ 'days since the report was completed': z.number(),
132
+ 'tested web page': z.object({description: z.string(), URL: z.string()}),
133
+ 'whether a later report about the same page exists': z.boolean()
134
+ }).meta({id: 'ReportBasics'});
135
+
136
+ const reportBasicsOrErrorSchema = z.union([
137
+ reportBasicsSchema,
138
+ z.object({error: z.string()})
139
+ ]).meta({id: 'ReportBasicsOrError'});
140
+
141
+ // Facts about an issue based on the Testilo issue classification.
142
+ const issueBasicsSchema = z.object({
143
+ identifier: z.string(),
144
+ summary: z.string(),
145
+ 'impact on a user': z.string(),
146
+ priority: z.enum(['lowest', 'low', 'high', 'highest'])
147
+ }).meta({id: 'IssueBasics'});
148
+
149
+ const issueBasicsOrErrorSchema = z.union([
150
+ issueBasicsSchema.extend({
151
+ 'related WCAG standard': z.object({
152
+ layer: z.enum(['success criterion', 'guideline']),
153
+ identifier: z.string()
154
+ })
155
+ }),
156
+ z.object({error: z.string()})
157
+ ]).meta({id: 'IssueBasicsOrError'});
158
+
159
+ exports.listReportsResponseSchema = envelope('GET', z.object({
160
+ 'basics about all available reports': z.array(reportBasicsSchema.extend({
161
+ 'how to get details about the report': z.object({method: z.literal('GET'), URL: z.string()}),
162
+ 'web users can get details about the report at': z.string()
163
+ })),
164
+ 'how to request that a page with no report be tested': z.object({
165
+ method: z.literal('POST'),
166
+ URL: z.string(),
167
+ 'request body': z.object({description: z.string(), URL: z.string(), reason: z.string()}),
168
+ 'how to check whether the request has been fulfilled': z.string()
169
+ }),
170
+ 'how a web user can request that the page be tested': z.object({URL: z.string()})
171
+ }));
172
+
173
+ exports.listIssuesResponseSchema = envelope('GET', z.object({
174
+ 'basics about the report': reportBasicsOrErrorSchema,
175
+ 'details about the report': z.object({
176
+ 'job definition': z.object({
177
+ 'whether the job prohibited redirection': z.unknown().describe('Copied from the report (strict); type not guaranteed.'),
178
+ 'whether the native results of rule engines are reported': z.boolean(),
179
+ 'whether standardized results are reported': z.boolean(),
180
+ 'device emulated by the job': z.unknown().describe('Device object copied verbatim from the report; shape may vary.'),
181
+ 'browser type used by the job': z.unknown().describe('Copied from the report (browserID); type not guaranteed.')
182
+ }),
183
+ 'test results': z.object({
184
+ 'rule engines that tried to test the page': z.array(ruleEngineFactsSchema),
185
+ 'rule engines that could not test the page': z.array(z.object({
186
+ name: z.string().nullable(),
187
+ 'reason for failure': z.unknown().describe('Copied verbatim from the Testaro report; shape may vary.')
188
+ })),
189
+ 'names of rule engines that reported rule violations': z.array(z.string().nullable()),
190
+ 'counts of issues by priority': z.object({
191
+ highest: z.number(), high: z.number(), low: z.number(), lowest: z.number()
192
+ }),
193
+ 'number of elements reported as violators': z.number()
194
+ })
195
+ }).nullable(),
196
+ 'how to request that the page be retested': z.union([
197
+ z.object({notice: z.string()}),
198
+ z.object({
199
+ method: z.literal('POST'),
200
+ URL: z.string(),
201
+ 'request body': z.object({reason: z.string()}),
202
+ 'how to check whether the request has been fulfilled': z.string()
203
+ })
204
+ ]).nullable(),
205
+ 'how a web user can request that the page be retested': z.object({URL: z.string()}).nullable(),
206
+ 'how to get the full report in JSON': z.object({
207
+ 'size of the report in bytes': z.number(),
208
+ method: z.literal('GET'),
209
+ URL: z.string()
210
+ }).nullable(),
211
+ 'how a web user can get the full report in JSON': z.object({URL: z.string()}).nullable(),
212
+ 'basics about all issues reported in the report': z.array(issueBasicsSchema.extend({
213
+ 'rule engines with any violations belonging to the issue': z.array(z.string()),
214
+ 'how to get details about the issue': z.object({method: z.literal('GET'), URL: z.string()}),
215
+ 'web users can get details about the issue at': z.string()
216
+ }))
217
+ }));
218
+
219
+ exports.listViolatorsResponseSchema = envelope('GET', z.object({
220
+ 'basics about the report': reportBasicsOrErrorSchema,
221
+ 'basics about the issue': issueBasicsOrErrorSchema,
222
+ 'details about the issue': z.object({
223
+ 'rule engines reporting violations belonging to the issue': z.array(ruleEngineFactsSchema)
224
+ }).nullable(),
225
+ 'basics about all elements reported as exhibiting the issue': z.array(z.object({
226
+ identifier: z.string(),
227
+ 'tag name': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
228
+ 'inner text': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
229
+ 'count of rule engines reporting that the element exhibited the issue': z.number(),
230
+ 'how to get details about the element': z.object({URL: z.string(), 'request method': z.literal('GET')}),
231
+ 'web users can get details about the element at': z.string()
232
+ })).nullable()
233
+ }));
234
+
235
+ exports.listDiagnosesResponseSchema = envelope('GET', z.object({
236
+ 'basics about the report': reportBasicsOrErrorSchema,
237
+ 'basics about the issue': issueBasicsOrErrorSchema,
238
+ 'basics about the element': z.object({
239
+ identifier: z.string(),
240
+ 'tag name': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
241
+ 'inner text': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.')
242
+ }).or(z.object({error: z.string()})).nullable(),
243
+ 'details about the element': z.object({
244
+ 'start tag': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
245
+ 'XPath': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.'),
246
+ 'x, y, width, and height of bounding box': z.unknown().describe('Copied from the report catalog entry; type not guaranteed.')
247
+ }).nullable(),
248
+ 'diagnoses of how the element exhibited the issue': z.array(z.object({
249
+ 'identifier of the violated rule': z.unknown().describe('Copied from the report instance (ruleID); type not guaranteed.'),
250
+ 'description of the violation': z.unknown().describe('Copied from the report instance (what); type not guaranteed.'),
251
+ 'severity of the violation on a 0-to-3 scale': z.unknown().describe('Copied from the report instance (ordinalSeverity); type not guaranteed.'),
252
+ 'count of violations of the rule by the element': z.number()
253
+ })).or(z.object({error: z.string()}))
254
+ }));
255
+
256
+ exports.getReportResponseSchema = envelope('GET', z.object({
257
+ 'size of the report in bytes': z.union([z.number(), z.string()]),
258
+ 'full report': z.unknown().describe('The full raw Testaro/Testilo report JSON, copied verbatim, or an error object if it could not be retrieved.')
259
+ }));
260
+
261
+ exports.requestTestResponseSchema = envelope(
262
+ 'POST',
263
+ z.object({'details about your request': z.union([
264
+ z.object({error: z.string()}),
265
+ z.object({
266
+ 'date and time received': z.string(),
267
+ 'page to be tested': z.object({description: z.string(), URL: z.string()}),
268
+ 'reason why the page should be tested': z.string()
269
+ })
270
+ ])}),
271
+ z.object(exports.requestTestSchema)
272
+ );
273
+
274
+ exports.requestRetestResponseSchema = envelope(
275
+ 'POST',
276
+ z.object({'details about your request': z.union([
277
+ z.object({error: z.string()}),
278
+ z.object({
279
+ 'date and time received': z.string(),
280
+ 'page to be retested': z.object({description: z.string(), URL: z.string()}),
281
+ 'reason why the page should be retested': z.string()
282
+ })
283
+ ])}),
284
+ z.object({reason: exports.requestRetestSchema.reason})
285
+ );
286
+
287
+ exports.requestFeatureResponseSchema = envelope(
288
+ 'POST',
289
+ z.object({'details about your request': z.union([
290
+ z.object({error: z.string()}),
291
+ z.object({
292
+ 'date and time received': z.string(),
293
+ disposition: z.string()
294
+ })
295
+ ])}),
296
+ z.object(exports.requestFeatureSchema)
297
+ );
@@ -0,0 +1,294 @@
1
+ /*
2
+ © 2026 Jonathan Robert Pool.
3
+
4
+ Licensed under the MIT License. See LICENSE file at the project root or
5
+ https://opensource.org/license/mit/ for details.
6
+
7
+ SPDX-License-Identifier: MIT
8
+ */
9
+
10
+ /*
11
+ researchAgent.js
12
+ Simulates a research agent.
13
+ */
14
+
15
+ // IMPORTS
16
+
17
+ require('dotenv').config();
18
+ const httpClient = require('http');
19
+ const httpsClient = require('https');
20
+
21
+ // CONSTANTS
22
+
23
+ const kilotestHosts = [
24
+ process.env.LOCAL_KILOTEST_HOST || 'http://localhost:3000',
25
+ process.env.DEPLOYED_KILOTEST_HOST || 'https://kilotest.com'
26
+ ];
27
+ // Kilotest host specified by the argument.
28
+ const kilotestHost = kilotestHosts[process.argv[2] === 'pub' ? 1 : 0];
29
+ const hostParts = kilotestHost.split(/:\/*/);
30
+ const scheme = hostParts[0] === 'https' ? 'https' : 'http';
31
+ const client = scheme === 'https' ? httpsClient : httpClient;
32
+ const host = hostParts[1] || 'localhost';
33
+ const port = hostParts[2] || (scheme === 'https' ? 443 : 80);
34
+
35
+ // FUNCTIONS
36
+
37
+ // Gets, parses into an object, and returns the body of a response.
38
+ const getBody = async response => {
39
+ const body = await new Promise(resolve => {
40
+ // Initialize an array of data from the response.
41
+ const chunks = [];
42
+ response
43
+ // If the response throws an error:
44
+ .on('error', error => {
45
+ const {message} = error;
46
+ // Report and return the error message.
47
+ console.log(message);
48
+ resolve({error: message});
49
+ })
50
+ // Whenever the response delivers data:
51
+ .on('data', chunk => {
52
+ // Add them to the array.
53
+ chunks.push(chunk);
54
+ })
55
+ // When the response is completed:
56
+ .on('end', () => {
57
+ const bodyString = chunks.join('');
58
+ try {
59
+ const body = JSON.parse(bodyString);
60
+ // Return the response body as an object.
61
+ resolve(body);
62
+ }
63
+ // If it is not JSON:
64
+ catch {
65
+ // Return this.
66
+ resolve({error: `Response body not JSON (${bodyString})`});
67
+ }
68
+ });
69
+ });
70
+ return body;
71
+ };
72
+ const getRequestOptions = (path, method = 'GET') => ({
73
+ method,
74
+ host,
75
+ port,
76
+ path,
77
+ headers: {
78
+ 'body-type': 'application/json; charset=utf-8'
79
+ }
80
+ });
81
+ // Submits a request, returns the response body, and increments the results.
82
+ const submitRequest = async (path, method, requestBody = null) => new Promise(resolve => {
83
+ console.log(`Making ${scheme} ${method} request on port ${port} of ${host} to ${path}`);
84
+ client.request(getRequestOptions(path, method), async response => {
85
+ const responseBody = await getBody(response);
86
+ resolve(responseBody);
87
+ })
88
+ .on('error', error => {
89
+ console.log(`ERROR submitting request (${JSON.stringify(error, null, 2)})`);
90
+ resolve({error});
91
+ })
92
+ .end(requestBody ? JSON.stringify(requestBody) : '');
93
+ });
94
+ // Submits requests to the specified Kilotest host.
95
+ const requestService = async () => {
96
+ let body;
97
+ let jobID;
98
+ let method;
99
+ let path;
100
+ let reportBasics;
101
+ let reportsBasics;
102
+ let requestDetails;
103
+ let requestDisposition;
104
+ let responseContent;
105
+ let timeStamp;
106
+ console.log('======================\nRequest: List all available reports');
107
+ method = 'GET';
108
+ path = '/api/listReports';
109
+ body = await submitRequest(path, method);
110
+ responseContent = body?.['response content'] ?? {};
111
+ reportsBasics = responseContent?.['basics about all available reports'] ?? [];
112
+ if (
113
+ !reportsBasics.length
114
+ || !Array.isArray(reportsBasics)
115
+ || !reportsBasics.length
116
+ || reportsBasics.some(
117
+ reportBasics => reportBasics['how to get details about the report']?.method !== 'GET'
118
+ )
119
+ ) {
120
+ console.log(`reportsBasics: ${JSON.stringify(reportsBasics, null, 2)}`);
121
+ return;
122
+ }
123
+ console.log('======================\nRequest: List issues in one nonexistent report');
124
+ [timeStamp, jobID] = ['111111T1111', 'abc'];
125
+ method = 'GET';
126
+ path = `/api/listIssues/${timeStamp}/${jobID}`;
127
+ body = await submitRequest(path, method);
128
+ responseContent = body?.['response content'] ?? {};
129
+ reportBasics = responseContent?.['basics about the report'] ?? {};
130
+ if (
131
+ !reportBasics.error || !reportBasics.error.includes('missing, unreadable, or not JSON')
132
+ ) {
133
+ console.log(`reportBasics: ${JSON.stringify(reportBasics, null, 2)}`);
134
+ return;
135
+ }
136
+ console.log('======================\nRequest: List issues in one report');
137
+ const reportsBasicsIndex = Math.floor(reportsBasics.length * Math.random());
138
+ // Choose one report at random.
139
+ reportBasics = reportsBasics[reportsBasicsIndex];
140
+ [timeStamp, jobID] = reportBasics.identifier?.split('-') || [null, null];
141
+ if (!(timeStamp && jobID)) {
142
+ console.log(`reportBasicsIndex: ${reportsBasicsIndex}`);
143
+ console.log(`reportBasics: ${JSON.stringify(reportBasics, null, 2)}`);
144
+ return;
145
+ }
146
+ path = `/api/listIssues/${timeStamp}/${jobID}`;
147
+ body = await submitRequest(path, method);
148
+ responseContent = body?.['response content'] ?? {};
149
+ const issuesBasics = responseContent?.['basics about all issues reported in the report'] ?? [];
150
+ if (
151
+ !issuesBasics.length
152
+ || !Array.isArray(issuesBasics)
153
+ || !issuesBasics.length
154
+ || issuesBasics.some(issueBasics => !issueBasics['impact on a user'])
155
+ ) {
156
+ console.log(`issuesBasics: ${JSON.stringify(issuesBasics, null, 2)}`);
157
+ return;
158
+ }
159
+ console.log('======================\nRequest: List violators of one issue in one report');
160
+ // Get the issue IDs.
161
+ const issueIDs = issuesBasics.map(issueBasics => issueBasics.identifier);
162
+ // Choose one at random.
163
+ const issueID = issueIDs[Math.floor(Math.random() * issueIDs.length)];
164
+ path = `/api/listViolators/${issueID}/${timeStamp}/${jobID}`;
165
+ body = await submitRequest(path, method);
166
+ responseContent = body?.['response content'] ?? {};
167
+ const violatorsBasics = responseContent?.['basics about all elements exhibiting the issue'] ?? [];
168
+ if (
169
+ !violatorsBasics.length
170
+ || !Array.isArray(violatorsBasics)
171
+ || !violatorsBasics.length
172
+ || violatorsBasics.some(violatorBasics => !violatorBasics[
173
+ 'count of rule engines reporting that the element exhibited the issue'
174
+ ])
175
+ ) {
176
+ console.log(`violatorsBasics: ${JSON.stringify(violatorsBasics, null, 2)}`);
177
+ return;
178
+ }
179
+ console.log(
180
+ '======================\nRequest: List diagnoses of one violation of one issue in one report'
181
+ );
182
+ // Get the issue IDs.
183
+ const violatorIDs = violatorsBasics.map(violatorBasics => violatorBasics.identifier);
184
+ // Choose one at random.
185
+ const violatorID = violatorIDs[Math.floor(Math.random() * violatorIDs.length)];
186
+ path = `/api/listDiagnoses/${violatorID}/${issueID}/${timeStamp}/${jobID}`;
187
+ body = await submitRequest(path, method);
188
+ responseContent = body?.['response content'] ?? {};
189
+ const diagnoses = responseContent?.['diagnoses of how the element exhibited the issue'] ?? [];
190
+ if (
191
+ !diagnoses.length
192
+ || !Array.isArray(diagnoses)
193
+ || !diagnoses.length
194
+ || diagnoses.some(
195
+ diagnosis => typeof diagnosis !== 'object' || !diagnosis['description of the violation']
196
+ )
197
+ ) {
198
+ console.log(`diagnoses: ${JSON.stringify(diagnoses, null, 2)}`);
199
+ return;
200
+ }
201
+ console.log('======================\nRequest: Get one report');
202
+ path = `/api/getReport/${timeStamp}/${jobID}`;
203
+ body = await submitRequest(path, method);
204
+ responseContent = body?.['response content'] ?? {};
205
+ const fullReport = responseContent?.['full report'] ?? {};
206
+ if (responseContent.error || typeof fullReport !== 'object' || !fullReport.device) {
207
+ console.log('Full report request failed');
208
+ return;
209
+ }
210
+ console.log('======================\nRequest: Make a test request with the GET method');
211
+ path = '/api/requestTest';
212
+ body = await submitRequest(path, method);
213
+ if (body.error?.message !== 'Invalid service request') {
214
+ console.log(`body: ${JSON.stringify(body, null, 2)}`);
215
+ return;
216
+ }
217
+ console.log('======================\nRequest: Request a test with a too short URL');
218
+ method = 'POST';
219
+ body = await submitRequest(path, method, {
220
+ description: 'Zilch',
221
+ URL: 'https://a.b',
222
+ reason: 'Just because'
223
+ });
224
+ responseContent = body?.['response content'] ?? {};
225
+ requestDetails = responseContent['details about your request'] ?? {};
226
+ if (!requestDetails.error?.includes('specified a URL for the page')) {
227
+ console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
228
+ return;
229
+ }
230
+ console.log('======================\nRequest: Request a test');
231
+ body = await submitRequest(path, method, {
232
+ description: 'Organization that does not exist',
233
+ URL: 'https://nonexistentorg.com',
234
+ reason: 'I have no good reason for wanting this page to be tested'
235
+ });
236
+ responseContent = body?.['response content'] ?? {};
237
+ requestDetails = responseContent['details about your request'] ?? {};
238
+ requestDisposition = responseContent['disposition of your request'] ?? {};
239
+ if (
240
+ !requestDetails['reason why the page should be tested']
241
+ || !requestDisposition['what happens next']?.includes('1 hour to 1 day')
242
+ ) {
243
+ console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
244
+ console.log(`requestDisposition: ${JSON.stringify(requestDisposition, null, 2)}`);
245
+ return;
246
+ }
247
+ console.log('======================\nRequest: Request a retest of a nonexistent report');
248
+ path = `/api/requestRetest/${timeStamp}/xyz`;
249
+ body = await submitRequest(path, method, {
250
+ reason: 'It would be cool to retest what does not exist'
251
+ });
252
+ responseContent = body?.['response content'] ?? {};
253
+ requestDetails = responseContent['details about your request'] ?? {};
254
+ if (!requestDetails.error?.includes('not an available report')) {
255
+ console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
256
+ return;
257
+ }
258
+ console.log('======================\nRequest: Request a retest');
259
+ path = `/api/requestRetest/${timeStamp}/${jobID}`;
260
+ body = await submitRequest(path, method, {
261
+ reason: 'I just feel like making this request'
262
+ });
263
+ responseContent = body?.['response content'] ?? {};
264
+ requestDetails = responseContent['details about your request'] ?? {};
265
+ requestDisposition = responseContent['disposition of your request'] ?? {};
266
+ if (
267
+ !requestDetails['reason why the page should be retested']
268
+ || !requestDisposition['how you can check for completion']
269
+ ) {
270
+ console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
271
+ console.log(`requestDisposition: ${JSON.stringify(requestDisposition, null, 2)}`);
272
+ return;
273
+ }
274
+ console.log('======================\nRequest: Make a feature request');
275
+ path = `/api/requestFeature/${timeStamp}/${jobID}`;
276
+ body = await submitRequest(path, method, {
277
+ feature: 'Do the impossible'
278
+ });
279
+ responseContent = body?.['response content'] ?? {};
280
+ requestDetails = responseContent['details about your request'] ?? {};
281
+ if (!requestDetails.disposition?.includes('received and logged')) {
282
+ console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
283
+ return;
284
+ }
285
+ console.log('======================\nRequest: Results');
286
+ console.log('All tests succeeded');
287
+ };
288
+
289
+ // EXECUTION
290
+
291
+ // Execute the research agent.
292
+ (async () => {
293
+ await requestService();
294
+ })();