@jrpool/kilotest 37.0.0 → 38.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/target/api.js CHANGED
@@ -21,9 +21,10 @@ const thisHost = process.env.THIS_KILOTEST_HOST;
21
21
 
22
22
  // Returns a response to a target request.
23
23
  exports.response = async args => {
24
- const [what = '', hostname = ''] = args;
24
+ const [description = '', hostname = ''] = args;
25
25
  const hostLC = hostname.toLowerCase();
26
- const whatLC = what.toLowerCase();
26
+ const whatLC = description.toLowerCase();
27
+ // Initialize an array of summaries of matching reports.
27
28
  const matchingReports = [];
28
29
  // Get the non-hidden logs.
29
30
  const targetLogs = await getLogs();
@@ -42,6 +43,7 @@ exports.response = async args => {
42
43
  // Get data about its report.
43
44
  const data = await getReportData(timeStamp, jobID);
44
45
  const {
46
+ error,
45
47
  creationDate,
46
48
  daysAgo,
47
49
  issueCount,
@@ -53,35 +55,44 @@ exports.response = async args => {
53
55
  preventedToolNames,
54
56
  preventedToolCount
55
57
  } = data;
56
- matchingReports.push({
57
- identifier: jobName,
58
- 'creation date': creationDate,
59
- 'days since the creation date': daysAgo,
60
- 'tested web page': {
61
- description: reportWhat,
62
- URL: reportURL
63
- },
64
- 'whether a later report about the same page exists': !! superseded,
65
- 'rule engines that tried to test the page': {
66
- number: toolCount,
67
- names: toolNames
68
- },
69
- 'rule engines that were unable to test the page': {
70
- number: preventedToolCount,
71
- names: preventedToolNames
72
- },
73
- 'rule engines that reported issues': {
74
- number: reporterCount,
75
- names: reporterNames
76
- },
77
- 'number of issues reported': issueCount,
78
- 'number of HTML elements reported as exhibiting issues': violatorCount,
79
- 'URLs for getting data about the reported issues': {
80
- 'for you': `${thisHost}/api/reportIssues/${timeStamp}/${jobID}`,
81
- 'for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
82
- },
83
- 'URL for getting the full technical report as JSON': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`
84
- });
58
+ // If this failed:
59
+ if (error) {
60
+ // Report why.
61
+ console.error(error);
62
+ }
63
+ // Otherwise, i.e. if it succeeded:
64
+ else {
65
+ // Add a report summary to the array.
66
+ matchingReports.push({
67
+ identifier: jobName,
68
+ 'creation date': creationDate,
69
+ 'days since the creation date': daysAgo,
70
+ 'tested web page': {
71
+ description: reportWhat,
72
+ URL: reportURL
73
+ },
74
+ 'whether a later report about the same page exists': !! superseded,
75
+ 'rule engines that tried to test the page': {
76
+ number: toolCount,
77
+ names: toolNames
78
+ },
79
+ 'rule engines that were unable to test the page': {
80
+ number: preventedToolCount,
81
+ names: preventedToolNames
82
+ },
83
+ 'rule engines that reported issues': {
84
+ number: reporterCount,
85
+ names: reporterNames
86
+ },
87
+ 'number of issues reported': issueCount,
88
+ 'number of HTML elements reported as exhibiting issues': violatorCount,
89
+ 'URLs for getting data about the reported issues': {
90
+ 'for you': `${thisHost}/api/reportSummary/${timeStamp}/${jobID}`,
91
+ 'for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
92
+ },
93
+ 'URL for getting the full technical report as JSON': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`
94
+ });
95
+ }
85
96
  }
86
97
  }
87
98
  // Get a response.
@@ -96,12 +107,12 @@ exports.response = async args => {
96
107
  },
97
108
  method: 'POST',
98
109
  payload: {
99
- 'all or part of a description of the web page': what,
100
- 'all or part of the hostname of the URL of the web page': hostname
110
+ description,
111
+ hostname
101
112
  },
