@jrpool/kilotest 26.0.1 → 28.0.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/index.js CHANGED
@@ -87,6 +87,9 @@ const serveError = async (error, response, isHumanUser = true) => {
87
87
  if (isHumanUser) {
88
88
  // Serve an HTML page containing the message property of the error.
89
89
  response.setHeader('content-type', 'text/html; charset=utf-8');
90
+ response.setHeader('content-location', '/error.html');
91
+ response.setHeader('Access-Control-Allow-Origin', '*');
92
+ response.setHeader('Cache-Control', 'public, max-age=300, stale-while-revalidate=3000');
90
93
  const errorTemplate = await fs.readFile('error.html', 'utf8');
91
94
  const errorPage = errorTemplate.replace(/__error__/, error.message || 'ERROR');
92
95
  response.end(errorPage);
@@ -168,11 +171,28 @@ const checkBalancesForAlerts = async report => {
168
171
  };
169
172
  // Handles a request.
170
173
  const requestHandler = async (request, response) => {
174
+ // Sets response headers.
175
+ const setHeaders = (contentType, location, volatility = 'high') => {
176
+ response.setHeader('content-type', `${contentType}; charset=utf-8`);
177
+ if (location) {
178
+ response.setHeader('content-location', location);
179
+ }
180
+ response.setHeader('Access-Control-Allow-Origin', '*');
181
+ const lives = {
182
+ high: [300, 3000],
183
+ medium: [1000, 10000],
184
+ low: [5000, 50000]
185
+ };
186
+ response.setHeader(
187
+ 'Cache-Control',
188
+ `public, max-age=${lives[volatility][0]}, stale-while-revalidate=${lives[volatility][1]}`
189
+ );
190
+ };
171
191
  const {method, url} = request;
172
192
  const requestURL = new URL(url, 'https://localhost:3000');
173
193
  const {pathname, search} = requestURL;
174
194
  const pageName = pathname.split('/')[1];
175
- const pageArgs = pathname.split('/').slice(2).join('/');
195
+ const pathTail = pathname.split('/').slice(2).join('/');
176
196
  // If the request is an OPTIONS request:
177
197
  if (method === 'OPTIONS') {
178
198
  // Serve response headers, including one allowing requests from other applications.
@@ -189,14 +209,30 @@ const requestHandler = async (request, response) => {
189
209
  // Get the home page.
190
210
  const homePage = await fs.readFile('index.html', 'utf8');
191
211
  // Serve it.
192
- response.setHeader('content-type', 'text/html; charset=utf-8');
193
- response.setHeader('content-location', '/index.html');
194
- response.setHeader('Access-Control-Allow-Origin', '*');
212
+ setHeaders('text/html', '/index.html', 'medium');
195
213
  response.end(homePage);
196
214
  }
215
+ // Otherwise, if it is for the the crawler specification:
216
+ else if (pageName === 'robots.txt') {
217
+ const robots = await fs.readFile('robots.txt', 'utf8');
218
+ setHeaders('text/plain', '/robots.txt', 'low');
219
+ response.end(robots);
220
+ }
221
+ // Otherwise, if it is for the the OpenAPI specification:
222
+ else if (pageName === 'openapi.yaml') {
223
+ const openapi = await fs.readFile('openapi.yaml', 'utf8');
224
+ setHeaders('text/yaml', '/openapi.yaml', 'high');
225
+ response.end(openapi);
226
+ }
227
+ // Otherwise, if it is for the the large-language-model specification:
228
+ else if (pageName === 'llms.txt') {
229
+ const llms = await fs.readFile('llms.txt', 'utf8');
230
+ setHeaders('text/plain', '/llms.txt', 'high');
231
+ response.end(llms);
232
+ }
197
233
  // Otherwise, if it is for a full report download:
198
- else if (pageName === 'fullReport.html') {
199
- const [timeStamp, jobID] = pageArgs.split('/');
234
+ else if (pageName === 'fullReport.json') {
235
+ const [timeStamp, jobID] = pathTail.split('/');
200
236
  // If the request is syntactically valid:
201
237
  if (isTimeStamp(timeStamp) && isJobID(jobID)) {
202
238
  const reportHidden = await isHidden(timeStamp, jobID);
@@ -222,11 +258,10 @@ const requestHandler = async (request, response) => {
222
258
  // If it exists and is valid:
223
259
  if (typeof report === 'object') {
224
260
  // Serve response headers for a JSON download.
225
- response.setHeader('content-type', 'application/json; charset=utf-8');
261
+ setHeaders('application/json', null, 'low');
226
262
  response.setHeader(
227
263
  'content-disposition', `attachment; filename="${timeStamp}-${jobID}.json"`,
228
264
  );
229
- response.setHeader('Access-Control-Allow-Origin', '*');
230
265
  // Download the report.
231
266
  response.end(getJSON(report));
232
267
  }
@@ -248,11 +283,9 @@ const requestHandler = async (request, response) => {
248
283
  const topic = pageName.slice(0, -5);
249
284
  // If the page can be generated:
250
285
  if (answer[topic]) {
251
- // Serve response headers, including one allowing requests from other applications.
252
- response.setHeader('content-type', 'text/html; charset=utf-8');
253
- response.setHeader('content-location', `${pathname}${search}`);
286
+ setHeaders('text/html', `${pathname}${search}`, 'medium');
254
287
  // Get the answer data.
255
- const answerData = await answer[topic](pageArgs, search);
288
+ const answerData = await answer[topic](pathTail, search);
256
289
  // If they are valid:
257
290
  if (answerData.status === 'ok') {
258
291
  // Serve the answer page.
@@ -270,6 +303,47 @@ const requestHandler = async (request, response) => {
270
303
  await serveError({message: 'ERROR: Invalid request'}, response, true);
271
304
  }
272
305
  }
306
+ // Otherwise, if it is for an API service:
307
+ else if (pageName === 'api') {
308
+ const [service, ... specs] = pathTail.split('/');
309
+ // If the service is provision of facts about the available reports:
310
+ if (service === 'targets') {
311
+ // Get the response (potentially error) data.
312
+ const responseData = await require(path.join(__dirname, 'targets', 'api')).response(specs);
313
+ // Send them.
314
+ setHeaders('application/json', null, 'high');
315
+ response.end(JSON.stringify(responseData));
316
+ }
317
+ // Otherwise, if the service is provision of issue statistics for a report:
318
+ else if (service === 'reportIssues') {
319
+ // Get the report identifiers from the path.
320
+ const [timeStamp, jobID] = specs;
321
+ const reportSpecsBad = await isHidden(timeStamp, jobID);
322
+ // If the report is nonexistent or hidden:
323
+ if (reportSpecsBad) {
324
+ // Report this.
325
+ await serveError(
326
+ {message: reportSpecsBad === true ? 'Report nonexistent or hidden' : reportSpecsBad},
327
+ response,
328
+ false
329
+ );
330
+ }
331
+ // Otherwise, i.e. if the report is available:
332
+ else {
333
+ // Get the response (potentially error) data.
334
+ const responseData = await require(path.join(__dirname, 'reportIssues', 'api'))
335
+ .response(specs);
336
+ // Send them.
337
+ setHeaders('application/json', null, 'high');
338
+ response.end(JSON.stringify(responseData));
339
+ }
340
+ }
341
+ // Otherwise, i.e. if the service is invalid:
342
+ else {
343
+ // Report this.
344
+ await serveError({message: 'ERROR: Invalid service request'}, response, false);
345
+ }
346
+ }
273
347
  // Otherwise, if it is for a tutorial image:
274
348
  else if (pathname.startsWith('/tutorial/images/')) {
275
349
  const imgFile = pathname.slice('/tutorial/images/'.length);
@@ -277,9 +351,15 @@ const requestHandler = async (request, response) => {
277
351
  try {
278
352
  const img = await fs.readFile(imgPath);
279
353
  const ext = path.extname(imgFile).toLowerCase();
280
- const mimeTypes = {'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml'};
281
- response.setHeader('content-type', mimeTypes[ext] || 'application/octet-stream');
282
- response.setHeader('cache-control', 'public, max-age=3600');
354
+ const mimeTypes = {
355
+ '.png': 'image/png',
356
+ '.jpg': 'image/jpeg',
357
+ '.jpeg': 'image/jpeg',
358
+ '.gif': 'image/gif',
359
+ '.webp': 'image/webp',
360
+ '.svg': 'image/svg+xml'
361
+ };
362
+ setHeaders(mimeTypes[ext] || 'application/octet-stream', null, 'low');
283
363
  response.end(img);
284
364
  }
285
365
  catch (_) {
@@ -291,7 +371,7 @@ const requestHandler = async (request, response) => {
291
371
  // Get the site icon.
292
372
  const icon = await fs.readFile(path.join(__dirname, 'favicon.ico'));
293
373
  // Serve it.
294
- response.setHeader('content-type', 'image/x-icon');
374
+ setHeaders('image/x-icon', null, 'low');
295
375
  response.write(icon, 'binary');
296
376
  response.end('');
297
377
  }
@@ -300,10 +380,7 @@ const requestHandler = async (request, response) => {
300
380
  try {
301
381
  // Serve it.
302
382
  const styleSheet = await fs.readFile('style.css', 'utf8');
303
- response.writeHead(200, {
304
- 'content-type': 'text/css; charset=utf-8',
305
- 'cache-control': 'public, max-age=600'
306
- });
383
+ setHeaders('text/css', null, 'low');
307
384
  response.end(styleSheet);
308
385
  }
309
386
  catch (error) {
@@ -325,16 +402,14 @@ const requestHandler = async (request, response) => {
325
402
  // If the request is a retest recommendation:
326
403
  if (pageName === 'retestRec.html') {
327
404
  const {why} = postData;
328
- const [timeStamp, jobID] = pageArgs.split('/');
405
+ const [timeStamp, jobID] = pathTail.split('/');
329
406
  // If the request is valid:
330
407
  if (isTimeStamp(timeStamp) && isJobID(jobID) && why) {
331
- // Serve response headers, including one allowing requests from other applications.
332
- response.setHeader('content-type', 'text/html; charset=utf-8');
333
- response.setHeader('content-location', `${pathname}${search}`);
334
- response.setHeader('Access-Control-Allow-Origin', '*');
408
+ // Serve response headers.
409
+ setHeaders('text/html', `${pathname}${search}`, 'high');
335
410
  // Get the answer data.
336
411
  const answerData = await require(path.join(__dirname, 'retestRec', 'index'))
337
- .answer(pageArgs, why);
412
+ .answer(pathTail, why);
338
413
  // If they are valid:
339
414
  if (answerData.status === 'ok') {
340
415
  // Serve the answer page.
@@ -358,8 +433,7 @@ const requestHandler = async (request, response) => {
358
433
  // If the request is valid:
359
434
  if (what && url.startsWith('https://') && why) {
360
435
  // Serve headers for a response.
361
- response.setHeader('content-type', 'text/html; charset=utf-8');
362
- response.setHeader('content-location', `${pathname}${search}`);
436
+ setHeaders('text/html', `${pathname}${search}`, 'high');
363
437
  // Get the answer data.
364
438
  const answerData = await require(path.join(__dirname, 'testRec', 'index'))
365
439
  .answer(what, url, why);
@@ -386,11 +460,11 @@ const requestHandler = async (request, response) => {
386
460
  const [url, what] = target.split('\t');
387
461
  // If the request is valid:
388
462
  if (url.startsWith('https://') && authCode === process.env.AUTH_CODE) {
389
- // Serve a content-type header for a response.
390
- response.setHeader('content-type', 'text/html; charset=utf-8');
463
+ // Set the non-location headers for a response.
464
+ setHeaders('text/html', null, 'high');
391
465
  // If the request is an approval:
392
466
  if (what) {
393
- // Serve a location header for a response.
467
+ // Set a location header for a response.
394
468
  response.setHeader('content-location', `${pathname}${search}`);
395
469
  // Get the answer data.
396
470
  const answerData = await require(path.join(__dirname, 'testOrder', 'index'))
@@ -414,7 +488,7 @@ const requestHandler = async (request, response) => {
414
488
  delete recs[url];
415
489
  // Save the revised recommendations.
416
490
  await fs.writeFile(path.join(__dirname, 'jobs', 'recs.json'), getJSON(recs));
417
- // Serve a location header for a response.
491
+ // Set a location header for a response.
418
492
  response.setHeader('content-location', '/recActionForm.html');
419
493
  // Get the answer data.
420
494
  const answerData = await require(path.join(__dirname, 'recActionForm', 'index')).answer();
@@ -431,9 +505,8 @@ const requestHandler = async (request, response) => {
431
505
  // Otherwise, if it is a reannotation order:
432
506
  else if (pageName === 'reannotate.html') {
433
507
  const {authCode} = postData;
434
- // Serve headers for a response.
435
- response.setHeader('content-type', 'text/html; charset=utf-8');
436
- response.setHeader('content-location', `${pathname}${search}`);
508
+ // Set headers for a response.
509
+ setHeaders('text/html', `${pathname}${search}`, 'high');
437
510
  // Get the answer data.
438
511
  const answerData = await require(path.join(__dirname, 'reannotate', 'index'))
439
512
  .answer(authCode);
@@ -451,9 +524,8 @@ const requestHandler = async (request, response) => {
451
524
  // Otherwise, if it is a WCAG map renewal:
452
525
  else if (pageName === 'wcagRenew.html') {
453
526
  const {authCode} = postData;
454
- // Serve headers for a response.
455
- response.setHeader('content-type', 'text/html; charset=utf-8');
456
- response.setHeader('content-location', `${pathname}${search}`);
527
+ // Set headers for a response.
528
+ setHeaders('text/html', `${pathname}${search}`, 'low');
457
529
  // Get the answer data.
458
530
  const answerData = await require(path.join(__dirname, 'wcagRenew', 'index'))
459
531
  .answer(authCode);
@@ -470,10 +542,13 @@ const requestHandler = async (request, response) => {
470
542
  }
471
543
  // Otherwise, if it is a request from an agent:
472
544
  else if (pageName === 'api') {
473
- // Get the agent ID, the service, and any service specifications from the path.
474
- const [agentID, service, ... specs] = pageArgs.split('/');
475
- // If the agent is the authorized Testaro instance and it is authenticated:
476
- if (agentID === testaroAgent && postData.agentPW === testaroAgentPW) {
545
+ // Get the segments of the path after api.
546
+ const segments = pathTail.split('/');
547
+ // If the first segment is the ID of the Testaro agent and the agent is authenticated:
548
+ if (specs[0] === testaroAgent && postData.agentPW === testaroAgentPW) {
549
+ const agentID = specs[0];
550
+ // Get the requested service from the path.
551
+ const service = specs[1];
477
552
  // If the service is job assignment:
478
553
  if (service === 'job') {
479
554
  let clean = true;
@@ -580,58 +655,11 @@ const requestHandler = async (request, response) => {
580
655
  );
581
656
  }
582
657
  }
583
- // Otherwise, if the agent is the authorized research agent and it is authenticated:
584
- else if (agentID === researchAgent && postData.agentPW === researchAgentPW) {
585
- // If the service is provision of facts about the available reports:
586
- if (service === 'targets') {
587
- // Get the agent ID from the path.
588
- const args = [agentID];
589
- // Get the response (potentially error) data.
590
- const responseData = await require(path.join(__dirname, 'targets', 'api')).response(args);
591
- // Send them.
592
- response.end(JSON.stringify(responseData));
593
- }
594
- // Otherwise, if the service is provision of facts about issues in a report:
595
- else if (service === 'reportIssues') {
596
- // Get the agent ID and report identifiers from the path.
597
- const [timeStamp, jobID] = specs;
598
- const args = [agentID, timeStamp, jobID];
599
- const reportSpecsBad = await isHidden(timeStamp, jobID);
600
- // If the report is nonexistent or hidden:
601
- if (reportSpecsBad) {
602
- // Report this.
603
- await serveError(
604
- {message: reportSpecsBad === true ? 'Report nonexistent or hidden' : reportSpecsBad},
605
- response,
606
- false
607
- );
608
- }
609
- // Otherwise, i.e. if the report is available:
610
- else {
611
- // Get the response (potentially error) data.
612
- const responseData = await require(path.join(__dirname, 'reportIssues', 'api'))
613
- .response(args);
614
- // Send them.
615
- response.end(JSON.stringify(responseData));
616
- }
617
- }
618
- // Otherwise, i.e. if the service is invalid:
619
- else {
620
- // Report this.
621
- await serveError({message: 'ERROR: Invalid service request from research agent'}, response, false);
622
- }
623
- }
624
- // Otherwise, i.e. if the agent is not authorized or not authenticated:
625
- else {
626
- // Report this.
627
- await serveError({message: 'ERROR: Invalid agent'}, response, false);
628
- }
629
658
  }
630
659
  // Otherwise, if it is a tutorial comment:
631
660
  else if (pageName === 'tutorialComment.html') {
632
661
  const {content} = postData;
633
- response.setHeader('content-type', 'application/json; charset=utf-8');
634
- response.setHeader('Access-Control-Allow-Origin', '*');
662
+ setHeaders('application/json', null, 'low');
635
663
  const answerData = await require(path.join(__dirname, 'tutorial', 'index')).saveComment(content);
636
664
  if (answerData.status === 'ok') {
637
665
  response.end(JSON.stringify({status: 'ok'}));
package/llms.txt ADDED
@@ -0,0 +1,16 @@
1
+ # Kilotest
2
+ Kilotest helps AI agents give accurate and truthful answers when users ask how accessible, usable, or standard conformant web pages are. Kilotest integrates ten independent rule engines to provide comprehensive multipage, single-page, and element-level statistics and details via its API and web UI.
3
+
4
+ ## API
5
+ - GET /api/targets: summarize test results for all tested pages
6
+ - GET /api/reportIssues/{timeStamp}/{jobID}: statistics on issues reported in a job
7
+ - GET /api/reportIssue/{issueID}/{timeStamp}/{jobID}: details on issues reported in a job
8
+ - GET /api/issues: statistics on frequently reported issues across all tested pages
9
+ - OpenAPI spec: https://kilotest.com/openapi.yaml
10
+
11
+ ## Web UI
12
+ - https://kilotest.com/targets.html: summarize test results for all tested pages
13
+ - https://kilotest.com/reportIssues.html/{timeStamp}/{jobID}: details on issues reported in a job
14
+ - https://kilotest.com/reportIssue.html/{issueID}/{timeStamp}/{jobID}: details on issues reported in a job
15
+ - https://kilotest.com/issues.html: statistics on frequently reported issues across all tested pages
16
+ - https://kilotest.com/tutorial.html: tutorial, “Accessibility testing strategies”
package/openapi.yaml ADDED
@@ -0,0 +1,346 @@
1
+ openapi: 3.1.0
2
+ info:
3
+ title: Kilotest Agent API
4
+ description: Kilotest tests web pages for accessibility, usability, and standard conformity using an ensemble of ten independent tools that employ rule-based and machine-learning-based methods. This API enables AI agents to recommend web pages for testing (not yet implemented), discover available test reports, and retrieve data from reports at multiple levels of detail. For background on Kilotest and the advantages of ensemble testing, visit https://kilotest.com.
5
+ version: 1.0.0
6
+ contact:
7
+ url: https://kilotest.com
8
+ email: info@kilotest.com
9
+ repository: https://github.com/jrpool/kilotest
10
+
11
+ servers:
12
+ - url: https://kilotest.com/api
13
+ description: Kilotest production server
14
+
15
+ paths:
16
+ /api/targets:
17
+ post:
18
+ operationId: getReportSummaries
19
+ summary: Summarize all available reports
20
+ description: Returns summary data about every non-hidden report available from Kilotest, including the name and URL of the tested web page, when the testing was performed, how many issues were reported, and URLs for retrieving more detailed data from the report.
21
+ responses:
22
+ '200':
23
+ description: Summaries of available reports
24
+ content:
25
+ application/json:
26
+ schema:
27
+ $ref: '#/components/schemas/ReportSummariesResponse'
28
+
29
+ /api/reportIssues/{timeStamp}/{jobID}:
30
+ post:
31
+ operationId: getReportIssues
32
+ summary: Get data on issues from a specific report
33
+ description: Returns data about the issues reported in a specific Kilotest report, grouped by priority. The data on each issue include the tools that reported it, the number of HTML elements exhibiting it, and URLs for retrieving element-level detail. The timeStamp and jobID components identify the report and are available in the getReportSummaries response.
34
+ parameters:
35
+ - name: timeStamp
36
+ in: path
37
+ required: true
38
+ description: timeStamp component of the report identifier, in the format YYMMDDTHHMM (e.g., 260503T0432).
39
+ schema:
40
+ type: string
41
+ pattern: '^\d{6}T\d{4}$'
42
+ example: '260503T0432'
43
+ - name: jobID
44
+ in: path
45
+ required: true
46
+ description: Job identifier component of the report identifier (e.g., xx0).
47
+ schema:
48
+ type: string
49
+ example: 'xx0'
50
+ responses:
51
+ '200':
52
+ description: Data on issues from the specified report
53
+ content:
54
+ application/json:
55
+ schema:
56
+ $ref: '#/components/schemas/ReportIssuesResponse'
57
+ '404':
58
+ description: No report with the specified identifier is available
59
+
60
+ /api/reportIssue/{issueID}/{timeStamp}/{jobID}:
61
+ post:
62
+ operationId: getReportIssue
63
+ summary: Get details about a specific issue in a specific report
64
+ description: Returns details about a single issue within a specific report, including which HTML elements exhibit the issue and, for each such element, URLs for retrieving tool-by-tool diagnoses of the issue on the element.
65
+ parameters:
66
+ - name: issueID
67
+ in: path
68
+ required: true
69
+ description: Issue identifier (e.g., imageNoText). Available under "issues reported" > priority level > "identifier" in the getReportIssues response.
70
+ schema:
71
+ type: string
72
+ example: 'imageNoText'
73
+ - name: timeStamp
74
+ in: path
75
+ required: true
76
+ schema:
77
+ type: string
78
+ - name: jobID
79
+ in: path
80
+ required: true
81
+ schema:
82
+ type: string
83
+ responses:
84
+ '200':
85
+ description: Details about the specified issue in the specified report
86
+ content:
87
+ application/json:
88
+ schema:
89
+ $ref: '#/components/schemas/ReportIssueResponse'
90
+ '404':
91
+ description: No report or issue found with the specified identifiers
92
+
93
+ components:
94
+ schemas:
95
+
96
+ CommonResponseFields:
97
+ type: object
98
+ properties:
99
+ summary:
100
+ type: string
101
+ description: Natural-language description of this response and of Kilotest. Provides context for an agent encountering Kilotest for the first time.
102
+ tool name:
103
+ type: string
104
+ example: Kilotest
105
+ request:
106
+ type: object
107
+ properties:
108
+ requesting agent:
109
+ type: object
110
+ properties:
111
+ identifier:
112
+ type: string
113
+ example: research-agent
114
+ name:
115
+ type: string
116
+ example: Internal Research Agent
117
+ type of request:
118
+ type: object
119
+ properties:
120
+ identifier:
121
+ type: string
122
+ description:
123
+ type: string
124
+ response metadata:
125
+ type: object
126
+ properties:
127
+ identifier:
128
+ type: string
129
+ description: Unique identifier for this response instance.
130
+ date and time:
131
+ type: string
132
+ format: date-time
133
+
134
+ ToolInfo:
135
+ type: object
136
+ description: An accessibility testing tool in the Kilotest ensemble.
137
+ properties:
138
+ identifier:
139
+ type: string
140
+ description: Short programmatic identifier for the tool.
141
+ example: alfa
142
+ name:
143
+ type: string
144
+ description: Display name of the tool.
145
+ example: Alfa
146
+ sponsor:
147
+ type: string
148
+ description: Organization that created or sponsors the tool.
149
+ example: Siteimprove
150
+
151
+ ToolFailure:
152
+ type: object
153
+ description: A tool that was unable to complete testing of the page.
154
+ properties:
155
+ name:
156
+ type: string
157
+ example: WAVE
158
+ reason for failure:
159
+ type: string
160
+ example: Not enough credits.
161
+
162
+ ToolsSummary:
163
+ type: object
164
+ description: Count and names of a set of tools.
165
+ properties:
166
+ number:
167
+ type: integer
168
+ names:
169
+ type: array
170
+ items:
171
+ type: string
172
+
173
+ NextTierURLs:
174
+ type: object
175
+ description: URLs for retrieving the next level of detail.
176
+ properties:
177
+ for you:
178
+ type: string
179
+ format: uri
180
+ description: URL for an agent to request the next tier of detail.
181
+ for humans:
182
+ type: string
183
+ format: uri
184
+ description: URL for a human to view the next tier of detail.
185
+
186
+ ReportSummaryItem:
187
+ type: object
188
+ description: Summary data about a single available report.
189
+ properties:
190
+ identifier:
191
+ type: string
192
+ example: 260504T1659-029
193
+ creation date:
194
+ type: string
195
+ format: date-time
196
+ days since the creation date:
197
+ type: integer
198
+ tested web page:
199
+ type: object
200
+ properties:
201
+ description:
202
+ type: string
203
+ URL:
204
+ type: string
205
+ format: uri
206
+ whether a later report about the same page exists:
207
+ type: boolean
208
+ description: If true, a more recent report about this page is available. If you want the latest results, use that report instead.
209
+ number of issues reported:
210
+ type: integer
211
+ number of HTML elements reported as exhibiting issues:
212
+ type: integer
213
+ tools that tried to test the page:
214
+ $ref: '#/components/schemas/ToolsSummary'
215
+ tools that were unable to test the page:
216
+ $ref: '#/components/schemas/ToolsSummary'
217
+ tools that reported issues:
218
+ $ref: '#/components/schemas/ToolsSummary'
219
+ URLs for getting data on the reported issues:
220
+ $ref: '#/components/schemas/NextTierURLs'
221
+ URL for getting the full technical report as JSON:
222
+ type: string
223
+ format: uri
224
+
225
+ ReportSummariesResponse:
226
+ allOf:
227
+ - $ref: '#/components/schemas/CommonResponseFields'
228
+ - type: object
229
+ properties:
230
+ available reports:
231
+ type: array
232
+ description: One entry per available Kilotest report, in alphabetical order by page description and, in case of multiple reports per page, in order of creation date and time. Reports are matched by page description, not page URL, so, if only the URL of a page has changed between reports, the reports are treated as reports about the same page.
233
+ items:
234
+ $ref: '#/components/schemas/ReportSummaryItem'
235
+
236
+ IssueEntry:
237
+ type: object
238
+ description: Details about a specific accessibility issue found on a page.
239
+ properties:
240
+ identifier:
241
+ type: string
242
+ example: imageNoText
243
+ summary:
244
+ type: string
245
+ description: Brief natural-language label for the issue.
246
+ example: image not named
247
+ related WCAG 2.2 standard:
248
+ type: object
249
+ description: The WCAG 2.2 success criterion or guideline most closely related to this issue.
250
+ properties:
251
+ layer:
252
+ type: string
253
+ enum: [guideline, success criterion]
254
+ 'numeric identifier':
255
+ type: string
256
+ pattern: '^\d\.\d(\.\d+)?$'
257
+ example: '1.1.1'
258
+ impact on a user:
259
+ type: string
260
+ description: How this issue is likely to affect users.
261
+ tools reporting the issue:
262
+ $ref: '#/components/schemas/ToolsSummary'
263
+ number of HTML elements reported as exhibiting the issue:
264
+ type: integer
265
+ URLs for details about the issue on the page:
266
+ $ref: '#/components/schemas/NextTierURLs'
267
+
268
+ IssuesByPriority:
269
+ type: object
270
+ properties:
271
+ highest priority:
272
+ type: array
273
+ items:
274
+ $ref: '#/components/schemas/IssueEntry'
275
+ high priority:
276
+ type: array
277
+ items:
278
+ $ref: '#/components/schemas/IssueEntry'
279
+ low priority:
280
+ type: array
281
+ items:
282
+ $ref: '#/components/schemas/IssueEntry'
283
+ lowest priority:
284
+ type: array
285
+ items:
286
+ $ref: '#/components/schemas/IssueEntry'
287
+
288
+ ReportIssuesResponse:
289
+ allOf:
290
+ - $ref: '#/components/schemas/CommonResponseFields'
291
+ - type: object
292
+ properties:
293
+ report:
294
+ type: object
295
+ properties:
296
+ identifier:
297
+ type: string
298
+ creation date:
299
+ type: string
300
+ format: date-time
301
+ days since the creation date:
302
+ type: integer
303
+ tested web page:
304
+ type: object
305
+ properties:
306
+ description:
307
+ type: string
308
+ URL:
309
+ type: string
310
+ format: uri
311
+ tools that tried to test the page:
312
+ type: array
313
+ items:
314
+ $ref: '#/components/schemas/ToolInfo'
315
+ tools that were unable to test the page:
316
+ type: array
317
+ items:
318
+ $ref: '#/components/schemas/ToolFailure'
319
+ tools that reported issues:
320
+ $ref: '#/components/schemas/ToolsSummary'
321
+ number of issues reported:
322
+ type: object
323
+ properties:
324
+ total:
325
+ type: integer
326
+ by priority:
327
+ type: object
328
+ properties:
329
+ highest priority:
330
+ type: integer
331
+ high priority:
332
+ type: integer
333
+ low priority:
334
+ type: integer
335
+ lowest priority:
336
+ type: integer
337
+ number of HTML elements reported as exhibiting issues:
338
+ type: integer
339
+ issues reported:
340
+ $ref: '#/components/schemas/IssuesByPriority'
341
+
342
+ ReportIssueResponse:
343
+ allOf:
344
+ - $ref: '#/components/schemas/CommonResponseFields'
345
+ - type: object
346
+ description: Element-level detail for a specific issue. Schema to be completed once the tier-3 service is implemented.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "26.0.1",
3
+ "version": "28.0.0",
4
4
  "description": "An ensemble testing service with a focus on accessibility",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,20 +12,22 @@ const {
12
12
  getRandomString,
13
13
  getToolsFacts,
14
14
  isHidden,
15
- researchAgents,
16
15
  tools
17
16
  } = require('../util');
18
17
 
19
18
  // FUNCTIONS
20
19
 
21
20
  // Gets facts about an issue.
22
- const getIssueFacts = (thisHost, agentID, timeStamp, jobID, issue) => {
21
+ const getIssueFacts = (thisHost, timeStamp, jobID, issue) => {
23
22
  const {issueID, reporterCount, reporters, summary, violatorCount, wcag, why} = issue;
24
- const wcagType = wcag.length === 3 ? 'principle' : 'success criterion';
23
+ const wcagType = wcag.length === 3 ? 'guideline' : 'success criterion';
25
24
  return {
26
25
  identifier: issueID,
27
26
  summary,
28
- [`related WCAG ${wcagType}`]: wcag,
27
+ 'related WCAG 2.2 standard': {
28
+ layer: wcagType,
29
+ 'numeric identifier': wcag
30
+ },
29
31
  'impact on a user': why,
30
32
  'tools reporting the issue': {
31
33
  'number': reporterCount,
@@ -33,14 +35,14 @@ const getIssueFacts = (thisHost, agentID, timeStamp, jobID, issue) => {
33
35
  },
34
36
  'number of HTML elements reported as exhibiting the issue': violatorCount,
35
37
  'URLs for details about the issue on the page': {
36
- 'for you': `${thisHost}/api/${agentID}/reportIssue/${timeStamp}/${jobID}/${issueID}`,
37
- 'for humans': `${thisHost}/reportIssue/${timeStamp}/${jobID}/${issueID}`
38
+ 'for you': `${thisHost}/api/reportIssue/${issueID}/${timeStamp}/${jobID}`,
39
+ 'for humans': `${thisHost}/reportIssue.html/${issueID}/${timeStamp}/${jobID}`
38
40
  }
39
41
  };
40
42
  };
41
43
  // Returns a response to a target-issues request.
42
44
  exports.response = async args => {
43
- const [agentID, timeStamp, jobID] = args;
45
+ const [timeStamp, jobID] = args;
44
46
  const reportIsHidden = await isHidden(timeStamp, jobID);
45
47
  // If the report is not available:
46
48
  if (reportIsHidden) {
@@ -64,24 +66,25 @@ exports.response = async args => {
64
66
  summary: `This document fulfills a request made by an agent to the Kilotest service. The agent requested data from a Kilotest report about the accessibility, usability, and standard-conformity of a web page. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten testing tools, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes several API endpoints for agents and several web UI URLs for humans to obtain information from Kilotest reports. To learn more about Kilotest and the advangages of testing with an ensemble of tools, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), which contains an introduction on its home page and a tutorial.`,
65
67
  'tool name': 'Kilotest',
66
68
  request: {
67
- 'requesting agent': {
68
- identifier: agentID,
69
- name: researchAgents[agentID]
70
- },
71
69
  'type of request': {
72
70
  identifier: 'reportIssues',
73
71
  description: 'What issues does the specified report describe?'
74
72
  },
73
+ URLs: {
74
+ 'URL of your request': `${thisHost}/api/reportIssues/${timeStamp}/${jobID}`,
75
+ 'equivalent URL for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
76
+ },
75
77
  'closest ancestor request': {
76
78
  description: 'Which web pages are reports available about, and what are the statistics about the issues reported for each page?',
77
- 'URL for you': `${thisHost}/api/${agentID}/targets.html`,
78
- 'URL for humans': `${thisHost}/targets.html`
79
+ URLs: {
80
+ 'for you': `${thisHost}/api/targets.html`,
81
+ 'for humans': `${thisHost}/targets.html`
82
+ }
79
83
  }
80
84
  },
81
85
  'response metadata': {
82
86
  identifier: `${getNowStamp()}-${getRandomString(3)}`,
83
- 'date and time': new Date().toISOString(),
84
- 'URL of the human-oriented equivalent of this response': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
87
+ 'date and time': new Date().toISOString()
85
88
  },
86
89
  report: {
87
90
  identifier: `${timeStamp}-${jobID}`,
@@ -110,13 +113,13 @@ exports.response = async args => {
110
113
  'number of HTML elements reported as exhibiting issues': violatorCount,
111
114
  'issues reported': {
112
115
  'highest priority': issues[4]
113
- .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
116
+ .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
114
117
  'high priority': issues[3]
115
- .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
118
+ .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
116
119
  'low priority': issues[2]
117
- .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue)),
120
+ .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
118
121
  'lowest priority': issues[1]
119
- .map(issue => getIssueFacts(thisHost, agentID, timeStamp, jobID, issue))
122
+ .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue))
120
123
  }
121
124
  };
122
125
  return response;
@@ -25,7 +25,7 @@
25
25
  <p>__reporterCount__ reported issues (__reporters__)</p>
26
26
  <p>__issueCount__ reported (__highestCount__ highest, __highCount__ high, __lowCount__ low, __lowestCount__ lowest priority)</p>
27
27
  <p>__violatorCount__ reported</p>
28
- <p>Download the <a href="/fullReport.html/__timeStamp__/__jobID__">full technical report</a></p>
28
+ <p>Download the <a href="/fullReport.json/__timeStamp__/__jobID__">full technical report</a></p>
29
29
  <h3>Details</h3>
30
30
  <details>
31
31
  <summary>Highest priority: __highestCount__</summary>
package/researchAgent.js CHANGED
@@ -20,8 +20,6 @@ const httpsClient = require('https');
20
20
 
21
21
  // CONSTANTS
22
22
 
23
- const agent = process.env.RESEARCH_AGENT;
24
- const agentPW = process.env.RESEARCH_AGENT_PW;
25
23
  const kilotestHosts = [process.env.LOCAL_KILOTEST_HOST, process.env.DEPLOYED_KILOTEST_HOST];
26
24
  // Randomly chosen Kilotest host.
27
25
  const kilotestHost = kilotestHosts[Math.random() < 0.5 ? 0 : 1];
@@ -36,7 +34,7 @@ const port = hostParts[2] || (scheme === 'https' ? 443 : 80);
36
34
  const requestService = async () => {
37
35
  const client = scheme === 'https' ? httpsClient : httpClient;
38
36
  const getRequestOptions = path => ({
39
- method: 'POST',
37
+ method: 'GET',
40
38
  host,
41
39
  port,
42
40
  path,
@@ -44,7 +42,7 @@ const requestService = async () => {
44
42
  'content-type': 'application/json; charset=utf-8'
45
43
  }
46
44
  });
47
- const path = `/api/${agent}/targets`;
45
+ const path = `/api/targets`;
48
46
  console.log(`About to submit ${scheme} request as JSON on port ${port} to ${host}${path}`);
49
47
  // Submit a targets request.
50
48
  client.request(getRequestOptions(path), response => {
@@ -75,7 +73,7 @@ const requestService = async () => {
75
73
  const [timeStamp, jobID] = reportIDs[Math.floor(Math.random() * reportIDs.length)]
76
74
  .split('-');
77
75
  console.log('======================');
78
- const path = `/api/${agent}/reportIssues/${timeStamp}/${jobID}`;
76
+ const path = `/api/reportIssues/${timeStamp}/${jobID}`;
79
77
  console.log(`About to submit ${scheme} request as JSON on port ${port} to ${host}${path}`);
80
78
  const requestOptions = getRequestOptions(path);
81
79
  // Submit an issues request for it.
@@ -109,9 +107,7 @@ const requestService = async () => {
109
107
  });
110
108
  })
111
109
  // Finish sending the issues request.
112
- .end(JSON.stringify({
113
- agentPW
114
- }));
110
+ .end();
115
111
  }
116
112
  catch (error) {
117
113
  console.log(error.message);
@@ -120,9 +116,7 @@ const requestService = async () => {
120
116
  });
121
117
  })
122
118
  // Finish sending the targets request.
123
- .end(JSON.stringify({
124
- agentPW
125
- }));
119
+ .end();
126
120
  };
127
121
 
128
122
  // EXECUTION
package/robots.txt ADDED
@@ -0,0 +1,4 @@
1
+ # robots.txt
2
+ User-agent: *
3
+ Allow: /
4
+ Sitemap: https://kilotest.com/sitemap.xml
package/sitemap.xml ADDED
@@ -0,0 +1,9 @@
1
+ <!-- sitemap.xml -->
2
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
+ <url><loc>https://kilotest.com/</loc></url>
4
+ <url><loc>https://kilotest.com/targets.html</loc></url>
5
+ <url><loc>https://kilotest.com/issues.html</loc></url>
6
+ <url><loc>https://kilotest.com/tutorial.html</loc></url>
7
+ <url><loc>https://kilotest.com/openapi.yaml</loc></url>
8
+ <url><loc>https://kilotest.com/llms.txt</loc></url>
9
+ </urlset>
package/targets/api.js CHANGED
@@ -20,7 +20,7 @@ const thisHost = process.env.THIS_KILOTEST_HOST;
20
20
  // FUNCTIONS
21
21
 
22
22
  // Returns a response to a targets request.
23
- exports.response = async agentID => {
23
+ exports.response = async () => {
24
24
  const availableReports = [];
25
25
  // Get the non-hidden logs.
26
26
  const targetLogs = await getLogs();
@@ -52,8 +52,6 @@ exports.response = async agentID => {
52
52
  URL: url
53
53
  },
54
54
  'whether a later report about the same page exists': !! superseded,
55
- 'number of issues reported': issueCount,
56
- 'number of HTML elements reported as exhibiting issues': violatorCount,
57
55
  'tools that tried to test the page': {
58
56
  number: toolCount,
59
57
  names: toolNames
@@ -66,28 +64,26 @@ exports.response = async agentID => {
66
64
  number: reporterCount,
67
65
  names: reporterNames
68
66
  },
69
- 'URLs for getting data on the reported issues': {
70
- 'for you': `${thisHost}/api/${agentID}/reportIssues/${timeStamp}/${jobID}`,
67
+ 'number of issues reported': issueCount,
68
+ 'number of HTML elements reported as exhibiting issues': violatorCount,
69
+ 'URLs for getting data about the reported issues': {
70
+ 'for you': `${thisHost}/api/reportIssues/${timeStamp}/${jobID}`,
71
71
  'for humans': `${thisHost}/reportIssues/${timeStamp}/${jobID}`
72
72
  },
73
- 'URL for getting the full technical report as JSON': `${thisHost}/fullReport.html/${timeStamp}/${jobID}`
73
+ 'URL for getting the full technical report as JSON': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`
74
74
  });
75
75
  }
76
76
  // Get a response.
77
- const response = {
78
- summary: `This document fulfills a request made by an agent to the Kilotest service. The agent requested data about the web pages that Kilotest had tested for accessibility, usability, and standard-conformity and statistics for each page on the results of the tests. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten testing tools, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes API endpoints for agents and web UI URLs for humans to recommend web pages for testing and obtain information from Kilotest reports. To learn more about Kilotest and the advangages of testing with an ensemble of tools, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), whose home page contains an introduction and a link to a tutorial.`,
77
+ const content = {
78
+ summary: `This document fulfills a request made by an agent to the Kilotest service. The agent requested data about the web pages that Kilotest had tested for accessibility, usability, and standard-conformity and, for each page, statistics about the results of the tests. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten testing tools, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes API endpoints for agents and web UI URLs for humans to recommend web pages for testing and to obtain information from Kilotest reports. To learn more about Kilotest and the advangages of testing with an ensemble of tools, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), whose home page contains an introduction and a link to a tutorial.`,
79
79
  'tool name': 'Kilotest',
80
80
  request: {
81
- 'requesting agent': {
82
- identifier: agentID,
83
- name: researchAgents[agentID]
84
- },
85
81
  'type of request': {
86
82
  identifier: 'targets',
87
83
  description: 'Give me summary data about each available report.'
88
84
  },
89
85
  URLs: {
90
- 'URL of this request': `${thisHost}/api/${agentID}/targets`,
86
+ 'URL of your request': `${thisHost}/api/targets`,
91
87
  'equivalent URL for humans': `${thisHost}/targets.html`
92
88
  }
93
89
  },
@@ -97,5 +93,5 @@ exports.response = async agentID => {
97
93
  },
98
94
  'available reports': availableReports
99
95
  };
100
- return response;
96
+ return content;
101
97
  };
package/util.js CHANGED
@@ -367,7 +367,7 @@ exports.getReportData = async (timeStamp, jobID) => {
367
367
  if (act.type === 'test') {
368
368
  const {result, which} = act;
369
369
  // Ensure that the tool is in the temporary data.
370
- toolNameSet.add(which);
370
+ toolNameSet.add(tools[which][0]);
371
371
  const instances = result?.standardResult?.instances ?? [];
372
372
  // For each standard instance of the act:
373
373
  instances.forEach(instance => {