@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.
- package/.devin/config.local.json +4 -1
- package/api/listReports.js +4 -2
- package/api/util.js +24 -20
- package/apiIntegration.test.js +1 -1
- package/index.js +6 -0
- package/index.test.js +89 -1
- package/package.json +1 -1
- package/smokeTest.js +6 -4
- package/util.js +11 -0
package/.devin/config.local.json
CHANGED
package/api/listReports.js
CHANGED
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// If the
|
|
75
|
-
if (!
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
//
|
|
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(
|
|
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}`,
|
package/apiIntegration.test.js
CHANGED
|
@@ -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
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
|
|
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.
|