102
113
  URLs: {
103
- 'URL of your request': `${thisHost}/api/target`,
104
- 'equivalent URL for humans': 'none'
114
+ 'for you': `${thisHost}/api/target`,
115
+ 'for humans': 'none'
105
116
  },
106
117
  'closest ancestor request': null
107
118
  },
package/targets/index.js CHANGED
@@ -80,8 +80,10 @@ const populateQuery = async query => {
80
80
  for (const targetLog of targetLogs) {
81
81
  const {jobName, url, what} = targetLog;
82
82
  const [timeStamp, jobID] = jobName.split('-');
83
+ // Get data about its report.
83
84
  const reportData = await getReportData(timeStamp, jobID);
84
85
  const {
86
+ error,
85
87
  issueCount,
86
88
  preventedToolCount,
87
89
  preventedToolNames,
@@ -89,6 +91,15 @@ const populateQuery = async query => {
89
91
  reporterCount,
90
92
  violatorCount
91
93
  } = reportData;
94
+ // If this failed:
95
+ if (error) {
96
+ console.error(error);
97
+ // Populate the query with the reason.
98
+ query.error = error;
99
+ // Stop populating the query.
100
+ return;
101
+ }
102
+ // Otherwise, i.e. if it succeeded, add lines about the report.
92
103
  lines.tested.push(`${margin}<details>`);
93
104
  const daysAgo = getAgoDays(timeStamp);
94
105
  const pageDataStrings = await getPageDataStrings(timeStamp, jobID, {what, url, daysAgo});
@@ -155,7 +166,15 @@ exports.answer = async () => {
155
166
  const query = {};
156
167
  // Create a query to replace placeholders.
157
168
  await populateQuery(query);
158
- // Get the template.
169
+ // If the query reports an error:
170
+ if (query.error) {
171
+ // Return it.
172
+ return {
173
+ status: 'error',
174
+ message: query.error
175
+ };
176
+ }
177
+ // Otherwise, i.e. if it does not report an error, get the template.
159
178
  let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
160
179
  // Replace its placeholders.
161
180
  Object.keys(query).forEach(param => {
@@ -41,8 +41,8 @@ exports.response = async (what, url, why) => {
41
41
  'reason for testing the web page': why
42
42
  },
43
43
  URLs: {
44
- 'URL of your request': `${thisHost}/api/testRecForm`,
45
- 'equivalent URL for humans': `${thisHost}/testRecForm.html`
44
+ 'for you': `${thisHost}/api/testRecForm`,
45
+ 'for humans': `${thisHost}/testRecForm.html`
46
46
  },
47
47
  'closest ancestor request': null
48
48
  },
package/tutorial/index.js CHANGED
@@ -5,7 +5,8 @@
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getJSON} = require('../util');
8
+ const {sendAlert} = require('../alerts');
9
+ const {getJSON, getNowStamp} = require('../util');
9
10
  const fs = require('fs/promises');
10
11
  const path = require('path');
11
12
 
@@ -31,7 +32,7 @@ exports.answer = async () => {
31
32
  };
32
33
  };
33
34
  // Sanitizes and saves a tutorial comment to comments.json.
34
- exports.saveComment = async content => {
35
+ exports.handleComment = async content => {
35
36
  if (!content || typeof content !== 'string') {
36
37
  return {status: 'error', message: 'No content provided'};
37
38
  }
@@ -41,13 +42,22 @@ exports.saveComment = async content => {
41
42
  }
42
43
  let comments = [];
43
44
  try {
45
+ // Get the existing comments.
44
46
  const existing = await fs.readFile(commentsPath, 'utf8');
45
47
  comments = JSON.parse(existing);
46
48
  }
47
- catch (_) {
48
- // File absent or unparsable; start a fresh array.
49
+ // If there are none:
50
+ catch {
51
+ // Initialize a comments array.
49
52
  }
50
- comments.push({timeStamp: new Date().toISOString(), content: sanitized});
53
+ // Add the comment to the existing ones.
54
+ comments.push({
55
+ timeStamp: getNowStamp(),
56
+ content: sanitized
57
+ });
58
+ // Save the revised comments.
51
59
  await fs.writeFile(commentsPath, getJSON(comments));
60
+ // Send an alert to the manager.
61
+ await sendAlert('New tutorial comment received', 'A new tutorial comment has been received.');
52
62
  return {status: 'ok'};
53
63
  };
package/util.js CHANGED
@@ -37,6 +37,7 @@ const tools = exports.tools = {
37
37
  wave: ['WAVE', 'Utah State University'],
38
38
  wax: ['WallyAX', 'Wally']
39
39
  };
40
+ exports.ruleEngines = tools;
40
41
  exports.researchAgents = {
41
42
  'research-agent': 'Internal Research Agent'
42
43
  }
@@ -83,7 +84,7 @@ const getRecordPath = exports.getRecordPath = (recordType, timeStamp, jobID) =>
83
84
  return null;
84
85
  }
85
86
  };
86
- // Returns a log or report or an error message.
87
+ // Returns a log or report.
87
88
  const getRecord = exports.getRecord = async (recordType, timeStamp, jobID) => {
88
89
  const recordPath = getRecordPath(recordType, timeStamp, jobID);
89
90
  let recordJSON, record;
@@ -92,18 +93,18 @@ const getRecord = exports.getRecord = async (recordType, timeStamp, jobID) => {
92
93
  }
93
94
  catch(error) {
94
95
  console.log(error.message);
95
- return `ERROR: Requested ${recordType} ${timeStamp}-${jobID} not found`;
96
+ recordJSON = JSON.stringify({error: `Requested ${recordType} ${timeStamp}-${jobID} not found`});
96
97
  }
97
98
  try {
98
99
  record = JSON.parse(recordJSON);
99
100
  }
100
101
  catch (error) {
101
102
  console.log(error.message);
102
- return `ERROR: Requested ${recordType} ${timeStamp}-${jobID} not JSON`;
103
+ record = {error: `Requested ${recordType} ${timeStamp}-${jobID} not JSON`};
103
104
  }
104
105
  return record;
105
106
  };
106
- // Returns a report or an error message.
107
+ // Returns a report.
107
108
  const getReport = exports.getReport = async (timeStamp, jobID) => await getRecord(
108
109
  'report', timeStamp, jobID
109
110
  );
@@ -246,13 +247,13 @@ const getIssue = exports.getIssue = (ruleIDs, toolID, ruleID) => {
246
247
  // Adds issue IDs to the standard instances of a report.
247
248
  const annotateReport = exports.annotateReport = async (ruleIDs, timeStamp, jobID) => {
248
249
  // Get a copy of the report.
249
- const reportOrError = await getReport(timeStamp, jobID);
250
+ const report = await getReport(timeStamp, jobID);
250
251
  // If this failed:
251
- if (typeof reportOrError === 'string') {
252
- // Return this.
253
- return reportOrError;
252
+ if (report.error) {
253
+ // Return why.
254
+ return report.error;
254
255
  }
255
- const report = reportOrError;
256
+ // Otherwise, i.e. if it succeeded:
256
257
  const unclassifiableRules = new Set();
257
258
  // For each of its acts:
258
259
  for (const act of report.acts) {
@@ -293,36 +294,35 @@ const annotateReport = exports.annotateReport = async (ruleIDs, timeStamp, jobID
293
294
  // Save the annotated report.
294
295
  await fs.writeFile(getReportPath(timeStamp, jobID), getJSON(report));
295
296
  // Get a copy of the log of the report.
296
- const logOrError = await getLog(timeStamp, jobID, false);
297
+ const log = await getLog(timeStamp, jobID, false);
297
298
  // If this failed:
298
- if (typeof logOrError === 'string') {
299
- // Return this.
300
- return logOrError;
299
+ if (log.error) {
300
+ // Return why.
301
+ return log.error;
301
302
  }
302
303
  // Otherwise, i.e. if it succeeded:
303
304
  else {
304
- const log = logOrError;
305
305
  // Mark the report as annotated in the log.
306
306
  log.annotated = true;
307
307
  // Save the revised log.
308
308
  await fs.writeFile(getLogPath(timeStamp, jobID), getJSON(log));
309
- // Return without an error message.
309
+ // Return success.
310
+ return '';
310
311
  }
311
312
  };
312
- // Returns a report log after conditionally annotating it or an error message.
313
+ // Returns a report log after conditionally annotating it.
313
314
  const getLog = exports.getLog = async (timeStamp, jobID, annotate = false) => {
314
315
  const log = await getRecord('log', timeStamp, jobID);
315
- if (typeof log === 'object' && annotate && ! log.annotated) {
316
- annotateReport(ruleIDs, timeStamp, jobID);
316
+ if (annotate && ! (log.error || log.annotated)) {
317
+ await annotateReport(ruleIDs, timeStamp, jobID);
317
318
  }
318
319
  return log;
319
320
  };
320
- // Returns whether a report is hidden or an error message.
321
+ // Returns whether a report is hidden.
321
322
  exports.isHidden = async (timeStamp, jobID) => {
323
+ // Get the log of the report.
322
324
  const log = await getLog(timeStamp, jobID, false);
323
- if (typeof log === 'string') {
324
- return log;
325
- }
325
+ // Return whether the log exists and its report is hidden.
326
326
  return !! log.hidden;
327
327
  };
328
328
  // Returns summary data on a report.
@@ -330,9 +330,9 @@ exports.getReportData = async (timeStamp, jobID) => {
330
330
  // Validate the report and annotate it if necessary.
331
331
  const log = await getLog(timeStamp, jobID, true);
332
332
  // If this failed:
333
- if (typeof log === 'string') {
334
- // Return this.
335
- return log;
333
+ if (log.error) {
334
+ // Return why.
335
+ return {error: log.error};
336
336
  }
337
337
  // Initialize the data.
338
338
  const data = {
@@ -357,9 +357,9 @@ exports.getReportData = async (timeStamp, jobID) => {
357
357
  // Get the report.
358
358
  const report = await getReport(timeStamp, jobID);
359
359
  // If this failed:
360
- if (typeof report === 'string') {
361
- // Return this.
362
- return report;
360
+ if (report.error) {
361
+ // Return why.
362
+ return {error: report.error};
363
363
  }
364
364
  // For each act of the report:
365
365
  report.acts.forEach(act => {
@@ -526,30 +526,31 @@ const getLogs = exports.getLogs = async () => {
526
526
  try {
527
527
  logFileNames = await fs.readdir(logsPath);
528
528
  }
529
- catch(error) {
530
- return `ERROR: logs directory not readable (${error.message})`;
529
+ catch {
530
+ console.error('Logs directory not readable');
531
+ logFileNames = [];
531
532
  }
532
533
  // For each log:
533
534
  for (const fileName of logFileNames) {
534
535
  const logName = fileName.slice(0, -5);
535
536
  const [timeStamp, jobID] = logName.split('-');
536
537
  // Get it.
537
- const logOrError = await getLog(timeStamp, jobID);
538
+ const log = await getLog(timeStamp, jobID);
538
539
  // If this failed:
539
- if (typeof logOrError === 'string') {
540
- // Return this.
541
- return logOrError;
540
+ if (log.error) {
541
+ // Report why.
542
+ console.error(log.error);
542
543
  }
543
- const log = logOrError;
544
- // If the report is hidden:
545
- if (log.hidden) {
546
- // Disregard it.
547
- continue;
544
+ // Otherwise, i.e. if it succeeded:
545
+ else {
546
+ // If the report is not hidden:
547
+ if (! log.hidden) {
548
+ // Add the job name to the log.
549
+ log.jobName = logName;
550
+ // Add the log to the logs.
551
+ logs.push(log);
552
+ }
548
553
  }
549
- // Add the job name to the log.
550
- log.jobName = logName;
551
- // Add the log to the logs.
552
- logs.push(log);
553
554
  }
554
555
  // Sort the logs by target name and secondarily by test time.
555
556
  logs.sort((a, b) => {
@@ -692,12 +693,12 @@ const getPageData = exports.getPageData = async (timeStamp, jobID) => {
692
693
  // Get the log of the report.
693
694
  const log = await getLog(timeStamp, jobID, false);
694
695
  // If this failed:
695
- if (typeof log === 'string') {
696
- // Return the error.
696
+ if (log.error) {
697
+ // Return why.
697
698
  return log;
698
699
  }
699
700
  const {url, what} = log;
700
- // Get the elapsed time in days since the test.
701
+ // Otherwise, i.e. if it succeeded, get the elapsed time in days since the test.
701
702
  const daysAgo = getAgoDays(timeStamp);
702
703
  // Return the data.
703
704
  return {
@@ -713,7 +714,15 @@ exports.getPageDataStrings = async (timeStamp, jobID, pageData) => {
713
714
  // Get them.
714
715
  pageData = await getPageData(timeStamp, jobID);
715
716
  }
716
- const {what, url, daysAgo} = pageData;
717
+ const {daysAgo, error, url, what} = pageData;
718
+ // If the page data are invalid:
719
+ if (error) {
720
+ // Return why.
721
+ return {
722
+ error
723
+ };
724
+ }
725
+ // Otherwise, i.e. if they are valid, get a description of the timestamp.
717
726
  const when = getDateTimeString(timeStamp);
718
727
  // Return the HTML strings.
719
728
  return {
@@ -766,7 +775,13 @@ exports.isReportAvailable = async (what, url) => {
766
775
  };
767
776
  // Returns the size of a report.
768
777
  exports.getReportSize = async (timeStamp, jobID) => {
769
- const reportStat = await fs.stat(path.join(reportsPath, `${timeStamp}-${jobID}.json`));
778
+ const reportStat = await fs.stat(
779
+ path.join(reportsPath, `${timeStamp}-${jobID}.json`),
780
+ {throwIfNoEntry: false}
781
+ );
782
+ if (! reportStat) {
783
+ return 0;
784
+ }
770
785
  const reportSize = reportStat.size;
771
786
  return reportSize;
772
787
  };
@@ -1,129 +0,0 @@
1
- /*
2
- api.js
3
- Responds to the report-issues request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getData} = require('./util');
9
- const {
10
- getDateTime,
11
- getNowStamp,
12
- getRandomString,
13
- getToolsFacts,
14
- isHidden,
15
- tools
16
- } = require('../util');
17
-
18
- // FUNCTIONS
19
-
20
- // Gets facts about an issue.
21
- const getIssueFacts = (thisHost, timeStamp, jobID, issue) => {
22
- const {issueID, reporterCount, reporters, summary, violatorCount, wcag, why} = issue;
23
- const wcagType = wcag.length === 3 ? 'guideline' : 'success criterion';
24
- return {
25
- identifier: issueID,
26
- summary,
27
- 'related WCAG 2.2 standard': {
28
- layer: wcagType,
29
- 'numeric identifier': wcag
30
- },
31
- 'impact on a user': why,
32
- 'rule engines reporting the issue': {
33
- 'number': reporterCount,
34
- 'names': reporters.map(tool => tool.toolName)
35
- },
36
- 'number of HTML elements reported as exhibiting the issue': violatorCount,
37
- 'URLs for details about the issue on the page': {
38
- 'for you': `${thisHost}/api/reportIssue/${issueID}/${timeStamp}/${jobID}`,
39
- 'for humans': `${thisHost}/reportIssue.html/${issueID}/${timeStamp}/${jobID}`
40
- }
41
- };
42
- };
43
- // Returns a response to a report-issues request.
44
- exports.response = async args => {
45
- const [timeStamp, jobID] = args;
46
- const reportIsHidden = await isHidden(timeStamp, jobID);
47
- // If the report is not available:
48
- if (reportIsHidden) {
49
- return {
50
- status: 'error',
51
- message: 'Report not available'
52
- };
53
- }
54
- // Otherwise, i.e. if the report is available, get data on the target and issues.
55
- const data = await getData(timeStamp, jobID);
56
- const {pageData, issuesData} = data;
57
- const {what, url, daysAgo} = pageData;
58
- const {issueCount, issues, preventions, reporterCount, reporters, violatorCount} = issuesData;
59
- const preventedTools = Object.entries(preventions).map(prevention => ({
60
- name: tools[prevention[0]][0],
61
- 'reason for failure': prevention[1]
62
- }));
63
- const thisHost = process.env.THIS_KILOTEST_HOST;
64
- // Get a response.
65
- const content = {
66
- summary: `This document fulfills a request made by a language model to a Kilotest tool. The model requested data from a Kilotest report about the front-end quality (i.e. accessibility, usability, and standards conformity) of a web page. Kilotest, with the help of Testaro, Testilo, and an ensemble of ten rule engines, performs tests on web pages, using a combination of rule- and machine-learning-based methods, and produces reports. Kilotest exposes several API endpoints to recommend web pages for testing and to obtain information from Kilotest reports. To learn more about Kilotest and the advantages of testing with an ensemble of rule engines, visit the deployed instance of Kilotest (${process.env.DEPLOYED_KILOTEST_HOST}), which contains an introduction on its home page and a tutorial.`,
67
- 'tool collection name': 'Kilotest',
68
- 'tool name': 'describeQualityOfOneWebPage',
69
- request: {
70
- 'type of request': {
71
- identifier: 'reportIssues',
72
- description: 'Describe the quality of one web page.'
73
- },
74
- method: 'GET',
75
- URLs: {
76
- 'URL of your request': `${thisHost}/api/reportIssues/${timeStamp}/${jobID}`,
77
- 'equivalent URL for humans': `${thisHost}/reportIssues.html/${timeStamp}/${jobID}`
78
- },
79
- 'closest ancestor request': {
80
- identifier: 'getListOfAllAvailableReports',
81
- description: 'Get a list of all available reports.',
82
- URLs: {
83
- 'for you': `${thisHost}/api/reportList`,
84
- 'for humans': `${thisHost}/targets.html`
85
- }
86
- }
87
- },
88
- 'response metadata': {
89
- identifier: `${getNowStamp()}-${getRandomString(3)}`,
90
- 'date and time': new Date().toISOString()
91
- },
92
- report: {
93
- identifier: `${timeStamp}-${jobID}`,
94
- 'creation date': getDateTime(timeStamp),
95
- 'days since the creation date': daysAgo
96
- },
97
- 'tested web page': {
98
- description: what,
99
- URL: url
100
- },
101
- 'rule engines that tried to test the page': getToolsFacts(Object.keys(tools)),
102
- 'rule engines that were unable to test the page': preventedTools,
103
- 'rule engines that reported issues': {
104
- number: reporterCount,
105
- names: reporters.map(tool => tool.toolName)
106
- },
107
- 'number of issues reported': {
108
- total: issueCount,
109
- 'by priority': {
110
- 'highest priority': issues[4].length,
111
- 'high priority': issues[3].length,
112
- 'low priority': issues[2].length,
113
- 'lowest priority': issues[1].length
114
- }
115
- },
116
- 'number of HTML elements reported as exhibiting issues': violatorCount,
117
- 'issues reported': {
118
- 'highest priority': issues[4]
119
- .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
120
- 'high priority': issues[3]
121
- .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
122
- 'low priority': issues[2]
123
- .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue)),
124
- 'lowest priority': issues[1]
125
- .map(issue => getIssueFacts(thisHost, timeStamp, jobID, issue))
126
- }
127
- };
128
- return content;
129
- };