@jrpool/kilotest 46.1.2 → 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 +5 -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} +61 -36
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
- 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} +380 -143
- package/{index.js → index.ts} +382 -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 -89
- 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 -128
- package/util.js +0 -744
- 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
|
}
|
|
@@ -324,18 +331,18 @@ test('GET /fullReport.json/990101T0000/xxx returns an abuse error for a nonexist
|
|
|
324
331
|
assert.ok(res.body.includes('Invalid request'));
|
|
325
332
|
});
|
|
326
333
|
|
|
327
|
-
test('GET /
|
|
328
|
-
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');
|
|
329
336
|
assert.equal(res.statusCode, 200);
|
|
330
337
|
assert.ok(res.headers['content-type'].includes('image/png'));
|
|
331
338
|
});
|
|
332
339
|
|
|
333
|
-
test('GET /
|
|
340
|
+
test('GET /tutorialWeb/images with an unknown extension serves octet-stream', async () => {
|
|
334
341
|
// Create a temporary image file with an unknown extension.
|
|
335
|
-
const imgPath = path.join(
|
|
342
|
+
const imgPath = path.join(import.meta.dirname, 'web', 'tutorialWeb', 'images', 'test.bmp');
|
|
336
343
|
await fs.writeFile(imgPath, 'fake bitmap data');
|
|
337
344
|
try {
|
|
338
|
-
const res = await request('GET', '/
|
|
345
|
+
const res = await request('GET', '/tutorialWeb/images/test.bmp');
|
|
339
346
|
assert.equal(res.statusCode, 200);
|
|
340
347
|
assert.ok(res.headers['content-type'].includes('application/octet-stream'));
|
|
341
348
|
}
|
|
@@ -344,13 +351,34 @@ test('GET /tutorial/images with an unknown extension serves octet-stream', async
|
|
|
344
351
|
}
|
|
345
352
|
});
|
|
346
353
|
|
|
347
|
-
test('GET /
|
|
348
|
-
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');
|
|
349
356
|
assert.equal(res.statusCode, 400);
|
|
350
357
|
assert.ok(res.body.includes('Image not found'));
|
|
351
358
|
});
|
|
352
359
|
|
|
353
|
-
test('GET /
|
|
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');
|
|
377
|
+
assert.equal(res.statusCode, 400);
|
|
378
|
+
assert.ok(res.body.includes('Image not found'));
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test('GET /nonexistent.html returns an abuse error', {timeout: 500}, async () => {
|
|
354
382
|
const res = await request('GET', '/nonexistent.html');
|
|
355
383
|
assert.equal(res.statusCode, 400);
|
|
356
384
|
assert.ok(res.body.includes('Invalid request'));
|
|
@@ -366,7 +394,7 @@ test('GET /forbidden-path returns an invalid GET request error', async () => {
|
|
|
366
394
|
|
|
367
395
|
test('POST /requestTest.html with valid data returns an HTML page', async () => {
|
|
368
396
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
369
|
-
|
|
397
|
+
description: `Unique Test Page ${uniqueStamp}`,
|
|
370
398
|
url: `https://example.com/unique-${uniqueStamp}`,
|
|
371
399
|
why: 'Because accessibility matters'
|
|
372
400
|
});
|
|
@@ -376,7 +404,7 @@ test('POST /requestTest.html with valid data returns an HTML page', async () =>
|
|
|
376
404
|
|
|
377
405
|
test('POST /requestTest.html with an already-tested URL returns an error', async () => {
|
|
378
406
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
379
|
-
|
|
407
|
+
description: 'Mixed Outcomes Page',
|
|
380
408
|
url: 'https://example.com/mixed',
|
|
381
409
|
why: 'Because accessibility matters'
|
|
382
410
|
});
|
|
@@ -386,17 +414,23 @@ test('POST /requestTest.html with an already-tested URL returns an error', async
|
|
|
386
414
|
|
|
387
415
|
test('POST /requestTest.html with invalid data returns an error', async () => {
|
|
388
416
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
389
|
-
|
|
417
|
+
description: '',
|
|
390
418
|
url: 'not-a-url',
|
|
391
419
|
why: ''
|
|
392
420
|
});
|
|
393
421
|
assert.equal(res.statusCode, 400);
|
|
394
|
-
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'));
|
|
395
429
|
});
|
|
396
430
|
|
|
397
431
|
test('POST /requestRetest.html/260202T0000/new with valid data returns HTML', async () => {
|
|
398
|
-
// Reset
|
|
399
|
-
await fs.writeFile(
|
|
432
|
+
// Reset testRequests.json to avoid duplicate-request errors from prior tests.
|
|
433
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
400
434
|
const res = await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
401
435
|
why: 'Because the report is obsolete and needs refreshing'
|
|
402
436
|
});
|
|
@@ -409,7 +443,7 @@ test('POST /requestRetest.html/invalid/invalid with invalid data returns an erro
|
|
|
409
443
|
why: 'Because'
|
|
410
444
|
});
|
|
411
445
|
assert.equal(res.statusCode, 400);
|
|
412
|
-
assert.ok(res.body.includes('Invalid retest
|
|
446
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
413
447
|
});
|
|
414
448
|
|
|
415
449
|
test('POST /api/requestTest with valid JSON returns a JSON response', {timeout: 500}, async () => {
|
|
@@ -463,6 +497,36 @@ test('POST /worker/job without authentication returns 401', async () => {
|
|
|
463
497
|
assert.ok(body.error.message.includes('Unauthorized'));
|
|
464
498
|
});
|
|
465
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
|
+
|
|
466
530
|
test('POST /worker/job with valid authentication returns a job or no-job response', async () => {
|
|
467
531
|
// Clean up any claimed jobs left by prior tests.
|
|
468
532
|
const claimedDir = path.join(fixtureDBDir, 'jobs', 'claimed');
|
|
@@ -514,6 +578,26 @@ test('PUT / returns an invalid method error', async () => {
|
|
|
514
578
|
assert.ok(res.body.includes('Invalid request method'));
|
|
515
579
|
});
|
|
516
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
|
+
|
|
517
601
|
// TESTS: serveError behavior
|
|
518
602
|
|
|
519
603
|
test('serveError sends JSON for agent requests (isHumanUser = false)', async () => {
|
|
@@ -530,10 +614,10 @@ test('serveError sends HTML for human requests (isHumanUser = true)', async () =
|
|
|
530
614
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
531
615
|
});
|
|
532
616
|
|
|
533
|
-
// TESTS:
|
|
617
|
+
// TESTS: requestAction.html
|
|
534
618
|
|
|
535
|
-
test('POST /
|
|
536
|
-
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', {
|
|
537
621
|
target: 'https://example.com\tTest Page',
|
|
538
622
|
authCode: 'wrong-code'
|
|
539
623
|
});
|
|
@@ -541,13 +625,13 @@ test('POST /recAction.html with invalid auth code returns an error', {timeout: 5
|
|
|
541
625
|
assert.ok(res.body.includes('Invalid test order'));
|
|
542
626
|
});
|
|
543
627
|
|
|
544
|
-
test('POST /
|
|
628
|
+
test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML', async () => {
|
|
545
629
|
await formRequest('POST', '/requestTest.html', {
|
|
546
|
-
|
|
630
|
+
description: `Reject Test Page ${uniqueStamp}`,
|
|
547
631
|
url: `https://example.com/reject-${uniqueStamp}`,
|
|
548
632
|
why: 'Because accessibility matters'
|
|
549
633
|
});
|
|
550
|
-
const res = await formRequest('POST', '/
|
|
634
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
551
635
|
target: `https://example.com/reject-${uniqueStamp}`,
|
|
552
636
|
authCode: 'test-auth-code'
|
|
553
637
|
});
|
|
@@ -555,31 +639,38 @@ test('POST /recAction.html with valid auth code and rejection (no what) returns
|
|
|
555
639
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
556
640
|
});
|
|
557
641
|
|
|
558
|
-
// TESTS:
|
|
642
|
+
// TESTS: tutorialWeb (web user tutorial)
|
|
643
|
+
|
|
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
|
+
});
|
|
559
649
|
|
|
560
|
-
test('POST /
|
|
561
|
-
const res = await request('POST', '/
|
|
562
|
-
content:
|
|
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}`
|
|
563
653
|
});
|
|
564
654
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
655
|
+
assert.deepEqual(JSON.parse(res.body), {status: 'ok'});
|
|
565
656
|
});
|
|
566
657
|
|
|
567
658
|
// TESTS: additional branch coverage
|
|
568
659
|
|
|
569
|
-
test('GET /fullReport.json/260101T0007/hid returns an
|
|
660
|
+
test('GET /fullReport.json/260101T0007/hid returns an error for an unavailable report', async () => {
|
|
570
661
|
const res = await request('GET', '/fullReport.json/260101T0007/hid');
|
|
571
662
|
assert.equal(res.statusCode, 400);
|
|
572
663
|
assert.ok(res.body.includes('Invalid request'));
|
|
573
664
|
});
|
|
574
665
|
|
|
575
|
-
test('POST /
|
|
576
|
-
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');
|
|
577
668
|
await formRequest('POST', '/requestTest.html', {
|
|
578
|
-
|
|
669
|
+
description: `Approval Test Page ${uniqueStamp}`,
|
|
579
670
|
url: `https://example.com/approval-${uniqueStamp}`,
|
|
580
671
|
why: 'Because accessibility matters'
|
|
581
672
|
});
|
|
582
|
-
const res = await formRequest('POST', '/
|
|
673
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
583
674
|
target: `https://example.com/approval-${uniqueStamp}\tApproval Test Page ${uniqueStamp}`,
|
|
584
675
|
authCode: 'test-auth-code'
|
|
585
676
|
});
|
|
@@ -595,10 +686,10 @@ test('POST /reannotate.html with invalid auth code returns an error page', async
|
|
|
595
686
|
});
|
|
596
687
|
|
|
597
688
|
test('POST /reannotate.html with valid auth code serves the answer page', async () => {
|
|
598
|
-
// Back up all fixture reports, because
|
|
689
|
+
// Back up all fixture reports, because reannotation modifies them in place.
|
|
599
690
|
const reportsDir = path.join(fixtureDBDir, 'reports');
|
|
600
691
|
const reportFiles = await fs.readdir(reportsDir);
|
|
601
|
-
const backups = {};
|
|
692
|
+
const backups: Record<string, string> = {};
|
|
602
693
|
for (const file of reportFiles) {
|
|
603
694
|
backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
|
|
604
695
|
}
|
|
@@ -613,7 +704,7 @@ test('POST /reannotate.html with valid auth code serves the answer page', async
|
|
|
613
704
|
finally {
|
|
614
705
|
// Restore all fixture reports.
|
|
615
706
|
for (const file of reportFiles) {
|
|
616
|
-
await fs.writeFile(path.join(reportsDir, file), backups[file]);
|
|
707
|
+
await fs.writeFile(path.join(reportsDir, file), backups[file]!);
|
|
617
708
|
}
|
|
618
709
|
}
|
|
619
710
|
});
|
|
@@ -628,7 +719,7 @@ test('POST /renewWCAG.html with invalid auth code returns an error page', async
|
|
|
628
719
|
test('POST /renewWCAG.html with valid auth code serves the answer page', async () => {
|
|
629
720
|
// Mock fetch to avoid a network dependency.
|
|
630
721
|
const originalFetch = global.fetch;
|
|
631
|
-
const wcagMapPath = path.join(
|
|
722
|
+
const wcagMapPath = path.join(import.meta.dirname, 'wcagMap.json');
|
|
632
723
|
const wcagMapBackup = await fs.readFile(wcagMapPath, 'utf8');
|
|
633
724
|
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
634
725
|
global.fetch = async () => ({
|
|
@@ -649,21 +740,56 @@ test('POST /renewWCAG.html with valid auth code serves the answer page', async (
|
|
|
649
740
|
}
|
|
650
741
|
});
|
|
651
742
|
|
|
652
|
-
test('POST /
|
|
653
|
-
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', {
|
|
654
768
|
content: ''
|
|
655
769
|
});
|
|
656
770
|
assert.ok(res.headers['content-type'].includes('application/json'));
|
|
657
771
|
});
|
|
658
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
|
+
|
|
659
785
|
test('POST /requestTest.html with non-https URL returns an error', async () => {
|
|
660
786
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
661
|
-
|
|
787
|
+
description: 'Test Page',
|
|
662
788
|
url: 'http://example.com',
|
|
663
789
|
why: 'Because accessibility matters'
|
|
664
790
|
});
|
|
665
791
|
assert.equal(res.statusCode, 400);
|
|
666
|
-
assert.ok(res.body.includes('Invalid test
|
|
792
|
+
assert.ok(res.body.includes('Invalid test request'));
|
|
667
793
|
});
|
|
668
794
|
|
|
669
795
|
test('POST /requestRetest.html/260202T0000/new with missing why returns an error', async () => {
|
|
@@ -671,7 +797,7 @@ test('POST /requestRetest.html/260202T0000/new with missing why returns an error
|
|
|
671
797
|
why: ''
|
|
672
798
|
});
|
|
673
799
|
assert.equal(res.statusCode, 400);
|
|
674
|
-
assert.ok(res.body.includes('Invalid retest
|
|
800
|
+
assert.ok(res.body.includes('Invalid retest request'));
|
|
675
801
|
});
|
|
676
802
|
|
|
677
803
|
test('POST /worker/report with valid authentication and wrong worker returns an error', async () => {
|
|
@@ -689,6 +815,31 @@ test('POST /worker/report with valid authentication and wrong worker returns an
|
|
|
689
815
|
assert.ok(body.error);
|
|
690
816
|
});
|
|
691
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
|
+
|
|
692
843
|
test('POST /worker/job with wrong secret returns 401', async () => {
|
|
693
844
|
const auth = Buffer.from('worker1:wrongsecret').toString('base64');
|
|
694
845
|
const res = await request('POST', '/worker/job', {}, {
|
|
@@ -743,7 +894,7 @@ test('POST /worker/job with a claimed job assigned to the worker returns an erro
|
|
|
743
894
|
const body = jsonBody(res);
|
|
744
895
|
assert.ok(body.error.message.includes('has not completed job'));
|
|
745
896
|
// Wait for the async rename to complete.
|
|
746
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
897
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
747
898
|
// The job should have been moved to failed.
|
|
748
899
|
const failedExists = await fs.access(failedJobPath).then(() => true).catch(() => false);
|
|
749
900
|
assert.ok(failedExists, 'Job should be moved to failed directory');
|
|
@@ -778,7 +929,7 @@ test('POST /worker/job with a queued job assigns it to the worker', {timeout: 50
|
|
|
778
929
|
assert.equal(body.id, '260101T0000-mix');
|
|
779
930
|
assert.equal(body.sources.worker, 'Worker One');
|
|
780
931
|
// Wait for the async unlink to complete.
|
|
781
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
932
|
+
await new Promise<void>(resolve => setTimeout(resolve, 100));
|
|
782
933
|
// The job should have been moved from queue to claimed.
|
|
783
934
|
const queueExists = await fs.access(path.join(queueDir, jobFile)).then(() => true).catch(() => false);
|
|
784
935
|
assert.equal(queueExists, false, 'Job should be removed from queue');
|
|
@@ -823,12 +974,57 @@ test('POST /worker/report with valid authentication and valid claimed job proces
|
|
|
823
974
|
assert.equal(body.status, 'ok');
|
|
824
975
|
});
|
|
825
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
|
+
|
|
826
1022
|
// TESTS: remaining error branches and web pages
|
|
827
1023
|
|
|
828
1024
|
test('GET /style.css with a read error returns an error page', async () => {
|
|
829
1025
|
// Temporarily rename style.css to trigger a read error.
|
|
830
|
-
const stylePath = path.join(
|
|
831
|
-
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');
|
|
832
1028
|
await fs.rename(stylePath, tempPath);
|
|
833
1029
|
try {
|
|
834
1030
|
const res = await request('GET', '/style.css');
|
|
@@ -855,7 +1051,7 @@ test('GET /requestRetest.html/260202T0000/new returns an error page when called
|
|
|
855
1051
|
// TESTS: GET HTML page routing
|
|
856
1052
|
// These verify that the HTTP routing layer connects each URL to the correct
|
|
857
1053
|
// handler and returns HTML with a 200 status. The handler logic itself is
|
|
858
|
-
// tested by the unit tests in web/*/*.test.
|
|
1054
|
+
// tested by the unit tests in web/*/*.test.cjs.
|
|
859
1055
|
|
|
860
1056
|
const htmlPagePaths = [
|
|
861
1057
|
'/listReports.html',
|
|
@@ -863,7 +1059,8 @@ const htmlPagePaths = [
|
|
|
863
1059
|
'/listDiagnoses.html/linkNoText/260101T0000/mix/0',
|
|
864
1060
|
'/enqueueForm.html',
|
|
865
1061
|
'/manage.html',
|
|
866
|
-
'/
|
|
1062
|
+
'/tutorialWeb.html',
|
|
1063
|
+
'/tutorialAI.html',
|
|
867
1064
|
'/listTopIssues.html',
|
|
868
1065
|
'/reannotateForm.html',
|
|
869
1066
|
'/renewWCAGForm.html',
|
|
@@ -876,17 +1073,17 @@ const htmlPagePaths = [
|
|
|
876
1073
|
];
|
|
877
1074
|
|
|
878
1075
|
for (const pagePath of htmlPagePaths) {
|
|
879
|
-
test(`GET ${pagePath} serves a generated HTML page`, async () => {
|
|
1076
|
+
test(`GET ${pagePath} serves a generated HTML page`, {timeout: 500}, async () => {
|
|
880
1077
|
const res = await request('GET', pagePath);
|
|
881
1078
|
assert.equal(res.statusCode, 200);
|
|
882
1079
|
assert.ok(res.headers['content-type'].includes('text/html'));
|
|
883
1080
|
});
|
|
884
1081
|
}
|
|
885
1082
|
|
|
886
|
-
test('GET /enqueueForm.html shows
|
|
887
|
-
await fs.writeFile(
|
|
1083
|
+
test('GET /enqueueForm.html shows requests when testRequests.json has entries', async () => {
|
|
1084
|
+
await fs.writeFile(testRequestsPath, JSON.stringify({
|
|
888
1085
|
'https://example.com/enqueue-test': [
|
|
889
|
-
{
|
|
1086
|
+
{description: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
|
|
890
1087
|
]
|
|
891
1088
|
}));
|
|
892
1089
|
const res = await request('GET', '/enqueueForm.html');
|
|
@@ -909,52 +1106,104 @@ test('GET /test.html.bak matches isPathAllowed but falls through to catch-all',
|
|
|
909
1106
|
assert.ok(res.body.includes('Invalid GET request'));
|
|
910
1107
|
});
|
|
911
1108
|
|
|
912
|
-
test('GET /mcp returns a
|
|
913
|
-
const res = await request('GET', '/mcp');
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
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);
|
|
917
1115
|
});
|
|
918
1116
|
|
|
919
|
-
test('
|
|
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);
|
|
1152
|
+
});
|
|
1153
|
+
|
|
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 () => {
|
|
920
1166
|
const res = await request('POST', '/mcp', {
|
|
921
1167
|
jsonrpc: '2.0',
|
|
922
1168
|
method: 'initialize',
|
|
923
1169
|
id: 1,
|
|
924
1170
|
params: {
|
|
925
|
-
protocolVersion: '
|
|
1171
|
+
protocolVersion: '2025-06-18',
|
|
926
1172
|
capabilities: {},
|
|
927
1173
|
clientInfo: {name: 'test-client', version: '1.0'}
|
|
928
1174
|
}
|
|
929
1175
|
}, {
|
|
930
1176
|
accept: 'application/json, text/event-stream'
|
|
931
1177
|
});
|
|
932
|
-
|
|
933
|
-
|
|
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');
|
|
934
1183
|
});
|
|
935
1184
|
|
|
936
1185
|
// TESTS: answer error branches
|
|
937
1186
|
|
|
938
1187
|
test('POST /requestTest.html with valid format but duplicate URL returns an answer error', {timeout: 500}, async () => {
|
|
939
|
-
await fs.writeFile(
|
|
940
|
-
// First request to create the
|
|
1188
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
1189
|
+
// First request to create the request.
|
|
941
1190
|
await formRequest('POST', '/requestTest.html', {
|
|
942
|
-
|
|
1191
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
943
1192
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
944
1193
|
why: 'Because accessibility matters'
|
|
945
1194
|
});
|
|
946
1195
|
// Second request with the same URL should get a duplicate error.
|
|
947
1196
|
const res = await formRequest('POST', '/requestTest.html', {
|
|
948
|
-
|
|
1197
|
+
description: `Dup Test Page ${uniqueStamp}`,
|
|
949
1198
|
url: `https://example.com/dup-${uniqueStamp}`,
|
|
950
1199
|
why: 'Because accessibility matters again'
|
|
951
1200
|
});
|
|
952
1201
|
assert.equal(res.statusCode, 400);
|
|
953
|
-
assert.ok(res.body.includes('Duplicate
|
|
1202
|
+
assert.ok(res.body.includes('Duplicate request'));
|
|
954
1203
|
});
|
|
955
1204
|
|
|
956
1205
|
test('POST /requestRetest.html with valid format but duplicate retest returns an answer error', {timeout: 500}, async () => {
|
|
957
|
-
await fs.writeFile(
|
|
1206
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
958
1207
|
// First retest to create the recommendation.
|
|
959
1208
|
await formRequest('POST', '/requestRetest.html/260202T0000/new', {
|
|
960
1209
|
why: 'Because the report is obsolete and needs refreshing'
|
|
@@ -964,26 +1213,26 @@ test('POST /requestRetest.html with valid format but duplicate retest returns an
|
|
|
964
1213
|
why: 'Because the report is obsolete and needs refreshing again'
|
|
965
1214
|
});
|
|
966
1215
|
assert.equal(res.statusCode, 400);
|
|
967
|
-
assert.ok(res.body.includes('Duplicate
|
|
1216
|
+
assert.ok(res.body.includes('Duplicate request'));
|
|
968
1217
|
});
|
|
969
1218
|
|
|
970
|
-
test('POST /
|
|
971
|
-
await fs.writeFile(
|
|
972
|
-
// 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.
|
|
973
1222
|
await formRequest('POST', '/requestTest.html', {
|
|
974
|
-
|
|
1223
|
+
description: `Action Dup Page ${uniqueStamp}`,
|
|
975
1224
|
url: `https://example.com/action-dup-${uniqueStamp}`,
|
|
976
1225
|
why: 'Because accessibility matters'
|
|
977
1226
|
});
|
|
978
1227
|
// Approve it once.
|
|
979
|
-
await formRequest('POST', '/
|
|
1228
|
+
await formRequest('POST', '/requestAction.html', {
|
|
980
1229
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
981
1230
|
authCode: 'test-auth-code',
|
|
982
1231
|
what: 'yes'
|
|
983
1232
|
});
|
|
984
|
-
// Approve it again (the
|
|
985
|
-
// This test covers the
|
|
986
|
-
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', {
|
|
987
1236
|
target: `https://example.com/action-dup-${uniqueStamp}\tAction Dup Page ${uniqueStamp}`,
|
|
988
1237
|
authCode: 'test-auth-code',
|
|
989
1238
|
what: 'yes'
|
|
@@ -992,10 +1241,10 @@ test('POST /recAction.html with valid auth code and approval of a duplicate retu
|
|
|
992
1241
|
assert.ok(res.statusCode === 200 || res.statusCode === 400);
|
|
993
1242
|
});
|
|
994
1243
|
|
|
995
|
-
test('POST /
|
|
1244
|
+
test('POST /requestAction.html with valid auth code and approval of an invalid URL returns an error', async () => {
|
|
996
1245
|
// A URL that starts with https:// but is not a valid URL causes
|
|
997
1246
|
// enqueue.answer to return status error.
|
|
998
|
-
const res = await formRequest('POST', '/
|
|
1247
|
+
const res = await formRequest('POST', '/requestAction.html', {
|
|
999
1248
|
target: 'https://\tTest Page',
|
|
1000
1249
|
authCode: 'test-auth-code',
|
|
1001
1250
|
what: 'yes'
|
|
@@ -1006,22 +1255,22 @@ test('POST /recAction.html with valid auth code and approval of an invalid URL r
|
|
|
1006
1255
|
|
|
1007
1256
|
// UNIT TESTS FOR serveError
|
|
1008
1257
|
|
|
1009
|
-
const mockRes = () => {
|
|
1010
|
-
const state = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
1258
|
+
const mockRes = (): any => {
|
|
1259
|
+
const state: any = {statusCode: null, headers: {}, body: null, writableEnded: false};
|
|
1011
1260
|
return {
|
|
1012
1261
|
get statusCode() {return state.statusCode;},
|
|
1013
|
-
set statusCode(value) {state.statusCode = value;},
|
|
1014
|
-
setHeader(name, value) {state.headers[name] = value;},
|
|
1015
|
-
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;},
|
|
1016
1265
|
_state: state
|
|
1017
1266
|
};
|
|
1018
1267
|
};
|
|
1019
1268
|
|
|
1020
1269
|
test('serveError does not write to a response that has already ended', async () => {
|
|
1021
1270
|
let wrote = false;
|
|
1022
|
-
const mockResponse = {
|
|
1271
|
+
const mockResponse: any = {
|
|
1023
1272
|
writableEnded: true,
|
|
1024
|
-
set statusCode(value) {
|
|
1273
|
+
set statusCode(value: any) {
|
|
1025
1274
|
wrote = true;
|
|
1026
1275
|
},
|
|
1027
1276
|
setHeader() {
|
|
@@ -1063,7 +1312,7 @@ test('getAbuseError uses unknown IP when no forwarding header or remote address'
|
|
|
1063
1312
|
headers: {},
|
|
1064
1313
|
socket: {remoteAddress: undefined}
|
|
1065
1314
|
};
|
|
1066
|
-
const result = getAbuseError(mockRequest, 'test reason');
|
|
1315
|
+
const result = getAbuseError(mockRequest as any, 'test reason');
|
|
1067
1316
|
assert.equal(result['IP address'], 'unknown');
|
|
1068
1317
|
assert.equal(result.reason, 'test reason');
|
|
1069
1318
|
});
|
|
@@ -1090,16 +1339,16 @@ test('startServer starts an HTTP server when protocol is http', {timeout: 500},
|
|
|
1090
1339
|
assert.equal(typeof server.listen, 'function');
|
|
1091
1340
|
// Verify the server is listening by making a request.
|
|
1092
1341
|
const address = server.address();
|
|
1093
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1342
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1094
1343
|
}
|
|
1095
1344
|
finally {
|
|
1096
|
-
server
|
|
1097
|
-
await new Promise(resolve => {
|
|
1345
|
+
server?.closeAllConnections?.();
|
|
1346
|
+
await new Promise<void>(resolve => {
|
|
1098
1347
|
const timer = setTimeout(() => {
|
|
1099
|
-
server
|
|
1348
|
+
server?.closeAllConnections?.();
|
|
1100
1349
|
resolve();
|
|
1101
1350
|
}, 1000);
|
|
1102
|
-
server
|
|
1351
|
+
server?.close(() => {
|
|
1103
1352
|
clearTimeout(timer);
|
|
1104
1353
|
resolve();
|
|
1105
1354
|
});
|
|
@@ -1116,12 +1365,12 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1116
1365
|
delete process.env.PORT;
|
|
1117
1366
|
try {
|
|
1118
1367
|
const server = await indexModule.serve(http, {});
|
|
1119
|
-
let bindError = null;
|
|
1368
|
+
let bindError: any = null;
|
|
1120
1369
|
server.on('error', error => {
|
|
1121
1370
|
bindError = error;
|
|
1122
1371
|
});
|
|
1123
1372
|
// Wait briefly for either successful binding or an EADDRINUSE error.
|
|
1124
|
-
await new Promise(resolve => {
|
|
1373
|
+
await new Promise<void>(resolve => {
|
|
1125
1374
|
const timer = setTimeout(resolve, 200);
|
|
1126
1375
|
server.on('listening', () => {
|
|
1127
1376
|
clearTimeout(timer);
|
|
@@ -1133,10 +1382,10 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1133
1382
|
}
|
|
1134
1383
|
else {
|
|
1135
1384
|
const address = server.address();
|
|
1136
|
-
assert.ok(typeof address === 'object' && address.port === 3000);
|
|
1385
|
+
assert.ok(typeof address === 'object' && address !== null && address.port === 3000);
|
|
1137
1386
|
}
|
|
1138
1387
|
server.closeAllConnections?.();
|
|
1139
|
-
await new Promise(resolve => {
|
|
1388
|
+
await new Promise<void>(resolve => {
|
|
1140
1389
|
const timer = setTimeout(() => {
|
|
1141
1390
|
server.closeAllConnections?.();
|
|
1142
1391
|
resolve();
|
|
@@ -1159,9 +1408,9 @@ test('serve uses the default port 3000 when PORT is not set', {timeout: 500}, as
|
|
|
1159
1408
|
|
|
1160
1409
|
test('startServer starts an HTTPS server when protocol is https', {timeout: 2000}, async () => {
|
|
1161
1410
|
// Generate a self-signed certificate for the test using openssl.
|
|
1162
|
-
const {execSync} =
|
|
1163
|
-
const os =
|
|
1164
|
-
const fsSync =
|
|
1411
|
+
const {execSync} = await import('node:child_process');
|
|
1412
|
+
const os = await import('node:os');
|
|
1413
|
+
const fsSync = await import('node:fs');
|
|
1165
1414
|
const tmpDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'kilotest-https-'));
|
|
1166
1415
|
const keyPath = path.join(tmpDir, 'key.pem');
|
|
1167
1416
|
const certPath = path.join(tmpDir, 'cert.pem');
|
|
@@ -1184,9 +1433,9 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1184
1433
|
assert.ok(server);
|
|
1185
1434
|
assert.equal(typeof server.listen, 'function');
|
|
1186
1435
|
const address = server.address();
|
|
1187
|
-
assert.ok(typeof address === 'object' && address.port > 0);
|
|
1436
|
+
assert.ok(typeof address === 'object' && address !== null && address.port > 0);
|
|
1188
1437
|
server.closeAllConnections?.();
|
|
1189
|
-
await new Promise(resolve => {
|
|
1438
|
+
await new Promise<void>(resolve => {
|
|
1190
1439
|
const timer = setTimeout(() => {
|
|
1191
1440
|
server.closeAllConnections?.();
|
|
1192
1441
|
resolve();
|
|
@@ -1228,37 +1477,25 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
|
|
|
1228
1477
|
|
|
1229
1478
|
test('runIfMain calls startServer when mainModule matches currentModule', async () => {
|
|
1230
1479
|
let called = false;
|
|
1231
|
-
const
|
|
1232
|
-
indexModule.startServer = async () => {
|
|
1480
|
+
const starter = async () => {
|
|
1233
1481
|
called = true;
|
|
1234
1482
|
return null;
|
|
1235
1483
|
};
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
assert.equal(called, true);
|
|
1243
|
-
}
|
|
1244
|
-
finally {
|
|
1245
|
-
indexModule.startServer = originalStartServer;
|
|
1246
|
-
}
|
|
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);
|
|
1247
1490
|
});
|
|
1248
1491
|
|
|
1249
1492
|
test('runIfMain does not call startServer when mainModule differs from currentModule', async () => {
|
|
1250
1493
|
let called = false;
|
|
1251
|
-
const
|
|
1252
|
-
indexModule.startServer = async () => {
|
|
1494
|
+
const starter = async () => {
|
|
1253
1495
|
called = true;
|
|
1254
1496
|
return null;
|
|
1255
1497
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
assert.equal(called, false);
|
|
1260
|
-
}
|
|
1261
|
-
finally {
|
|
1262
|
-
indexModule.startServer = originalStartServer;
|
|
1263
|
-
}
|
|
1498
|
+
runIfMain({}, {}, starter);
|
|
1499
|
+
await new Promise<void>(resolve => setImmediate(resolve));
|
|
1500
|
+
assert.equal(called, false);
|
|
1264
1501
|
});
|