@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.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,47 +1,50 @@
1
1
  /*
2
- index.test.js
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 (must be set before requiring index.js, because index.js reads
8
- // TESTARO_WORKERS and AUTH_CODE at module load time, and util.js reads DB_DIR.)
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
- const path = require('node:path');
11
- const fixtureDBDir = path.join(__dirname, 'test', 'fixtures', 'db');
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
- const {test, before, beforeEach, after} = require('node:test');
22
- const assert = require('node:assert/strict');
23
- const http = require('node:http');
24
- const fs = require('node:fs/promises');
25
- const indexModule = require('./index');
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 recsPath = path.join(fixtureDBDir, 'jobs', 'recs.json');
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 recs.json and clean job directories before each test, so tests do not depend on execution order.
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(recsPath, '{}\n');
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 recs.json and clean job directories after running.
69
- await fs.writeFile(recsPath, '{}\n');
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 /tutorial/images/newsletter-form.png serves the image', async () => {
328
- const res = await request('GET', '/tutorial/images/newsletter-form.png');
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 /tutorial/images with an unknown extension serves octet-stream', async () => {
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(__dirname, 'web', 'tutorial', 'images', 'test.bmp');
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', '/tutorial/images/test.bmp');
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 /tutorial/images/nonexistent.png returns an error page', async () => {
348
- const res = await request('GET', '/tutorial/images/nonexistent.png');
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 /nonexistent.html returns an abuse error', async () => {
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
- what: `Unique Test Page ${uniqueStamp}`,
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
- what: 'Mixed Outcomes Page',
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
- what: '',
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 recommendation'));
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 recs.json to avoid duplicate-recommendation errors from prior tests.
399
- await fs.writeFile(recsPath, '{}\n');
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 recommendation'));
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: recAction.html
617
+ // TESTS: requestAction.html
534
618
 
535
- test('POST /recAction.html with invalid auth code returns an error', {timeout: 500}, async () => {
536
- const res = await formRequest('POST', '/recAction.html', {
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 /recAction.html with valid auth code and rejection (no what) returns HTML', async () => {
628
+ test('POST /requestAction.html with valid auth code and rejection (no description) returns HTML', async () => {
545
629
  await formRequest('POST', '/requestTest.html', {
546
- what: `Reject Test Page ${uniqueStamp}`,
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', '/recAction.html', {
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: tutorialComment
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 /tutorialComment.html with content returns JSON', async () => {
561
- const res = await request('POST', '/tutorialComment.html', {
562
- content: 'This is a test comment'
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 abuse error for a hidden report', async () => {
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 /recAction.html with valid auth code and approval returns HTML', {timeout: 500}, async () => {
576
- await fs.writeFile(recsPath, '{}\n');
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
- what: `Approval Test Page ${uniqueStamp}`,
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', '/recAction.html', {
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 annotateReport modifies them in place.
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(__dirname, 'wcagMap.json');
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 /tutorialComment.html with empty content returns a JSON error', {timeout: 500}, async () => {
653
- const res = await request('POST', '/tutorialComment.html', {
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
- what: 'Test Page',
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 recommendation'));
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 recommendation'));
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(__dirname, 'style.css');
831
- const tempPath = path.join(__dirname, 'style.css.bak');
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.js.
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
- '/tutorial.html',
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 recommendations when recs.json has entries', async () => {
887
- await fs.writeFile(recsPath, JSON.stringify({
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
- {what: 'Enqueue Test Page', why: 'Needs testing for accessibility'}
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 response from the MCP handler', async () => {
913
- const res = await request('GET', '/mcp');
914
- // The MCP handler responds to GET requests, typically with an error
915
- // about acceptable content types or a similar MCP protocol message.
916
- assert.ok(res.statusCode >= 400);
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('POST /mcp returns a response from the MCP handler', async () => {
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: '2024-11-05',
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
- // The MCP handler should process the initialize request.
933
- assert.ok(res.statusCode === 200 || res.statusCode >= 400);
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(recsPath, '{}\n');
940
- // First request to create the recommendation.
1188
+ await fs.writeFile(testRequestsPath, '{}\n');
1189
+ // First request to create the request.
941
1190
  await formRequest('POST', '/requestTest.html', {
942
- what: `Dup Test Page ${uniqueStamp}`,
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
- what: `Dup Test Page ${uniqueStamp}`,
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 recommendation'));
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(recsPath, '{}\n');
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 recommendation'));
1216
+ assert.ok(res.body.includes('Duplicate request'));
968
1217
  });
969
1218
 
970
- test('POST /recAction.html with valid auth code and approval of a duplicate returns an error', {timeout: 500}, async () => {
971
- await fs.writeFile(recsPath, '{}\n');
972
- // Create a recommendation.
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
- what: `Action Dup Page ${uniqueStamp}`,
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', '/recAction.html', {
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 recs may have been cleared, so this may succeed or fail).
985
- // This test covers the recAction answer error branch.
986
- const res = await formRequest('POST', '/recAction.html', {
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 /recAction.html with valid auth code and approval of an invalid URL returns an error', async () => {
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', '/recAction.html', {
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.closeAllConnections?.();
1097
- await new Promise(resolve => {
1345
+ server?.closeAllConnections?.();
1346
+ await new Promise<void>(resolve => {
1098
1347
  const timer = setTimeout(() => {
1099
- server.closeAllConnections?.();
1348
+ server?.closeAllConnections?.();
1100
1349
  resolve();
1101
1350
  }, 1000);
1102
- server.close(() => {
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} = require('node:child_process');
1163
- const os = require('node:os');
1164
- const fsSync = require('node:fs');
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 originalStartServer = indexModule.startServer;
1232
- indexModule.startServer = async () => {
1480
+ const starter = async () => {
1233
1481
  called = true;
1234
1482
  return null;
1235
1483
  };
1236
- try {
1237
- // Pass the same object for both arguments so the guard is true.
1238
- const fakeModule = {};
1239
- runIfMain(fakeModule, fakeModule);
1240
- // Wait for the microtask queue to flush the promise.
1241
- await new Promise(resolve => setImmediate(resolve));
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 originalStartServer = indexModule.startServer;
1252
- indexModule.startServer = async () => {
1494
+ const starter = async () => {
1253
1495
  called = true;
1254
1496
  return null;
1255
1497
  };
1256
- try {
1257
- runIfMain({}, {});
1258
- await new Promise(resolve => setImmediate(resolve));
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
  });