@jrpool/kilotest 46.1.0 → 46.1.2

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.
@@ -29,7 +29,10 @@
29
29
  "Exec($DOCKER exec)",
30
30
  "Exec($DOCKER stop)",
31
31
  "Exec(sed)",
32
- "Exec(awk)"
32
+ "Exec(awk)",
33
+ "Exec(nvm ls)",
34
+ "Exec(.)",
35
+ "Exec(nvm alias)"
33
36
  ]
34
37
  }
35
38
  }
@@ -6,7 +6,7 @@
6
6
  // IMPORTS
7
7
 
8
8
  const {getReportBasics, getResponseMetadata, getToolsFacts, thisHost} = require('./util');
9
- const {getReportExtracts} = require('../util');
9
+ const {getLatestReportExtracts, getReportExtracts} = require('../util');
10
10
 
11
11
  // FUNCTIONS
12
12
 
@@ -22,11 +22,13 @@ exports.response = async () => {
22
22
  const reportsBasics = [];
23
23
  // Get extracts of all available reports.
24
24
  const reportExtracts = await getReportExtracts();
25
+ // Get the latest report extracts once, to avoid recomputing them for every report.
26
+ const latestReportExtracts = await getLatestReportExtracts();
25
27
  // For each report:
26
28
  for (const extract of reportExtracts) {
27
29
  const {error, jobID, timeStamp} = extract;
28
30
  // Get the basics about it (which may be only an error message).
29
- const reportBasics = await getReportBasics(timeStamp, jobID);
31
+ const reportBasics = await getReportBasics(timeStamp, jobID, extract, latestReportExtracts);
30
32
  // If this succeeded:
31
33
  if (!error) {
32
34
  // Add instructions for getting details to the basics.
package/api/util.js CHANGED
@@ -68,28 +68,32 @@ exports.getRuleEnginesFacts = ruleEngineIDSet => {
68
68
  return ruleEnginesFacts;
69
69
  };
70
70
  // Returns the basics about a report.
71
- exports.getReportBasics = async (timeStamp, jobID) => {
72
- // Get the creation time of the report.
73
- const reportStats = await getReportStats(timeStamp, jobID);
74
- // If the report does not exist:
75
- if (!reportStats) {
76
- // Log and return this.
77
- console.error(`Report ${timeStamp}-${jobID} does not exist.`);
78
- return {
79
- error: `Report ${timeStamp}-${jobID} could not be retrieved.`
80
- };
71
+ // Accepts optional precomputed extract and latestReportExtracts to avoid redundant
72
+ // reads when called in a loop over all reports (e.g. by listReports).
73
+ exports.getReportBasics = async (timeStamp, jobID, extract = null, latestReportExtracts = null) => {
74
+ // If an extract was not provided, verify the report exists and read it.
75
+ if (!extract) {
76
+ // Get the creation time of the report.
77
+ const reportStats = await getReportStats(timeStamp, jobID);
78
+ // If the report does not exist:
79
+ if (!reportStats) {
80
+ // Log and return this.
81
+ console.error(`Report ${timeStamp}-${jobID} does not exist.`);
82
+ return {
83
+ error: `Report ${timeStamp}-${jobID} could not be retrieved.`
84
+ };
85
+ }
86
+ // Otherwise, i.e. if it exists, get an extract of the report.
87
+ extract = await getReportExtract(timeStamp, jobID);
88
+ }
89
+ const {url, what, reportTime} = extract;
90
+ // If the latest report extracts were not provided, compute them.
91
+ if (!latestReportExtracts) {
92
+ latestReportExtracts = await getLatestReportExtracts();
81
93
  }
82
- // Otherwise, i.e. if it exists, get an extract of the report.
83
- const reportExtract = await getReportExtract(timeStamp, jobID);
84
- const {url, what} = reportExtract;
85
- const latestReportExtracts = await getLatestReportExtracts();
86
- // Otherwise, i.e. if the report exists, get whether this report has been superseded.
94
+ // Get whether this report has been superseded.
87
95
  const isSuperseded = latestReportExtracts
88
- .every(extract => extract.timeStamp !== timeStamp || extract.jobID !== jobID);
89
- // Get the completion time from the report content, not the file system birth time,
90
- // because the birth time depends on when the file was created on the file system
91
- // and does not reflect the actual report completion time.
92
- const {reportTime} = reportExtract;
96
+ .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
93
97
  // Get the basics about the report.
94
98
  const basics = {
95
99
  identifier: `${timeStamp}-${jobID}`,
@@ -77,7 +77,7 @@ const request = (method, requestPath, body = null) => new Promise((resolve, reje
77
77
 
78
78
  // TESTS
79
79
 
80
- test('GET /api/listReports returns valid JSON through the HTTP routing layer', async () => {
80
+ test('GET /api/listReports returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
81
81
  const {body} = await request('GET', '/api/listReports');
82
82
  assert.equal(body['tool name'], 'listReports');
83
83
  const reports = body['response content']['basics about all available reports'];
package/index.js CHANGED
@@ -360,6 +360,12 @@ const requestHandler = async (request, response) => {
360
360
  const {pathname, search} = requestURL;
361
361
  const pageName = pathname.split('/')[1];
362
362
  const pathTail = pathname.split('/').slice(2).join('/');
363
+ // If the request is a smoke-test probe, respond perfunctorily without executing a handler.
364
+ if (request.headers['x-kilotest-smoke']) {
365
+ response.setHeader('content-type', 'application/json; charset=utf-8');
366
+ response.end('{}');
367
+ return;
368
+ }
363
369
  // If the request is a GET request:
364
370
  if (method === 'GET') {
365
371
  // If the path is not authorized for GET requests:
package/index.test.js CHANGED
@@ -147,6 +147,26 @@ test('routes has GET and POST arrays', () => {
147
147
  assert.ok(Array.isArray(routes.POST));
148
148
  });
149
149
 
150
+ // TESTS: smoke-test short-circuit
151
+
152
+ test('GET with x-kilotest-smoke header returns a perfunctory 200', async () => {
153
+ const res = await request('GET', '/api/listReports', null, {'x-kilotest-smoke': '1'});
154
+ assert.equal(res.statusCode, 200);
155
+ assert.equal(res.body, '{}');
156
+ });
157
+
158
+ test('POST with x-kilotest-smoke header returns a perfunctory 200', async () => {
159
+ const res = await request('POST', '/api/requestFeature', {}, {'x-kilotest-smoke': '1'});
160
+ assert.equal(res.statusCode, 200);
161
+ assert.equal(res.body, '{}');
162
+ });
163
+
164
+ test('GET without x-kilotest-smoke header executes the handler', async () => {
165
+ const res = await request('GET', '/api/listReports');
166
+ assert.equal(res.statusCode, 200);
167
+ assert.notEqual(res.body, '{}');
168
+ });
169
+
150
170
  // TESTS: GET routes
151
171
 
152
172
  test('GET / serves the home page as HTML', async () => {
@@ -1050,12 +1070,21 @@ test('getAbuseError uses unknown IP when no forwarding header or remote address'
1050
1070
 
1051
1071
  // UNIT TESTS FOR startServer
1052
1072
 
1053
- test('startServer starts an HTTP server when protocol is http', async () => {
1073
+ test('startServer starts an HTTP server when protocol is http', {timeout: 500}, async () => {
1054
1074
  // Delete PROTOCOL to exercise the 'http' fallback in startServer.
1055
1075
  const savedProtocol = process.env.PROTOCOL;
1076
+ // Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
1077
+ const savedPort = process.env.PORT;
1078
+ process.env.PORT = '0';
1056
1079
  delete process.env.PROTOCOL;
1057
1080
  const server = await startServer();
1058
1081
  process.env.PROTOCOL = savedProtocol;
1082
+ if (savedPort !== undefined) {
1083
+ process.env.PORT = savedPort;
1084
+ }
1085
+ else {
1086
+ delete process.env.PORT;
1087
+ }
1059
1088
  try {
1060
1089
  assert.ok(server);
1061
1090
  assert.equal(typeof server.listen, 'function');
@@ -1078,6 +1107,56 @@ test('startServer starts an HTTP server when protocol is http', async () => {
1078
1107
  }
1079
1108
  });
1080
1109
 
1110
+ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, async () => {
1111
+ // Exercise the '|| 3000' fallback branch in serve by deleting PORT and calling
1112
+ // serve directly. The server will attempt to bind to port 3000. On a system
1113
+ // where port 3000 is free, it succeeds and we close it. Where it is occupied,
1114
+ // the error event fires and we accept that as proof the fallback was reached.
1115
+ const savedPort = process.env.PORT;
1116
+ delete process.env.PORT;
1117
+ try {
1118
+ const server = await indexModule.serve(http, {});
1119
+ let bindError = null;
1120
+ server.on('error', error => {
1121
+ bindError = error;
1122
+ });
1123
+ // Wait briefly for either successful binding or an EADDRINUSE error.
1124
+ await new Promise(resolve => {
1125
+ const timer = setTimeout(resolve, 200);
1126
+ server.on('listening', () => {
1127
+ clearTimeout(timer);
1128
+ resolve();
1129
+ });
1130
+ });
1131
+ if (bindError) {
1132
+ assert.equal(bindError.code, 'EADDRINUSE', `Expected EADDRINUSE, got: ${bindError.message}`);
1133
+ }
1134
+ else {
1135
+ const address = server.address();
1136
+ assert.ok(typeof address === 'object' && address.port === 3000);
1137
+ }
1138
+ server.closeAllConnections?.();
1139
+ await new Promise(resolve => {
1140
+ const timer = setTimeout(() => {
1141
+ server.closeAllConnections?.();
1142
+ resolve();
1143
+ }, 1000);
1144
+ server.close(() => {
1145
+ clearTimeout(timer);
1146
+ resolve();
1147
+ });
1148
+ });
1149
+ }
1150
+ finally {
1151
+ if (savedPort !== undefined) {
1152
+ process.env.PORT = savedPort;
1153
+ }
1154
+ else {
1155
+ delete process.env.PORT;
1156
+ }
1157
+ }
1158
+ });
1159
+
1081
1160
  test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
1082
1161
  // Generate a self-signed certificate for the test using openssl.
1083
1162
  const {execSync} = require('node:child_process');
@@ -1094,6 +1173,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
1094
1173
  const savedProtocol = process.env.PROTOCOL;
1095
1174
  const savedKey = process.env.KEY;
1096
1175
  const savedCert = process.env.CERT;
1176
+ // Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
1177
+ const savedPort = process.env.PORT;
1178
+ process.env.PORT = '0';
1097
1179
  process.env.PROTOCOL = 'https';
1098
1180
  process.env.KEY = keyPath;
1099
1181
  process.env.CERT = certPath;
@@ -1134,6 +1216,12 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
1134
1216
  else {
1135
1217
  delete process.env.CERT;
1136
1218
  }
1219
+ if (savedPort !== undefined) {
1220
+ process.env.PORT = savedPort;
1221
+ }
1222
+ else {
1223
+ delete process.env.PORT;
1224
+ }
1137
1225
  fsSync.rmSync(tmpDir, {recursive: true, force: true});
1138
1226
  }
1139
1227
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "46.1.0",
3
+ "version": "46.1.2",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "main": "index.js",
6
6
  "engines": {
package/smokeTest.js CHANGED
@@ -68,14 +68,16 @@ const postBodies = {
68
68
  // Sends an HTTPS request and returns the status code and body length.
69
69
  const sendRequest = (method, requestPath) => new Promise((resolve, reject) => {
70
70
  const body = method === 'POST' ? JSON.stringify(postBodies[requestPath] || {}) : null;
71
+ const headers = {'x-kilotest-smoke': '1'};
72
+ if (body) {
73
+ headers['content-type'] = 'application/json; charset=utf-8';
74
+ headers['content-length'] = Buffer.byteLength(body);
75
+ }
71
76
  const options = {
72
77
  method,
73
78
  host,
74
79
  path: requestPath,
75
- headers: body ? {
76
- 'content-type': 'application/json; charset=utf-8',
77
- 'content-length': Buffer.byteLength(body)
78
- } : {}
80
+ headers
79
81
  };
80
82
  const req = https.request(options, response => {
81
83
  const chunks = [];
package/util.js CHANGED
@@ -673,12 +673,14 @@ exports.isHidden = async (timeStamp, jobID) => {
673
673
  // Returns an extract of an available report, or an error object if it cannot be read or parsed.
674
674
  const getReportExtract = exports.getReportExtract = async (timeStamp, jobID) => {
675
675
  try {
676
+ // Get the report.
676
677
  const reportJSON = await fs.readFile(
677
678
  path.join(reportsPath(), `${timeStamp}-${jobID}.json`), 'utf8'
678
679
  );
679
680
  const report = JSON.parse(reportJSON);
680
681
  const {target, jobData} = report;
681
682
  const {what, url} = target;
683
+ // Return an extract of it.
682
684
  return {
683
685
  timeStamp,
684
686
  jobID,
@@ -695,12 +697,17 @@ const getReportExtract = exports.getReportExtract = async (timeStamp, jobID) =>
695
697
  };
696
698
  // Returns extracts of all available reports.
697
699
  const getReportExtracts = exports.getReportExtracts = async () => {
700
+ // Get the names of the available report files.
698
701
  const reportFileNames = await fs.readdir(reportsPath());
702
+ // Initialize an array of extracts.
699
703
  const extracts = [];
704
+ // For each one:
700
705
  for (const reportFileName of reportFileNames) {
701
706
  const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-');
707
+ // Get an extract of it.
702
708
  const extract = await getReportExtract(timeStamp, jobID);
703
709
  if (!extract.error) {
710
+ // Add the extract to the array.
704
711
  extracts.push(extract);
705
712
  }
706
713
  }
@@ -715,11 +722,15 @@ exports.isReportAvailable = async (what, url) => {
715
722
  };
716
723
  // Gets extracts of the latest available reports for all page descriptions.
717
724
  exports.getLatestReportExtracts = async () => {
725
+ // Get extracts of all available reports.
718
726
  const reportExtracts = await getReportExtracts();
727
+ // Sort them by page description and, secondarily, completion time.
719
728
  objectSort(reportExtracts, 'reportTime', 'alpha');
720
729
  objectSort(reportExtracts, 'what', 'alpha');
730
+ // Get those that are not superseded.
721
731
  const latestReportExtracts = reportExtracts
722
732
  .filter((extract, index) => extract.what !== reportExtracts[index + 1]?.what);
733
+ // Return them.
723
734
  return latestReportExtracts;
724
735
  };
725
736
  // Gets the descriptions of multi-report pages.