@jrpool/kilotest 46.1.0 → 47.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +8 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
- package/api/{requestRetest.js → requestRetest.ts} +32 -25
- package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
- package/api/{requestTest.js → requestTest.ts} +35 -28
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +31 -11
- package/api/{util.js → util.ts} +75 -46
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +356 -0
- package/docs/SERVICE.md +144 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +462 -137
- package/{index.js → index.ts} +388 -283
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/{util.test.js → util.test.ts} +350 -257
- package/util.ts +921 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +48 -53
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +70 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +31 -0
- package/web/requestRetest/requestRetest.test.ts +116 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/{index.js → index.ts} +5 -5
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +17 -21
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -87
- package/api/requestFeature.test.js +0 -58
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -126
- package/util.js +0 -733
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,47 +1,50 @@
|
|
|
1
1
|
/*
|
|
2
|
-
index.test.
|
|
2
|
+
index.test.ts
|
|
3
3
|
Integration tests for index.js requestHandler, covering GET routes, POST routes,
|
|
4
4
|
error paths, worker authentication, and helper function behavior.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// ENVIRONMENT (
|
|
8
|
-
//
|
|
7
|
+
// ENVIRONMENT (the test environment configuration; all modules now read
|
|
8
|
+
// process.env at call time, so load order no longer matters.)
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import {fixtureDBDir} from './test/dbFixture.ts';
|
|
12
12
|
|
|
13
|
+
const testCommentsDir = path.join(import.meta.dirname, 'test/fixtures/comments');
|
|
13
14
|
process.env.DB_DIR = fixtureDBDir;
|
|
14
15
|
process.env.AUTH_CODE = 'test-auth-code';
|
|
16
|
+
process.env.TUTORIAL_WEB_COMMENTS_PATH = path.join(testCommentsDir, 'tutorialWeb.json');
|
|
17
|
+
process.env.TUTORIAL_AI_COMMENTS_PATH = path.join(testCommentsDir, 'tutorialAI.json');
|
|
15
18
|
process.env.TESTARO_WORKERS = JSON.stringify({
|
|
16
19
|
worker1: {secret: 'secret1', name: 'Worker One'}
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
// IMPORTS
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
import {test, before, beforeEach, after} from 'node:test';
|
|
25
|
+
import assert from 'node:assert/strict';
|
|
26
|
+
import http from 'node:http';
|
|
27
|
+
import fs from 'node:fs/promises';
|
|
28
|
+
import * as indexModule from './index.ts';
|
|
26
29
|
const {requestHandler, routes, serveError, startServer, runIfMain, isPathAllowed, getAbuseError} = indexModule;
|
|
27
30
|
|
|
28
31
|
// CONSTANTS
|
|
29
32
|
|
|
30
33
|
const port = 3997;
|
|
31
|
-
const
|
|
34
|
+
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
32
35
|
|
|
33
36
|
// SETUP AND TEARDOWN
|
|
34
37
|
|
|
35
|
-
let server;
|
|
38
|
+
let server: http.Server;
|
|
36
39
|
|
|
37
40
|
before(async () => {
|
|
38
41
|
server = http.createServer(requestHandler);
|
|
39
|
-
await new Promise(resolve => server.listen(port, () => resolve()));
|
|
42
|
+
await new Promise<void>(resolve => server.listen(port, () => resolve()));
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
// Restore
|
|
45
|
+
// Restore testRequests.json and clean job directories before each test, so tests do not depend on execution order.
|
|
43
46
|
beforeEach(async () => {
|
|
44
|
-
await fs.writeFile(
|
|
47
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
45
48
|
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
46
49
|
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
47
50
|
await fs.mkdir(dir, {recursive: true});
|
|
@@ -50,12 +53,16 @@ beforeEach(async () => {
|
|
|
50
53
|
await fs.unlink(path.join(dir, file)).catch(() => {});
|
|
51
54
|
}
|
|
52
55
|
}
|
|
56
|
+
// Create test comments directory and reset test comment files so tests start clean.
|
|
57
|
+
await fs.mkdir(testCommentsDir, {recursive: true});
|
|
58
|
+
await fs.writeFile(path.join(testCommentsDir, 'tutorialWeb.json'), '[]\n');
|
|
59
|
+
await fs.writeFile(path.join(testCommentsDir, 'tutorialAI.json'), '[]\n');
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
after(async () => {
|
|
56
63
|
// Close all idle connections, then close the server with a timeout.
|
|
57
64
|
server.closeAllConnections?.();
|
|
58
|
-
await new Promise(resolve => {
|
|
65
|
+
await new Promise<void>(resolve => {
|
|
59
66
|
const timer = setTimeout(() => {
|
|
60
67
|
server.closeAllConnections?.();
|
|
61
68
|
resolve();
|
|
@@ -65,8 +72,8 @@ after(async () => {
|
|
|
65
72
|
resolve();
|
|
66
73
|
});
|
|
67
74
|
});
|
|
68
|
-
// Restore
|
|
69
|
-
await fs.writeFile(
|
|
75
|
+
// Restore testRequests.json and clean job directories after running.
|
|
76
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
70
77
|
for (const sub of ['claimed', 'queue', 'failed']) {
|
|
71
78
|
const dir = path.join(fixtureDBDir, 'jobs', sub);
|
|
72
79
|
await fs.mkdir(dir, {recursive: true});
|
|
@@ -83,7 +90,7 @@ const uniqueStamp = Date.now();
|
|
|
83
90
|
|
|
84
91
|
// HELPERS
|
|
85
92
|
|
|
86
|
-
const request = (method, requestPath, body = null, headers = {}) => new Promise((resolve, reject) => {
|
|
93
|
+
const request = (method: string, requestPath: string, body: any = null, headers: any = {}): Promise<any> => new Promise((resolve, reject) => {
|
|
87
94
|
const options = {method, host: 'localhost', port, path: requestPath, headers: {...headers}};
|
|
88
95
|
let bodyData = '';
|
|
89
96
|
if (body) {
|
|
@@ -97,7 +104,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
97
104
|
options.headers['content-length'] = Buffer.byteLength(bodyData);
|
|
98
105
|
}
|
|
99
106
|
const req = http.request(options, response => {
|
|
100
|
-
const chunks = [];
|
|
107
|
+
const chunks: Buffer[] = [];
|
|
101
108
|
response.on('data', chunk => chunks.push(chunk));
|
|
102
109
|
response.on('end', () => {
|
|
103
110
|
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
@@ -107,7 +114,7 @@ const request = (method, requestPath, body = null, headers = {}) => new Promise(
|
|
|
107
114
|
req.end(bodyData || '');
|
|
108
115
|
});
|
|
109
116
|
|
|
110
|
-
const formRequest = (method, requestPath, formData, headers = {}) => new Promise((resolve, reject) => {
|
|
117
|
+
const formRequest = (method: string, requestPath: string, formData: any, headers: any = {}): Promise<any> => new Promise((resolve, reject) => {
|
|
111
118
|
const body = new URLSearchParams(formData).toString();
|
|
112
119
|
const options = {
|
|
113
120
|
method,
|
|
@@ -121,7 +128,7 @@ const formRequest = (method, requestPath, formData, headers = {}) => new Promise
|
|
|
121
128
|
}
|
|
122
129
|
};
|
|
123
130
|
const req = http.request(options, response => {
|
|
124
|
-
const chunks = [];
|
|
131
|
+
const chunks: Buffer[] = [];
|
|
125
132
|
response.on('data', chunk => chunks.push(chunk));
|
|
126
133
|
response.on('end', () => {
|
|
127
134
|
resolve({statusCode: response.statusCode, headers: response.headers, body: Buffer.concat(chunks).toString()});
|
|
@@ -131,7 +138,7 @@ const formRequest = (method, requestPath, formData, headers = {}) => new Promise
|
|
|
131
138
|
req.end(body);
|
|
132
139
|
});
|
|
133
140
|
|
|
134
|
-
const jsonBody = res => {
|
|
141
|
+
const jsonBody = (res: any) => {
|
|
135
142
|
try {
|
|
136
143
|
return JSON.parse(res.body);
|
|
137
144
|
}
|
|
@@ -147,6 +154,26 @@ test('routes has GET and POST arrays', () => {
|
|
|
147
154
|
assert.ok(Array.isArray(routes.POST));
|
|
148
155
|
});
|
|
149
156
|
|
|
157
|
+
// TESTS: smoke-test short-circuit
|
|
158
|
+
|
|
159
|
+
test('GET with x-kilotest-smoke header returns a perfunctory 200', async () => {
|
|
160
|
+
const res = await request('GET', '/api/listReports', null, {'x-kilotest-smoke': '1'});
|
|
161
|
+
assert.equal(res.statusCode, 200);
|
|
162
|
+
assert.equal(res.body, '{}');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('POST with x-kilotest-smoke header returns a perfunctory 200', async () => {
|
|
166
|
+
const res = await request('POST', '/api/requestFeature', {}, {'x-kilotest-smoke': '1'});
|
|
167
|
+
assert.equal(res.statusCode, 200);
|
|
168
|
+
assert.equal(res.body, '{}');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('GET without x-kilotest-smoke header executes the handler', async () => {
|
|
172
|
+
const res = await request('GET', '/api/listReports');
|
|
173
|
+
assert.equal(res.statusCode, 200);
|
|
174
|
+
assert.notEqual(res.body, '{}');
|
|
175
|
+
});
|
|
176
|
+
|
|
150
177
|
// TESTS: GET routes
|
|
151
178
|
|
|
152
179
|
test('GET / serves the home page as HTML', async () => {
|
|
@@ -304,18 +331,18 @@ test('GET /fullReport.json/990101T0000/xxx returns an abuse error for a nonexist
|
|
|
304
331
|
assert.ok(res.body.includes('Invalid request'));
|
|
305
332
|
});
|
|
306
333
|
|
|
307
|
-
test('GET /
|
|
308
|
-
const res = await request('GET', '/
|
|
334
|
+
test('GET /tutorialWeb/images/newsletter-form.png serves the image', async () => {
|
|
335
|
+
const res = await request('GET', '/tutorialWeb/images/newsletter-form.png');
|
|
309
336
|
assert.equal(res.statusCode, 200);
|
|
310
337
|
assert.ok(res.headers['content-type'].includes('image/png'));
|
|
311
338
|
});
|
|
312
339
|
|
|
313
|
-
test('GET /
|
|
340
|
+
test('GET /tutorialWeb/images with an unknown extension serves octet-stream', async () => {
|
|
314
341
|
// Create a temporary image file with an unknown extension.
|
|
315
|
-
const imgPath = path.join(
|
|
342
|
+
const imgPath = path.join(import.meta.dirname, 'web', 'tutorialWeb', 'images', 'test.bmp');
|
|
316
343
|
await fs.writeFile(imgPath, 'fake bitmap data');
|
|
317
344
|
try {
|
|
318
|
-
const res = await request('GET', '/
|
|
345
|
+
const res = await request('GET', '/tutorialWeb/images/test.bmp');
|
|
319
346
|
assert.equal(res.statusCode, 200);
|
|
320
347
|
assert.ok(res.headers['content-type'].includes('application/octet-stream'));
|
|
321
348
|
}
|
|
@@ -324,13 +351,34 @@ test('GET /tutorial/images with an unknown extension serves octet-stream', async
|
|
|
324
351
|
}
|
|
325
352
|
});
|
|
326
353
|
|
|
327
|
-
test('GET /
|
|
328
|
-
const res = await request('GET', '/
|
|
354
|
+
test('GET /tutorialWeb/images/nonexistent.png returns an error page', async () => {
|
|
355
|
+
const res = await request('GET', '/tutorialWeb/images/nonexistent.png');
|
|
356
|
+
assert.equal(res.statusCode, 400);
|
|
357
|
+
assert.ok(res.body.includes('Image not found'));
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('GET /tutorialAI/images/test.png serves the image', async () => {
|
|
361
|
+
// Create a temporary image file in tutorialAI.
|
|
362
|
+
const imgPath = path.join(import.meta.dirname, 'web', 'tutorialAI', 'images', 'test.png');
|
|
363
|
+
await fs.mkdir(path.dirname(imgPath), {recursive: true});
|
|
364
|
+
await fs.writeFile(imgPath, 'fake png data');
|
|
365
|
+
try {
|
|
366
|
+
const res = await request('GET', '/tutorialAI/images/test.png');
|
|
367
|
+
assert.equal(res.statusCode, 200);
|
|
368
|
+
assert.ok(res.headers['content-type'].includes('image/png'));
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
await fs.unlink(imgPath).catch(() => {});
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('GET /tutorialAI/images/nonexistent.png returns an error page', async () => {
|
|
376
|
+
const res = await request('GET', '/tutorialAI/images/nonexistent.png');
|
|
329
377
|
assert.equal(res.statusCode, 400);
|
|
330
378
|
assert.ok(res.body.includes('Image not found'));
|
|
331
379
|
});
|
|
332
380
|
|
|
333
|
-
test('GET /nonexistent.html returns an abuse error', async () => {
|
|
381
|
+
test('GET /nonexistent.html returns an abuse error', {timeout: 500}, async () => {
|
|
334
382
|
const res = await request('GET', '/nonexistent.html');
|
|
335
383
|
assert.equal(res.statusCode, 400);
|
|
336
384
|
assert.ok(res.body.includes('Invalid request'));
|
|
@@ -346,7 +394,7 @@ test('GET /forbidden-path returns an invalid GET request error', async () => {
|
|
|
346
394
|
|
|
347
395
|
test('POST /requestTest.html with valid data returns an HTML page', async () => {
|
|
348
396
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
349
|
-
|
|
397
|
+
description: `Unique Test Page ${uniqueStamp}`,
|
|
350
398
|
url: `https://example.com/unique-${uniqueStamp}`,
|
|
351
399
|
why: 'Because accessibility matters'
|
|
352
400
|
});
|
|
@@ -356,7 +404,7 @@ test('POST /requestTest.html with valid data returns an HTML page', async () =>
|
|
|
356
404
|
|
|
357
405
|
test('POST /requestTest.html with an already-tested URL returns an error', async () => {
|
|
358
406
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
359
|
-
|
|
407
|
+
description: 'Mixed Outcomes Page',
|
|
360
408
|
url: 'https://example.com/mixed',
|
|
361
409
|
why: 'Because accessibility matters'
|
|
362
410
|
});
|
|
@@ -366,17 +414,23 @@ test('POST /requestTest.html with an already-tested URL returns an error', async
|
|
|
366
414
|
|
|
367
415
|
test('POST /requestTest.html with invalid data returns an error', async () => {
|
|
368
416
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
369
|
-
|
|
417
|
+
description: '',
|
|
370
418
|
url: 'not-a-url',
|
|
371
419
|
why: ''
|
|
372
420
|
});
|
|
373
421
|
assert.equal(res.statusCode, 400);
|
|
374
|
-
assert.ok(res.body.includes('Invalid test
|
|
422
|
+
assert.ok(res.body.includes('Invalid test request'));
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test('POST /requestTest.html with an unreadable body returns an error', async () => {
|
|
426
|
+
const res = await request('POST', '/requestTest.html', 'rawbody', {'content-type': 'text/plain'});
|
|
427
|
+
assert.equal(res.statusCode, 400);
|
|
428
|
+
assert.ok(res.body.includes('Unreadable request body'));
|
|
375
429
|
});
|
|
376
430
|
|
|
377
431
|
test('POST /requestRetest.html/260202T0000/new with valid data returns HTML', async () => {
|
|
378
|
-
// Reset
|
|
379
|
-
await fs.writeFile(
|
|
432
|
+
// Reset testRequests.json to avoid duplicate-request errors from prior tests.
|
|
433
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
380
434
|
const res = await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
381
435
|
why: 'Because the report is obsolete and needs refreshing'
|
|
382
436
|
});
|
|
@@ -389,7 +443,7 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
389
443
|
why: 'Because'
|
|
390
444
|
});
|
|
391
445
|
assert.equal(res.statusCode, 400);
|
|
392
|
-
assert.ok(res.body.includes('Invalid retest
|
|
446
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
393
447
|
});
|
|
394
448
|
|
|
395
449
|
test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
@@ -443,6 +497,36 @@ test('POST /worker/job without authentication returns 401', async () => {
|
|
|
443
497
|
assert.ok(body.error.message.includes('Unauthorized'));
|
|
444
498
|
});
|
|
445
499
|
|
|
500
|
+
test('POST /worker/job when TESTARO_WORKERS is unset returns 401', async () => {
|
|
501
|
+
const saved = process.env.TESTARO_WORKERS;
|
|
502
|
+
delete process.env.TESTARO_WORKERS;
|
|
503
|
+
try {
|
|
504
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
505
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
506
|
+
authorization: `Basic ${auth}`
|
|
507
|
+
});
|
|
508
|
+
assert.equal(res.statusCode, 401);
|
|
509
|
+
}
|
|
510
|
+
finally {
|
|
511
|
+
process.env.TESTARO_WORKERS = saved;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test('POST /worker/job when TESTARO_WORKERS is invalid JSON returns 401', async () => {
|
|
516
|
+
const saved = process.env.TESTARO_WORKERS;
|
|
517
|
+
process.env.TESTARO_WORKERS = 'not valid JSON {';
|
|
518
|
+
try {
|
|
519
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
520
|
+
const res = await request('POST', '/worker/job', {}, {
|
|
521
|
+
authorization: `Basic ${auth}`
|
|
522
|
+
});
|
|
523
|
+
assert.equal(res.statusCode, 401);
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
process.env.TESTARO_WORKERS = saved;
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
446
530
|
test('POST /worker/job with valid authentication returns a job or no-job response', async () => {
|
|
447
531
|
// Clean up any claimed jobs left by prior tests.
|
|
448
532
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
@@ -494,6 +578,26 @@ test('PUT / returns an invalid method error', async () => {
|
|
|
494
578
|
assert.ok(res.body.includes('Invalid request method'));
|
|
495
579
|
});
|
|
496
580
|
|
|
581
|
+
// TESTS: requestHandler catch boundary
|
|
582
|
+
|
|
583
|
+
test('GET /listReports.html returns a 500 error when an unexpected internal error occurs', {timeout: 500}, async () => {
|
|
584
|
+
// Corrupt testRequests.json so that getTestRequests throws, an error that should never occur in normal
|
|
585
|
+
// operation and so is not handled by handleRequest itself, only by requestHandler's
|
|
586
|
+
// outer catch boundary.
|
|
587
|
+
const backup = await fs.readFile(testRequestsPath, 'utf8');
|
|
588
|
+
await fs.writeFile(testRequestsPath, 'not valid json');
|
|
589
|
+
try {
|
|
590
|
+
const res = await request('GET', '/listReports.html');
|
|
591
|
+
assert.equal(res.statusCode, 500);
|
|
592
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
593
|
+
const body = jsonBody(res);
|
|
594
|
+
assert.ok(body.error);
|
|
595
|
+
}
|
|
596
|
+
finally {
|
|
597
|
+
await fs.writeFile(testRequestsPath, backup);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
|
|
497
601
|
// TESTS: serveError behavior
|
|
498
602
|
|
|
499
603
|
test('serveError sends JSON for agent requests (isHumanUser = false)', async () => {
|
|
@@ -510,10 +614,10 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
510
614
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
511
615
|
});
|
|
512
616
|
|
|
513
|
-
// TESTS:
|
|
617
|
+
// TESTS: requestAction.html
|
|
514
618
|
|
|
515
|
-
test('POST /
|
|
516
|
-
const res = await formRequest('POST', '/
|
|
619
|
+
test('POST /requestAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
|
|
620
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
517
621
|
target: 'https://example.com\tTest Page',
|
|
518
622
|
authCode: 'wrong-code'
|
|
519
623
|
});
|
|
@@ -521,13 +625,13 @@ test('POST /recAction.html with invalid auth code returns an error', {timeout: 5
|
|
|
521
625
|
assert.ok(res.body.includes('Invalid test order'));
|
|
522
626
|
});
|
|
523
627
|
|
|
524
|
-
test('POST /
|
|
628
|
+
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML', async () => {
|
|
525
629
|
await formRequest('POST', '/requestTest.html', {
|
|
526
|
-
|
|
630
|
+
description: `Reject Test Page ${uniqueStamp}`,
|
|
527
631
|
url: `https://example.com/reject-${uniqueStamp}`,
|
|
528
632
|
why: 'Because accessibility matters'
|
|
529
633
|
});
|
|
530
|
-
const res = await formRequest('POST', '/
|
|
634
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
531
635
|
target: `https://example.com/reject-${uniqueStamp}`,
|
|
532
636
|
authCode: 'test-auth-code'
|
|
533
637
|
});
|
|
@@ -535,31 +639,38 @@ test('POST /recAction.html with valid auth code and rejection (no what) returns
|
|
|
535
639
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
536
640
|
});
|
|
537
641
|
|
|
538
|
-
// TESTS:
|
|
642
|
+
// TESTS: tutorialWeb (web user tutorial)
|
|
539
643
|
|
|
540
|
-
test('
|
|
541
|
-
const res = await request('
|
|
542
|
-
|
|
644
|
+
test('GET /tutorialWeb.html returns HTML', async () => {
|
|
645
|
+
const res = await request('GET', '/tutorialWeb.html');
|
|
646
|
+
assert.equal(res.statusCode, 200);
|
|
647
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
test('POST /tutorialWebComment.html with content returns JSON', async () => {
|
|
651
|
+
const res = await request('POST', '/tutorialWebComment.html', {
|
|
652
|
+
content: `This is a test comment ${uniqueStamp}`
|
|
543
653
|
});
|
|
544
654
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
655
|
+
assert.deepEqual(JSON.parse(res.body), {status: 'ok'});
|
|
545
656
|
});
|
|
546
657
|
|
|
547
658
|
// TESTS: additional branch coverage
|
|
548
659
|
|
|
549
|
-
test('GET /fullReport.json/260101T0007/hid returns an
|
|
660
|
+
test('GET /fullReport.json/260101T0007/hid returns an error for an unavailable report', async () => {
|
|
550
661
|
const res = await request('GET', '/fullReport.json/260101T0007/hid');
|
|
551
662
|
assert.equal(res.statusCode, 400);
|
|
552
663
|
assert.ok(res.body.includes('Invalid request'));
|
|
553
664
|
});
|
|
554
665
|
|
|
555
|
-
test('POST /
|
|
556
|
-
await fs.writeFile(
|
|
666
|
+
test('POST /requestAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
|
|
667
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
557
668
|
await formRequest('POST', '/requestTest.html', {
|
|
558
|
-
|
|
669
|
+
description: `Approval Test Page ${uniqueStamp}`,
|
|
559
670
|
url: `https://example.com/approval-${uniqueStamp}`,
|
|
560
671
|
why: 'Because accessibility matters'
|
|
561
672
|
});
|
|
562
|
-
const res = await formRequest('POST', '/
|
|
673
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
563
674
|
target: `https://example.com/approval-${uniqueStamp}\tApproval Test Page ${uniqueStamp}`,
|
|
564
675
|
authCode: 'test-auth-code'
|
|
565
676
|
});
|
|
@@ -575,10 +686,10 @@ test('POST /reannotate.html with invalid auth code returns an error page', async
|
|
|
575
686
|
});
|
|
576
687
|
|
|
577
688
|
test('POST /reannotate.html with valid auth code serves the answer page', async () => {
|
|
578
|
-
// Back up all fixture reports, because
|
|
689
|
+
// Back up all fixture reports, because reannotation modifies them in place.
|
|
579
690
|
const reportsDir = path.join(fixtureDBDir, 'reports');
|
|
580
691
|
const reportFiles = await fs.readdir(reportsDir);
|
|
581
|
-
const backups = {};
|
|
692
|
+
const backups: Record<string, string> = {};
|
|
582
693
|
for (const file of reportFiles) {
|
|
583
694
|
backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
|
|
584
695
|
}
|
|
@@ -593,7 +704,7 @@ test('POST /reannotate.html with valid auth code serves the answer page', async
|
|
|
593
704
|
finally {
|
|
594
705
|
// Restore all fixture reports.
|
|
595
706
|
for (const file of reportFiles) {
|
|
596
|
-
await fs.writeFile(path.join(reportsDir, file), backups[file]);
|
|
707
|
+
await fs.writeFile(path.join(reportsDir, file), backups[file]!);
|
|
597
708
|
}
|
|
598
709
|
}
|
|
599
710
|
});
|
|
@@ -608,7 +719,7 @@ test('POST /renewWCAG.html with invalid auth code returns an error page', async
|
|
|
608
719
|
test('POST /renewWCAG.html with valid auth code serves the answer page', async () => {
|
|
609
720
|
// Mock fetch to avoid a network dependency.
|
|
610
721
|
const originalFetch = global.fetch;
|
|
611
|
-
const wcagMapPath = path.join(
|
|
722
|
+
const wcagMapPath = path.join(import.meta.dirname, 'wcagMap.json');
|
|
612
723
|
const wcagMapBackup = await fs.readFile(wcagMapPath, 'utf8');
|
|
613
724
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
614
725
|
global.fetch = async () => ({
|
|
@@ -629,21 +740,56 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
629
740
|
}
|
|
630
741
|
});
|
|
631
742
|
|
|
632
|
-
test('POST /
|
|
633
|
-
const res = await request('POST', '/
|
|
743
|
+
test('POST /tutorialWebComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
744
|
+
const res = await request('POST', '/tutorialWebComment.html', {
|
|
745
|
+
content: ''
|
|
746
|
+
});
|
|
747
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
// TESTS: tutorialAI (AI agent configuration tutorial)
|
|
751
|
+
|
|
752
|
+
test('GET /tutorialAI.html returns HTML', async () => {
|
|
753
|
+
const res = await request('GET', '/tutorialAI.html');
|
|
754
|
+
assert.equal(res.statusCode, 200);
|
|
755
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test('POST /tutorialAIComment.html with content returns JSON', async () => {
|
|
759
|
+
const res = await request('POST', '/tutorialAIComment.html', {
|
|
760
|
+
content: `This is a test AI tutorial comment ${uniqueStamp}`
|
|
761
|
+
});
|
|
762
|
+
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
763
|
+
assert.deepEqual(JSON.parse(res.body), {status: 'ok'});
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
test('POST /tutorialAIComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
|
|
767
|
+
const res = await request('POST', '/tutorialAIComment.html', {
|
|
634
768
|
content: ''
|
|
635
769
|
});
|
|
636
770
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
637
771
|
});
|
|
638
772
|
|
|
773
|
+
test('GET /qai redirects to /tutorialAI.html', async () => {
|
|
774
|
+
const res = await request('GET', '/qai', {}, {followRedirects: false});
|
|
775
|
+
assert.equal(res.statusCode, 301);
|
|
776
|
+
assert.equal(res.headers['location'], '/tutorialAI.html');
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
test('GET /qai/comments redirects to /tutorialAI.html', async () => {
|
|
780
|
+
const res = await request('GET', '/qai/comments', {}, {followRedirects: false});
|
|
781
|
+
assert.equal(res.statusCode, 301);
|
|
782
|
+
assert.equal(res.headers['location'], '/tutorialAI.html');
|
|
783
|
+
});
|
|
784
|
+
|
|
639
785
|
test('POST /requestTest.html with non-https URL returns an error', async () => {
|
|
640
786
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
641
|
-
|
|
787
|
+
description: 'Test Page',
|
|
642
788
|
url: 'http://example.com',
|
|
643
789
|
why: 'Because accessibility matters'
|
|
644
790
|
});
|
|
645
791
|
assert.equal(res.statusCode, 400);
|
|
646
|
-
assert.ok(res.body.includes('Invalid test
|
|
792
|
+
assert.ok(res.body.includes('Invalid test request'));
|
|
647
793
|
});
|
|
648
794
|
|
|
649
795
|
test('POST /requestRetest.html/260202T0000/new with missing why returns an error', async () => {
|
|
@@ -651,7 +797,7 @@ test('POST /requestRetest.html/260202T0000/new with missing why returns an error
|
|
|
651
797
|
why: ''
|
|
652
798
|
});
|
|
653
799
|
assert.equal(res.statusCode, 400);
|
|
654
|
-
assert.ok(res.body.includes('Invalid retest
|
|
800
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
655
801
|
});
|
|
656
802
|
|
|
657
803
|
test('POST /worker/report with valid authentication and wrong worker returns an error', async () => {
|
|
@@ -669,6 +815,31 @@ test('POST /worker/report with valid authentication and wrong worker returns an
|
|
|
669
815
|
assert.ok(body.error);
|
|
670
816
|
});
|
|
671
817
|
|
|
818
|
+
test('POST /worker/report with a claimed job file that is not valid JSON returns a 500 error', {timeout: 500}, async () => {
|
|
819
|
+
// A corrupt claimed-job file is not the normal "no such claim" outcome (ENOENT), so it
|
|
820
|
+
// should propagate as an unexpected error rather than being treated as an unassigned job.
|
|
821
|
+
const jobID = '990101T0002-bad';
|
|
822
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
823
|
+
const jobPath = path.join(claimedDir, `${jobID}.json`);
|
|
824
|
+
await fs.writeFile(jobPath, 'not valid json');
|
|
825
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
826
|
+
const report = {
|
|
827
|
+
id: jobID,
|
|
828
|
+
target: {what: 'Test', url: 'https://example.com/test'}
|
|
829
|
+
};
|
|
830
|
+
try {
|
|
831
|
+
const res = await request('POST', '/worker/report', {report}, {
|
|
832
|
+
authorization: `Basic ${auth}`
|
|
833
|
+
});
|
|
834
|
+
assert.equal(res.statusCode, 500);
|
|
835
|
+
const body = jsonBody(res);
|
|
836
|
+
assert.ok(body.error);
|
|
837
|
+
}
|
|
838
|
+
finally {
|
|
839
|
+
await fs.unlink(jobPath).catch(() => {});
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
672
843
|
test('POST /worker/job with wrong secret returns 401', async () => {
|
|
673
844
|
const auth = Buffer.from('worker1:wrongsecret').toString('base64');
|
|
674
845
|
const res = await request('POST', '/worker/job', {}, {
|
|
@@ -723,7 +894,7 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
723
894
|
const body = jsonBody(res);
|
|
724
895
|
assert.ok(body.error.message.includes('has not completed job'));
|
|
725
896
|
// Wait for the async rename to complete.
|
|
726
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
897
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
727
898
|
// The job should have been moved to failed.
|
|
728
899
|
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
729
900
|
assert.ok(failedExists, 'Job should be moved to failed directory');
|
|
@@ -758,7 +929,7 @@ test('POST /worker/job with a queued job assigns it to the worker', {timeout: 50
|
|
|
758
929
|
assert.equal(body.id, '260101T0000-mix');
|
|
759
930
|
assert.equal(body.sources.worker, 'Worker One');
|
|
760
931
|
// Wait for the async unlink to complete.
|
|
761
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
932
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
762
933
|
// The job should have been moved from queue to claimed.
|
|
763
934
|
const queueExists = await fs.access(path.join(queueDir, jobFile)).then(() => true).catch(() => false);
|
|
764
935
|
assert.equal(queueExists, false, 'Job should be removed from queue');
|
|
@@ -803,12 +974,57 @@ test('POST /worker/report with valid authentication and valid claimed job proces
|
|
|
803
974
|
assert.equal(body.status, 'ok');
|
|
804
975
|
});
|
|
805
976
|
|
|
977
|
+
test('POST /worker/report with a claimed job but an unusable report returns an error and reclassifies the job', {timeout: 500}, async () => {
|
|
978
|
+
// Use a unique job ID that does not conflict with existing fixtures.
|
|
979
|
+
const jobID = '990101T0001-unu';
|
|
980
|
+
const reportPath = path.join(fixtureDBDir, 'reports', `${jobID}.json`);
|
|
981
|
+
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
982
|
+
const failedDir = path.join(fixtureDBDir, 'jobs', 'failed');
|
|
983
|
+
const jobFile = `${jobID}.json`;
|
|
984
|
+
const claimedJobPath = path.join(claimedDir, jobFile);
|
|
985
|
+
const failedJobPath = path.join(failedDir, jobFile);
|
|
986
|
+
// Clean up any leftover files.
|
|
987
|
+
await fs.unlink(reportPath).catch(() => {});
|
|
988
|
+
await fs.unlink(failedJobPath).catch(() => {});
|
|
989
|
+
// Create a claimed job assigned to Worker One.
|
|
990
|
+
await fs.writeFile(claimedJobPath, JSON.stringify({
|
|
991
|
+
id: jobID,
|
|
992
|
+
target: {what: 'Test', url: 'https://example.com/test'},
|
|
993
|
+
sources: {worker: 'Worker One'}
|
|
994
|
+
}));
|
|
995
|
+
const auth = Buffer.from('worker1:secret1').toString('base64');
|
|
996
|
+
// A report that is syntactically valid (has id, target.what, target.url) but is not
|
|
997
|
+
// usable by Kilotest, because it has no acts, jobData, or catalog.
|
|
998
|
+
const report = {
|
|
999
|
+
id: jobID,
|
|
1000
|
+
target: {what: 'Test', url: 'https://example.com/test'}
|
|
1001
|
+
};
|
|
1002
|
+
const res = await request('POST', '/worker/report', {report}, {
|
|
1003
|
+
authorization: `Basic ${auth}`
|
|
1004
|
+
});
|
|
1005
|
+
assert.equal(res.statusCode, 400);
|
|
1006
|
+
const body = jsonBody(res);
|
|
1007
|
+
assert.ok(body.error.message.includes('not usable'));
|
|
1008
|
+
// The report should not have been recorded.
|
|
1009
|
+
const reportExists = await fs.access(reportPath).then(() => true).catch(() => false);
|
|
1010
|
+
assert.equal(reportExists, false, 'Unusable report should not be saved');
|
|
1011
|
+
// Wait for the async rename to complete.
|
|
1012
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
1013
|
+
// The job should have been reclassified as failed rather than left claimed.
|
|
1014
|
+
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
1015
|
+
assert.ok(failedExists, 'Job should be moved to the failed directory');
|
|
1016
|
+
const claimedExists = await fs.access(claimedJobPath).then(() => true).catch(() => false);
|
|
1017
|
+
assert.equal(claimedExists, false, 'Job should be removed from the claimed directory');
|
|
1018
|
+
// Clean up.
|
|
1019
|
+
await fs.unlink(failedJobPath).catch(() => {});
|
|
1020
|
+
});
|
|
1021
|
+
|
|
806
1022
|
// TESTS: remaining error branches and web pages
|
|
807
1023
|
|
|
808
1024
|
test('GET /style.css with a read error returns an error page', async () => {
|
|
809
1025
|
// Temporarily rename style.css to trigger a read error.
|
|
810
|
-
const stylePath = path.join(
|
|
811
|
-
const tempPath = path.join(
|
|
1026
|
+
const stylePath = path.join(import.meta.dirname, 'style.css');
|
|
1027
|
+
const tempPath = path.join(import.meta.dirname, 'style.css.bak');
|
|
812
1028
|
await fs.rename(stylePath, tempPath);
|
|
813
1029
|
try {
|
|
814
1030
|
const res = await request('GET', '/style.css');
|
|
@@ -835,7 +1051,7 @@ test('GET /requestRetest.html/260202T0000/new returns an error page when called
|
|
|
835
1051
|
// TESTS: GET HTML page routing
|
|
836
1052
|
// These verify that the HTTP routing layer connects each URL to the correct
|
|
837
1053
|
// handler and returns HTML with a 200 status. The handler logic itself is
|
|
838
|
-
// tested by the unit tests in web/*/*.test.
|
|
1054
|
+
// tested by the unit tests in web/*/*.test.cjs.
|
|
839
1055
|
|
|
840
1056
|
const htmlPagePaths = [
|
|
841
1057
|
'/listReports.html',
|
|
@@ -843,7 +1059,8 @@ const htmlPagePaths = [
|
|
|
843
1059
|
'/listDiagnoses.html/linkNoText/260101T0000/mix/0',
|
|
844
1060
|
'/enqueueForm.html',
|
|
845
1061
|
'/manage.html',
|
|
846
|
-
'/
|
|
1062
|
+
'/tutorialWeb.html',
|
|
1063
|
+
'/tutorialAI.html',
|
|
847
1064
|
'/listTopIssues.html',
|
|
848
1065
|
'/reannotateForm.html',
|
|
849
1066
|
'/renewWCAGForm.html',
|
|
@@ -856,17 +1073,17 @@ const htmlPagePaths = [
|
|
|
856
1073
|
];
|
|
857
1074
|
|
|
858
1075
|
for (const pagePath of htmlPagePaths) {
|
|
859
|
-
test(`GET ${pagePath} serves a generated HTML page`, async () => {
|
|
1076
|
+
test(`GET ${pagePath} serves a generated HTML page`, {timeout: 500}, async () => {
|
|
860
1077
|
const res = await request('GET', pagePath);
|
|
861
1078
|
assert.equal(res.statusCode, 200);
|
|
862
1079
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
863
1080
|
});
|
|
864
1081
|
}
|
|
865
1082
|
|
|
866
|
-
test('GET /enqueueForm.html shows
|
|
867
|
-
await fs.writeFile(
|
|
1083
|
+
test('GET /enqueueForm.html shows requests when testRequests.json has entries', async () => {
|
|
1084
|
+
await fs.writeFile(testRequestsPath, JSON.stringify({
|
|
868
1085
|
'https://example.com/enqueue-test': [
|
|
869
|
-
{
|
|
1086
|
+
{description: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
|
|
870
1087
|
]
|
|
871
1088
|
}));
|
|
872
1089
|
const res = await request('GET', '/enqueueForm.html');
|
|
@@ -889,52 +1106,104 @@ test('GET /test.html.bak matches isPathAllowed but falls through to catch-all',
|
|
|
889
1106
|
assert.ok(res.body.includes('Invalid GET request'));
|
|
890
1107
|
});
|
|
891
1108
|
|
|
892
|
-
test('GET /mcp returns a
|
|
893
|
-
const res = await request('GET', '/mcp');
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
assert.
|
|
1109
|
+
test('GET /mcp with an SSE-capable Accept header returns a JSON-RPC 405 error instead of opening an SSE stream', async () => {
|
|
1110
|
+
const res = await request('GET', '/mcp', null, {accept: 'application/json, text/event-stream'});
|
|
1111
|
+
assert.equal(res.statusCode, 405);
|
|
1112
|
+
const body = jsonBody(res);
|
|
1113
|
+
assert.equal(body.jsonrpc, '2.0');
|
|
1114
|
+
assert.equal(body.error.code, -32000);
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
test('GET /mcp without an SSE-capable Accept header serves an HTML explanation page instead of the MCP protocol response', async () => {
|
|
1118
|
+
const res = await request('GET', '/mcp', null, {accept: 'text/html'});
|
|
1119
|
+
assert.equal(res.statusCode, 400);
|
|
1120
|
+
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
1121
|
+
assert.ok(res.body.includes('/tutorialAI.html'));
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test('POST /mcp without an SSE-capable Accept header returns a JSON-RPC 406 error', async () => {
|
|
1125
|
+
const res = await request('POST', '/mcp', {jsonrpc: '2.0', method: 'initialize', id: 1, params: {}});
|
|
1126
|
+
assert.equal(res.statusCode, 406);
|
|
1127
|
+
assert.equal(jsonBody(res).error.code, -32000);
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test('POST /mcp with malformed JSON returns a JSON-RPC 400 parse error', async () => {
|
|
1131
|
+
const res = await request('POST', '/mcp', '{not json', {
|
|
1132
|
+
'content-type': 'application/json',
|
|
1133
|
+
accept: 'application/json, text/event-stream'
|
|
1134
|
+
});
|
|
1135
|
+
assert.equal(res.statusCode, 400);
|
|
1136
|
+
assert.equal(jsonBody(res).error.code, -32700);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
test('POST /mcp with a non-JSON-RPC body returns a JSON-RPC 400 error', async () => {
|
|
1140
|
+
const res = await request('POST', '/mcp', {foo: 'bar'}, {accept: 'application/json, text/event-stream'});
|
|
1141
|
+
assert.equal(res.statusCode, 400);
|
|
1142
|
+
assert.equal(jsonBody(res).error.code, -32700);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
test('POST /mcp with a non-JSON content type returns a JSON-RPC 415 error', async () => {
|
|
1146
|
+
const res = await request('POST', '/mcp', 'x=1', {
|
|
1147
|
+
'content-type': 'application/x-www-form-urlencoded',
|
|
1148
|
+
accept: 'application/json, text/event-stream'
|
|
1149
|
+
});
|
|
1150
|
+
assert.equal(res.statusCode, 415);
|
|
1151
|
+
assert.equal(jsonBody(res).error.code, -32000);
|
|
897
1152
|
});
|
|
898
1153
|
|
|
899
|
-
test('POST /mcp
|
|
1154
|
+
test('POST /mcp with an unknown JSON-RPC method returns a -32601 error', async () => {
|
|
1155
|
+
const res = await request('POST', '/mcp', {jsonrpc: '2.0', method: 'bogus/method', id: 9}, {
|
|
1156
|
+
accept: 'application/json, text/event-stream'
|
|
1157
|
+
});
|
|
1158
|
+
assert.equal(res.statusCode, 200);
|
|
1159
|
+
const dataLine = res.body.split('\n').find((line: string) => line.startsWith('data: '));
|
|
1160
|
+
assert.ok(dataLine);
|
|
1161
|
+
const message = JSON.parse(dataLine.slice(6));
|
|
1162
|
+
assert.equal(message.error.code, -32601);
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test('POST /mcp with a valid initialize request returns server info via SSE', async () => {
|
|
900
1166
|
const res = await request('POST', '/mcp', {
|
|
901
1167
|
jsonrpc: '2.0',
|
|
902
1168
|
method: 'initialize',
|
|
903
1169
|
id: 1,
|
|
904
1170
|
params: {
|
|
905
|
-
protocolVersion: '
|
|
1171
|
+
protocolVersion: '2025-06-18',
|
|
906
1172
|
capabilities: {},
|
|
907
1173
|
clientInfo: {name: 'test-client', version: '1.0'}
|
|
908
1174
|
}
|
|
909
1175
|
}, {
|
|
910
1176
|
accept: 'application/json, text/event-stream'
|
|
911
1177
|
});
|
|
912
|
-
|
|
913
|
-
|
|
1178
|
+
assert.equal(res.statusCode, 200);
|
|
1179
|
+
const dataLine = res.body.split('\n').find((line: string) => line.startsWith('data: '));
|
|
1180
|
+
assert.ok(dataLine);
|
|
1181
|
+
const message = JSON.parse(dataLine.slice(6));
|
|
1182
|
+
assert.equal(message.result.serverInfo.name, 'Kilotest');
|
|
914
1183
|
});
|
|
915
1184
|
|
|
916
1185
|
// TESTS: answer error branches
|
|
917
1186
|
|
|
918
1187
|
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
919
|
-
await fs.writeFile(
|
|
920
|
-
// First request to create the
|
|
1188
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1189
|
+
// First request to create the request.
|
|
921
1190
|
await formRequest('POST', '/requestTest.html', {
|
|
922
|
-
|
|
1191
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
923
1192
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
924
1193
|
why: 'Because accessibility matters'
|
|
925
1194
|
});
|
|
926
1195
|
// Second request with the same URL should get a duplicate error.
|
|
927
1196
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
928
|
-
|
|
1197
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
929
1198
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
930
1199
|
why: 'Because accessibility matters again'
|
|
931
1200
|
});
|
|
932
1201
|
assert.equal(res.statusCode, 400);
|
|
933
|
-
assert.ok(res.body.includes('Duplicate
|
|
1202
|
+
assert.ok(res.body.includes('Duplicate request'));
|
|
934
1203
|
});
|
|
935
1204
|
|
|
936
1205
|
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
937
|
-
await fs.writeFile(
|
|
1206
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
938
1207
|
// First retest to create the recommendation.
|
|
939
1208
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
940
1209
|
why: 'Because the report is obsolete and needs refreshing'
|
|
@@ -944,26 +1213,26 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
944
1213
|
why: 'Because the report is obsolete and needs refreshing again'
|
|
945
1214
|
});
|
|
946
1215
|
assert.equal(res.statusCode, 400);
|
|
947
|
-
assert.ok(res.body.includes('Duplicate
|
|
1216
|
+
assert.ok(res.body.includes('Duplicate request'));
|
|
948
1217
|
});
|
|
949
1218
|
|
|
950
|
-
test('POST /
|
|
951
|
-
await fs.writeFile(
|
|
952
|
-
// Create a
|
|
1219
|
+
test('POST /requestAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
|
|
1220
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1221
|
+
// Create a request.
|
|
953
1222
|
await formRequest('POST', '/requestTest.html', {
|
|
954
|
-
|
|
1223
|
+
description: `Action Dup Page ${uniqueStamp}`,
|
|
955
1224
|
url: `https://example.com/action-dup-${uniqueStamp}`,
|
|
956
1225
|
why: 'Because accessibility matters'
|
|
957
1226
|
});
|
|
958
1227
|
// Approve it once.
|
|
959
|
-
await formRequest('POST', '/
|
|
1228
|
+
await formRequest('POST', '/requestAction.html', {
|
|
960
1229
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
961
1230
|
authCode: 'test-auth-code',
|
|
962
1231
|
what: 'yes'
|
|
963
1232
|
});
|
|
964
|
-
// Approve it again (the
|
|
965
|
-
// This test covers the
|
|
966
|
-
const res = await formRequest('POST', '/
|
|
1233
|
+
// Approve it again (the requests may have been cleared, so this may succeed or fail).
|
|
1234
|
+
// This test covers the requestAction answer error branch.
|
|
1235
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
967
1236
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
968
1237
|
authCode: 'test-auth-code',
|
|
969
1238
|
what: 'yes'
|
|
@@ -972,10 +1241,10 @@ test('POST /recAction.html with valid auth code and approval of a duplicate retu
|
|
|
972
1241
|
assert.ok(res.statusCode === 200 || res.statusCode === 400);
|
|
973
1242
|
});
|
|
974
1243
|
|
|
975
|
-
test('POST /
|
|
1244
|
+
test('POST /requestAction.html with valid auth code and approval of an invalid URL returns an error', async () => {
|
|
976
1245
|
// A URL that starts with https:// but is not a valid URL causes
|
|
977
1246
|
// enqueue.answer to return status error.
|
|
978
|
-
const res = await formRequest('POST', '/
|
|
1247
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
979
1248
|
target: 'https://\tTest Page',
|
|
980
1249
|
authCode: 'test-auth-code',
|
|
981
1250
|
what: 'yes'
|
|
@@ -986,22 +1255,22 @@ test('POST /recAction.html with valid auth code and approval of an invalid URL r
|
|
|
986
1255
|
|
|
987
1256
|
// UNIT TESTS FOR serveError
|
|
988
1257
|
|
|
989
|
-
const mockRes = () => {
|
|
990
|
-
const state = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
1258
|
+
const mockRes = (): any => {
|
|
1259
|
+
const state: any = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
991
1260
|
return {
|
|
992
1261
|
get statusCode() {return state.statusCode;},
|
|
993
|
-
set statusCode(value) {state.statusCode = value;},
|
|
994
|
-
setHeader(name, value) {state.headers[name] = value;},
|
|
995
|
-
end(data) {state.body = data; state.writableEnded = true;},
|
|
1262
|
+
set statusCode(value: any) {state.statusCode = value;},
|
|
1263
|
+
setHeader(name: any, value: any) {state.headers[name] = value;},
|
|
1264
|
+
end(data: any) {state.body = data; state.writableEnded = true;},
|
|
996
1265
|
_state: state
|
|
997
1266
|
};
|
|
998
1267
|
};
|
|
999
1268
|
|
|
1000
1269
|
test('serveError does not write to a response that has already ended', async () => {
|
|
1001
1270
|
let wrote = false;
|
|
1002
|
-
const mockResponse = {
|
|
1271
|
+
const mockResponse: any = {
|
|
1003
1272
|
writableEnded: true,
|
|
1004
|
-
set statusCode(value) {
|
|
1273
|
+
set statusCode(value: any) {
|
|
1005
1274
|
wrote = true;
|
|
1006
1275
|
},
|
|
1007
1276
|
setHeader() {
|
|
@@ -1043,29 +1312,80 @@ test('getAbuseError uses unknown IP when no forwarding header or remote address'
|
|
|
1043
1312
|
headers: {},
|
|
1044
1313
|
socket: {remoteAddress: undefined}
|
|
1045
1314
|
};
|
|
1046
|
-
const result = getAbuseError(mockRequest, 'test reason');
|
|
1315
|
+
const result = getAbuseError(mockRequest as any, 'test reason');
|
|
1047
1316
|
assert.equal(result['IP address'], 'unknown');
|
|
1048
1317
|
assert.equal(result.reason, 'test reason');
|
|
1049
1318
|
});
|
|
1050
1319
|
|
|
1051
1320
|
// UNIT TESTS FOR startServer
|
|
1052
1321
|
|
|
1053
|
-
test('startServer starts an HTTP server when protocol is http', async () => {
|
|
1322
|
+
test('startServer starts an HTTP server when protocol is http', {timeout: 500}, async () => {
|
|
1054
1323
|
// Delete PROTOCOL to exercise the 'http' fallback in startServer.
|
|
1055
1324
|
const savedProtocol = process.env.PROTOCOL;
|
|
1325
|
+
// Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
|
|
1326
|
+
const savedPort = process.env.PORT;
|
|
1327
|
+
process.env.PORT = '0';
|
|
1056
1328
|
delete process.env.PROTOCOL;
|
|
1057
1329
|
const server = await startServer();
|
|
1058
1330
|
process.env.PROTOCOL = savedProtocol;
|
|
1331
|
+
if (savedPort !== undefined) {
|
|
1332
|
+
process.env.PORT = savedPort;
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
delete process.env.PORT;
|
|
1336
|
+
}
|
|
1059
1337
|
try {
|
|
1060
1338
|
assert.ok(server);
|
|
1061
1339
|
assert.equal(typeof server.listen, 'function');
|
|
1062
1340
|
// Verify the server is listening by making a request.
|
|
1063
1341
|
const address = server.address();
|
|
1064
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1342
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1065
1343
|
}
|
|
1066
1344
|
finally {
|
|
1345
|
+
server?.closeAllConnections?.();
|
|
1346
|
+
await new Promise<void>(resolve => {
|
|
1347
|
+
const timer = setTimeout(() => {
|
|
1348
|
+
server?.closeAllConnections?.();
|
|
1349
|
+
resolve();
|
|
1350
|
+
}, 1000);
|
|
1351
|
+
server?.close(() => {
|
|
1352
|
+
clearTimeout(timer);
|
|
1353
|
+
resolve();
|
|
1354
|
+
});
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, async () => {
|
|
1360
|
+
// Exercise the '|| 3000' fallback branch in serve by deleting PORT and calling
|
|
1361
|
+
// serve directly. The server will attempt to bind to port 3000. On a system
|
|
1362
|
+
// where port 3000 is free, it succeeds and we close it. Where it is occupied,
|
|
1363
|
+
// the error event fires and we accept that as proof the fallback was reached.
|
|
1364
|
+
const savedPort = process.env.PORT;
|
|
1365
|
+
delete process.env.PORT;
|
|
1366
|
+
try {
|
|
1367
|
+
const server = await indexModule.serve(http, {});
|
|
1368
|
+
let bindError: any = null;
|
|
1369
|
+
server.on('error', error => {
|
|
1370
|
+
bindError = error;
|
|
1371
|
+
});
|
|
1372
|
+
// Wait briefly for either successful binding or an EADDRINUSE error.
|
|
1373
|
+
await new Promise<void>(resolve => {
|
|
1374
|
+
const timer = setTimeout(resolve, 200);
|
|
1375
|
+
server.on('listening', () => {
|
|
1376
|
+
clearTimeout(timer);
|
|
1377
|
+
resolve();
|
|
1378
|
+
});
|
|
1379
|
+
});
|
|
1380
|
+
if (bindError) {
|
|
1381
|
+
assert.equal(bindError.code, 'EADDRINUSE', `Expected EADDRINUSE, got: ${bindError.message}`);
|
|
1382
|
+
}
|
|
1383
|
+
else {
|
|
1384
|
+
const address = server.address();
|
|
1385
|
+
assert.ok(typeof address === 'object' && address !== null && address.port === 3000);
|
|
1386
|
+
}
|
|
1067
1387
|
server.closeAllConnections?.();
|
|
1068
|
-
await new Promise(resolve => {
|
|
1388
|
+
await new Promise<void>(resolve => {
|
|
1069
1389
|
const timer = setTimeout(() => {
|
|
1070
1390
|
server.closeAllConnections?.();
|
|
1071
1391
|
resolve();
|
|
@@ -1076,13 +1396,21 @@ test('startServer starts an HTTP server when protocol is http', async () => {
|
|
|
1076
1396
|
});
|
|
1077
1397
|
});
|
|
1078
1398
|
}
|
|
1399
|
+
finally {
|
|
1400
|
+
if (savedPort !== undefined) {
|
|
1401
|
+
process.env.PORT = savedPort;
|
|
1402
|
+
}
|
|
1403
|
+
else {
|
|
1404
|
+
delete process.env.PORT;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1079
1407
|
});
|
|
1080
1408
|
|
|
1081
1409
|
test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
|
|
1082
1410
|
// Generate a self-signed certificate for the test using openssl.
|
|
1083
|
-
const {execSync} =
|
|
1084
|
-
const os =
|
|
1085
|
-
const fsSync =
|
|
1411
|
+
const {execSync} = await import('node:child_process');
|
|
1412
|
+
const os = await import('node:os');
|
|
1413
|
+
const fsSync = await import('node:fs');
|
|
1086
1414
|
const tmpDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'kilotest-https-'));
|
|
1087
1415
|
const keyPath = path.join(tmpDir, 'key.pem');
|
|
1088
1416
|
const certPath = path.join(tmpDir, 'cert.pem');
|
|
@@ -1094,6 +1422,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1094
1422
|
const savedProtocol = process.env.PROTOCOL;
|
|
1095
1423
|
const savedKey = process.env.KEY;
|
|
1096
1424
|
const savedCert = process.env.CERT;
|
|
1425
|
+
// Use port 0 so the OS assigns an ephemeral port, avoiding collisions with production.
|
|
1426
|
+
const savedPort = process.env.PORT;
|
|
1427
|
+
process.env.PORT = '0';
|
|
1097
1428
|
process.env.PROTOCOL = 'https';
|
|
1098
1429
|
process.env.KEY = keyPath;
|
|
1099
1430
|
process.env.CERT = certPath;
|
|
@@ -1102,9 +1433,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1102
1433
|
assert.ok(server);
|
|
1103
1434
|
assert.equal(typeof server.listen, 'function');
|
|
1104
1435
|
const address = server.address();
|
|
1105
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1436
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1106
1437
|
server.closeAllConnections?.();
|
|
1107
|
-
await new Promise(resolve => {
|
|
1438
|
+
await new Promise<void>(resolve => {
|
|
1108
1439
|
const timer = setTimeout(() => {
|
|
1109
1440
|
server.closeAllConnections?.();
|
|
1110
1441
|
resolve();
|
|
@@ -1134,43 +1465,37 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1134
1465
|
else {
|
|
1135
1466
|
delete process.env.CERT;
|
|
1136
1467
|
}
|
|
1468
|
+
if (savedPort !== undefined) {
|
|
1469
|
+
process.env.PORT = savedPort;
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
delete process.env.PORT;
|
|
1473
|
+
}
|
|
1137
1474
|
fsSync.rmSync(tmpDir, {recursive: true, force: true});
|
|
1138
1475
|
}
|
|
1139
1476
|
});
|
|
1140
1477
|
|
|
1141
1478
|
test('runIfMain calls startServer when mainModule matches currentModule', async () => {
|
|
1142
1479
|
let called = false;
|
|
1143
|
-
const
|
|
1144
|
-
indexModule.startServer = async () => {
|
|
1480
|
+
const starter = async () => {
|
|
1145
1481
|
called = true;
|
|
1146
1482
|
return null;
|
|
1147
1483
|
};
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
assert.equal(called, true);
|
|
1155
|
-
}
|
|
1156
|
-
finally {
|
|
1157
|
-
indexModule.startServer = originalStartServer;
|
|
1158
|
-
}
|
|
1484
|
+
// Pass the same object for both arguments so the guard is true.
|
|
1485
|
+
const fakeModule = {};
|
|
1486
|
+
runIfMain(fakeModule, fakeModule, starter);
|
|
1487
|
+
// Wait for the microtask queue to flush the promise.
|
|
1488
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1489
|
+
assert.equal(called, true);
|
|
1159
1490
|
});
|
|
1160
1491
|
|
|
1161
1492
|
test('runIfMain does not call startServer when mainModule differs from currentModule', async () => {
|
|
1162
1493
|
let called = false;
|
|
1163
|
-
const
|
|
1164
|
-
indexModule.startServer = async () => {
|
|
1494
|
+
const starter = async () => {
|
|
1165
1495
|
called = true;
|
|
1166
1496
|
return null;
|
|
1167
1497
|
};
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
assert.equal(called, false);
|
|
1172
|
-
}
|
|
1173
|
-
finally {
|
|
1174
|
-
indexModule.startServer = originalStartServer;
|
|
1175
|
-
}
|
|
1498
|
+
runIfMain({}, {}, starter);
|
|
1499
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1500
|
+
assert.equal(called, false);
|
|
1176
1501
|
});
|