@jrpool/kilotest 46.1.0 → 47.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +8 -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} +75 -46
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
  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} +462 -137
  48. package/{index.js → index.ts} +388 -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 -87
  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 -126
  133. package/util.js +0 -733
  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,16 +1,18 @@
1
1
  /*
2
- util.test.js
3
- Tests for util.js data-path injection (Phase 1) and utility functions.
2
+ util.test.ts
3
+ Tests for util.ts data-path injection (Phase 1) and utility functions.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const path = require('node:path');
11
- const fs = require('node:fs/promises');
12
- const {
13
- annotateReport,
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import path from 'node:path';
11
+ import fs from 'node:fs/promises';
12
+ import {
13
+ annotateReportObject,
14
+ checkCommentDuplicate,
15
+ checkCommentLength,
14
16
  createLock,
15
17
  dbPath,
16
18
  getAgoDays,
@@ -18,10 +20,10 @@ const {
18
20
  getCountString,
19
21
  getDateString,
20
22
  getDateTime,
21
- getEngineList,
23
+ getEngineNamesString,
22
24
  getIssue,
25
+ errorMessage,
23
26
  getJSON,
24
- getLatestReportExtracts,
25
27
  getMultiReportWhats,
26
28
  getNowStamp,
27
29
  getObject,
@@ -39,7 +41,6 @@ const {
39
41
  getWeightName,
40
42
  hiddenReportsPath,
41
43
  htmlSafe,
42
- isHidden,
43
44
  isJobID,
44
45
  isReportAvailable,
45
46
  isTimeStamp,
@@ -49,19 +50,74 @@ const {
49
50
  minifyURL,
50
51
  objectSort,
51
52
  processTestRequest,
52
- recsLock,
53
- recsPath,
53
+ testRequestsLock,
54
+ testRequestsPath,
54
55
  reportsPath,
55
- updateRecs
56
- } = require('./util');
56
+ addTestRequest
57
+ } from './util.ts';
57
58
 
58
59
  // TESTS
59
60
 
61
+ test('checkCommentLength accepts a comment of exactly 20 characters', () => {
62
+ const result = checkCommentLength('a'.repeat(20));
63
+ assert.deepEqual(result, {status: 'ok'});
64
+ });
65
+
66
+ test('checkCommentLength accepts a comment of exactly 1000 characters', () => {
67
+ const result = checkCommentLength('a'.repeat(1000));
68
+ assert.deepEqual(result, {status: 'ok'});
69
+ });
70
+
71
+ test('checkCommentLength rejects a comment shorter than 20 characters', () => {
72
+ const result = checkCommentLength('a'.repeat(19));
73
+ assert.deepEqual(result, {
74
+ status: 'error',
75
+ message: 'Your comment was shorter than 20 characters'
76
+ });
77
+ });
78
+
79
+ test('checkCommentLength rejects a comment longer than 1000 characters', () => {
80
+ const result = checkCommentLength('a'.repeat(1001));
81
+ assert.deepEqual(result, {
82
+ status: 'error',
83
+ message: 'Your comment was longer than 1000 characters'
84
+ });
85
+ });
86
+
87
+ test('checkCommentDuplicate accepts a comment with no existing comments', () => {
88
+ const result = checkCommentDuplicate([], 'a'.repeat(20));
89
+ assert.deepEqual(result, {status: 'ok'});
90
+ });
91
+
92
+ test('checkCommentDuplicate rejects a comment repeating one submitted within the last 1000 seconds', () => {
93
+ const content = 'a'.repeat(20);
94
+ const comments = [{timeStamp: getNowStamp(), content}];
95
+ const result = checkCommentDuplicate(comments, content);
96
+ assert.deepEqual(result, {
97
+ status: 'error',
98
+ message: 'Your comment repeats a recently submitted one, but you are welcome to submit a different comment'
99
+ });
100
+ });
101
+
102
+ test('checkCommentDuplicate accepts a comment repeating one submitted more than 1000 seconds ago', () => {
103
+ const content = 'a'.repeat(20);
104
+ const oldTimeStamp = getTimeStamp(new Date(Date.now() - 2000000));
105
+ const comments = [{timeStamp: oldTimeStamp, content}];
106
+ const result = checkCommentDuplicate(comments, content);
107
+ assert.deepEqual(result, {status: 'ok'});
108
+ });
109
+
110
+ test('checkCommentDuplicate accepts a comment that differs from a recent one', () => {
111
+ const comments = [{timeStamp: getNowStamp(), content: 'a'.repeat(20)}];
112
+ const result = checkCommentDuplicate(comments, 'b'.repeat(20));
113
+ assert.deepEqual(result, {status: 'ok'});
114
+ });
115
+
60
116
  test('dbPath defaults to the project db directory when DB_DIR is unset', () => {
61
117
  const saved = process.env.DB_DIR;
62
118
  delete process.env.DB_DIR;
63
119
  try {
64
- assert.equal(dbPath(), path.join(__dirname, 'db'));
120
+ assert.equal(dbPath(), path.join(import.meta.dirname, 'db'));
65
121
  }
66
122
  finally {
67
123
  if (saved !== undefined) {
@@ -86,12 +142,12 @@ test('dbPath honors DB_DIR when it is set', () => {
86
142
  }
87
143
  });
88
144
 
89
- test('jobsPath, recsPath, reportsPath, and hiddenReportsPath derive from DB_DIR', () => {
145
+ test('jobsPath, testRequestsPath, reportsPath, and hiddenReportsPath derive from DB_DIR', () => {
90
146
  const saved = process.env.DB_DIR;
91
147
  process.env.DB_DIR = '/tmp/kilotest-fixtures';
92
148
  try {
93
149
  assert.equal(jobsPath(), path.join('/tmp/kilotest-fixtures', 'jobs'));
94
- assert.equal(recsPath(), path.join('/tmp/kilotest-fixtures', 'jobs', 'recs.json'));
150
+ assert.equal(testRequestsPath(), path.join('/tmp/kilotest-fixtures', 'jobs', 'testRequests.json'));
95
151
  assert.equal(reportsPath(), path.join('/tmp/kilotest-fixtures', 'reports'));
96
152
  assert.equal(hiddenReportsPath(), path.join('/tmp/kilotest-fixtures', 'hiddenReports'));
97
153
  }
@@ -134,7 +190,7 @@ test('getAgoDays returns null for an invalid string', () => {
134
190
  });
135
191
 
136
192
  test('getAgoDays returns null for a non-string non-Date argument', () => {
137
- assert.equal(getAgoDays(42), null);
193
+ assert.equal(getAgoDays(42 as any), null);
138
194
  });
139
195
 
140
196
  test('getAgoDays returns 0 for the current time', () => {
@@ -190,20 +246,26 @@ test('getIssue returns null for an unknown engine', () => {
190
246
  assert.equal(getIssue('nonexistentEngine', 'anyRule'), null);
191
247
  });
192
248
 
249
+ test('errorMessage returns the message of an Error instance', () => {
250
+ assert.equal(errorMessage(new Error('something went wrong')), 'something went wrong');
251
+ });
252
+
253
+ test('errorMessage returns the string representation of a non-Error value', () => {
254
+ assert.equal(errorMessage('bare string'), 'bare string');
255
+ });
256
+
193
257
  test('getJSON returns a JSON string with a trailing newline', () => {
194
258
  assert.equal(getJSON({a: 1}), '{\n "a": 1\n}\n');
195
259
  });
196
260
 
197
261
  test('getObject returns the parsed object for a valid JSON file', async () => {
198
- const result = await getObject(path.join(__dirname, 'package.json'));
199
- assert.ok(typeof result === 'object');
200
- assert.equal(result.name, '@jrpool/kilotest');
262
+ const result = await getObject(path.join(import.meta.dirname, 'package.json'));
263
+ assert.ok(typeof result === 'object' && result !== null);
264
+ assert.equal((result as {name: unknown}).name, '@jrpool/kilotest');
201
265
  });
202
266
 
203
- test('getObject returns an error string for a nonexistent file', async () => {
204
- const result = await getObject('/tmp/nonexistent-file.json');
205
- assert.equal(typeof result, 'string');
206
- assert.ok(result.startsWith('ERROR'));
267
+ test('getObject throws for a nonexistent file', async () => {
268
+ await assert.rejects(getObject('/tmp/nonexistent-file.json'), /not readable/);
207
269
  });
208
270
 
209
271
  test('getNowStamp returns an 11-character time stamp', () => {
@@ -264,10 +326,8 @@ test('isTimeStamp returns false for an invalid time stamp', () => {
264
326
  assert.equal(isTimeStamp('invalid'), false);
265
327
  });
266
328
 
267
- test('isURL returns a URL object for a valid HTTPS URL', () => {
268
- const result = isURL('https://example.com/page');
269
- assert.ok(result instanceof URL);
270
- assert.equal(result.hostname, 'example.com');
329
+ test('isURL returns true for a valid HTTPS URL', () => {
330
+ assert.equal(isURL('https://example.com/page'), true);
271
331
  });
272
332
 
273
333
  test('isURL returns false for a non-HTTPS URL', () => {
@@ -304,11 +364,11 @@ test('objectSort sorts objects numerically descending', () => {
304
364
  assert.deepEqual(sorted.map(i => i.count), [3, 2, 1]);
305
365
  });
306
366
 
307
- // TESTS FOR processTestRequest, annotateReport, AND isReportAvailable
367
+ // TESTS FOR processTestRequest AND isReportAvailable
308
368
 
309
369
  // These tests use the fixture database directory and the real web/requestTest template.
310
- const fixtureDbDir = path.join(__dirname, 'test', 'fixtures', 'db');
311
- const requestTestDir = path.join(__dirname, 'web', 'requestTest');
370
+ import {fixtureDBDir as fixtureDbDir} from './test/dbFixture.ts';
371
+ const requestTestDir = path.join(import.meta.dirname, 'web', 'requestTest');
312
372
  const savedDbDir = process.env.DB_DIR;
313
373
 
314
374
  before(() => {
@@ -325,20 +385,20 @@ after(() => {
325
385
  });
326
386
 
327
387
  test('getPageData returns page data for a valid report', async () => {
328
- const data = await getPageData('260101T0000', 'mix');
329
- assert.equal(data.what, 'Mixed Outcomes Page');
388
+ const data = await getPageData('260101T0000', 'mix') as any;
389
+ assert.equal(data.description, 'Mixed Outcomes Page');
330
390
  assert.equal(data.url, 'https://example.com/mixed');
331
391
  assert.equal(typeof data.daysAgo, 'number');
332
392
  });
333
393
 
334
394
  test('getPageData returns an error for a nonexistent report', async () => {
335
- const data = await getPageData('999999T9999', 'xxx');
395
+ const data = await getPageData('999999T9999', 'xxx') as any;
336
396
  assert.ok(data.error);
337
397
  });
338
398
 
339
399
  test('getPageDataStrings returns HTML strings for a valid report', async () => {
340
- const strings = await getPageDataStrings('260101T0000', 'mix');
341
- assert.equal(strings.what, 'Mixed Outcomes Page');
400
+ const strings = await getPageDataStrings('260101T0000', 'mix') as any;
401
+ assert.equal(strings.description, 'Mixed Outcomes Page');
342
402
  assert.equal(strings.url, 'https://example.com/mixed');
343
403
  assert.equal(strings.urlLink, '<a href="https://example.com/mixed">https://example.com/mixed</a>');
344
404
  assert.ok(strings.testInfo.includes('by job <code>mix</code>'));
@@ -346,8 +406,8 @@ test('getPageDataStrings returns HTML strings for a valid report', async () => {
346
406
  });
347
407
 
348
408
  test('getPageDataStrings returns different testInfo for a different timeStamp', async () => {
349
- const strings = await getPageDataStrings('260101T0001', 'ct');
350
- assert.equal(strings.what, 'All CantTell Page');
409
+ const strings = await getPageDataStrings('260101T0001', 'ct') as any;
410
+ assert.equal(strings.description, 'All CantTell Page');
351
411
  assert.ok(strings.testInfo.includes('by job <code>ct</code>'));
352
412
  assert.ok(strings.testInfo.includes('2026-01-01 at 00:01'));
353
413
  });
@@ -359,84 +419,128 @@ test('getPageDataStrings returns an error for a nonexistent report', async () =>
359
419
 
360
420
  test('getPageDataStrings uses provided pageData instead of reading the report', async () => {
361
421
  const strings = await getPageDataStrings('260101T0000', 'mix', {
362
- what: 'Custom Page',
422
+ description: 'Custom Page',
363
423
  url: 'https://custom.com',
364
424
  daysAgo: 1
365
- });
366
- assert.equal(strings.what, 'Custom Page');
425
+ }) as any;
426
+ assert.equal(strings.description, 'Custom Page');
367
427
  assert.equal(strings.url, 'https://custom.com');
368
428
  assert.ok(strings.testInfo.includes('1 day ago'));
369
429
  });
370
430
 
371
431
  test('processTestRequest returns an error for an invalid test type', async () => {
372
- const result = await processTestRequest('invalid', requestTestDir, 'Page', 'https://example.com', 'because');
432
+ const result: any = await processTestRequest('invalid', requestTestDir, 'Page', 'https://example.com', 'because');
373
433
  assert.equal(result.status, 'error');
374
- assert.equal(result.message, 'Invalid recommendation');
434
+ assert.equal(result.message, 'Invalid request');
375
435
  });
376
436
 
377
437
  test('processTestRequest returns an error for an invalid URL', async () => {
378
- const result = await processTestRequest('test', requestTestDir, 'Page', 'not-a-url', 'because');
438
+ const result: any = await processTestRequest('test', requestTestDir, 'Page', 'not-a-url', 'because');
379
439
  assert.equal(result.status, 'error');
380
- assert.equal(result.message, 'Invalid recommendation');
440
+ assert.equal(result.message, 'Invalid request');
381
441
  });
382
442
 
383
443
  test('processTestRequest returns an error for a short reason', async () => {
384
- const result = await processTestRequest('test', requestTestDir, 'Page', 'https://example.com', 'abc');
444
+ const result: any = await processTestRequest('test', requestTestDir, 'Page', 'https://example.com', 'abc');
385
445
  assert.equal(result.status, 'error');
386
- assert.equal(result.message, 'Invalid recommendation');
446
+ assert.equal(result.message, 'Invalid request');
387
447
  });
388
448
 
389
449
  test('processTestRequest returns an error for a missing description', async () => {
390
- const result = await processTestRequest('test', requestTestDir, '', 'https://example.com', 'because');
450
+ const result: any = await processTestRequest('test', requestTestDir, '', 'https://example.com', 'because');
391
451
  assert.equal(result.status, 'error');
392
- assert.equal(result.message, 'Invalid recommendation');
452
+ assert.equal(result.message, 'Invalid request');
393
453
  });
394
454
 
395
455
  test('processTestRequest returns an error for a mismatched directory name', async () => {
396
- const result = await processTestRequest('test', '/tmp/wrongDir', 'Page', 'https://example.com', 'because');
456
+ const result: any = await processTestRequest('test', '/tmp/wrongDir', 'Page', 'https://example.com', 'because');
397
457
  assert.equal(result.status, 'error');
398
- assert.equal(result.message, 'Invalid recommendation');
458
+ assert.equal(result.message, 'Invalid request');
399
459
  });
400
460
 
461
+
401
462
  test('processTestRequest succeeds and populates the template for a valid request', {timeout: 500}, async () => {
402
- // Reset recs.json to empty before the test.
403
- await fs.writeFile(recsPath(), '{}\n');
404
- const result = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
463
+ // Reset testRequests.json to empty before the test.
464
+ await fs.writeFile(testRequestsPath(), '{}\n');
465
+ const result: any = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
405
466
  assert.equal(result.status, 'ok');
406
467
  assert.ok(result.answerPage.includes('Example Page'));
407
468
  assert.ok(result.answerPage.includes('because accessibility'));
408
- // Clean up recs.json.
409
- await fs.writeFile(recsPath(), '{}\n');
469
+ // Clean up testRequests.json.
470
+ await fs.writeFile(testRequestsPath(), '{}\n');
410
471
  });
411
472
 
412
473
  test('processTestRequest returns a duplicate error for a repeated request', {timeout: 500}, async () => {
413
- // Reset recs.json to empty, then make a successful request.
414
- await fs.writeFile(recsPath(), '{}\n');
474
+ // Reset testRequests.json to empty, then make a successful request.
475
+ await fs.writeFile(testRequestsPath(), '{}\n');
415
476
  await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'because accessibility');
416
477
  // Repeat the same request.
417
- const result = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'another reason');
478
+ const result: any = await processTestRequest('test', requestTestDir, 'Example Page', 'https://example.com', 'another reason');
418
479
  assert.equal(result.status, 'error');
419
- assert.equal(result.message, 'Duplicate recommendation');
420
- // Clean up recs.json.
421
- await fs.writeFile(recsPath(), '{}\n');
480
+ assert.equal(result.message, 'Duplicate request');
481
+ // Clean up testRequests.json.
482
+ await fs.writeFile(testRequestsPath(), '{}\n');
422
483
  });
423
484
 
424
- test('annotateReport returns an error for a nonexistent report', async () => {
425
- const result = await annotateReport('990101T0000', 'xxx');
426
- assert.ok(typeof result === 'string');
427
- assert.ok(result.includes('missing'));
485
+ test('annotateReportObject annotates a report object in place without reading or writing a file', async () => {
486
+ // A report object that was never written to disk (e.g. one just received from a worker,
487
+ // as in index.ts's worker/report handler, which annotates before the first write).
488
+ const report: any = {
489
+ id: '990101T0099-obj',
490
+ target: {what: 'Object Page', url: 'https://example.com/object'},
491
+ acts: [
492
+ {
493
+ type: 'test',
494
+ which: 'alfa',
495
+ result: {
496
+ standardResult: {
497
+ instances: [
498
+ // A classifiable rule.
499
+ {ruleID: 'r11', what: 'The link does not have an accessible name', outcome: 'failed', catalogIndex: '0'},
500
+ // An unclassifiable rule.
501
+ {ruleID: 'unknownRule123', what: 'Unknown', outcome: 'failed', catalogIndex: '0'}
502
+ ]
503
+ }
504
+ }
505
+ }
506
+ ],
507
+ jobData: {endTime: '26-01-01T00:00'},
508
+ catalog: {}
509
+ };
510
+ await annotateReportObject(report);
511
+ const instances = report.acts[0].result.standardResult.instances;
512
+ assert.equal(instances[0].issueID, 'linkNoText');
513
+ assert.equal(instances[1].issueID, undefined);
514
+ assert.deepEqual(report.jobData.issuelessRules, ['alfa:unknownRule123']);
428
515
  });
429
516
 
430
- test('annotateReport annotates a valid report and returns an empty string', async () => {
431
- const reportPath = path.join(fixtureDbDir, 'reports', '260101T0000-mix.json');
432
- const original = await fs.readFile(reportPath, 'utf8');
433
- try {
434
- const result = await annotateReport('260101T0000', 'mix');
435
- assert.equal(result, '');
436
- }
437
- finally {
438
- await fs.writeFile(reportPath, original);
439
- }
517
+ test('annotateReportObject handles a test act with no standardResult instances', async () => {
518
+ const report: any = {
519
+ id: '990101T0098-ni',
520
+ target: {what: 'Test', url: 'https://example.com'},
521
+ acts: [
522
+ {type: 'test', which: 'axe', result: {standardResult: {instances: []}}},
523
+ {type: 'other'}
524
+ ],
525
+ jobData: {endTime: '26-01-01T00:00'},
526
+ catalog: {}
527
+ };
528
+ await annotateReportObject(report);
529
+ assert.deepEqual(report.jobData.issuelessRules, []);
530
+ });
531
+
532
+ test('annotateReportObject handles a test act with no standardResult', async () => {
533
+ const report: any = {
534
+ id: '990101T0097-ns',
535
+ target: {what: 'Test', url: 'https://example.com'},
536
+ acts: [
537
+ {type: 'test', which: 'axe', result: {}}
538
+ ],
539
+ jobData: {endTime: '26-01-01T00:00'},
540
+ catalog: {}
541
+ };
542
+ await annotateReportObject(report);
543
+ assert.deepEqual(report.jobData.issuelessRules, []);
440
544
  });
441
545
 
442
546
  test('isReportAvailable returns true for a known page description', async () => {
@@ -457,18 +561,19 @@ test('isReportAvailable returns false for an unknown page and URL', async () =>
457
561
  // TESTS FOR REMAINING BRANCH COVERAGE
458
562
 
459
563
  test('getJobNames creates missing job directories and returns empty arrays', async () => {
460
- const os = require('node:os');
564
+ const os = await import('node:os');
461
565
  const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-jobnames-'));
462
566
  const tmpDbDir = path.join(tmpRoot, 'db');
463
567
  const savedDbDir = process.env.DB_DIR;
464
568
  process.env.DB_DIR = tmpDbDir;
465
569
  try {
466
- const {getJobNames} = require('./util');
570
+ const {getJobNames} = await import('./util.ts');
467
571
  const result = await getJobNames();
468
- assert.equal(typeof result, 'object');
469
- assert.deepEqual(result.queue, []);
470
- assert.deepEqual(result.claimed, []);
471
- assert.deepEqual(result.failed, []);
572
+ assert.ok(typeof result === 'object' && result !== null);
573
+ const jobNames = result as Record<string, string[]>;
574
+ assert.deepEqual(jobNames.queue, []);
575
+ assert.deepEqual(jobNames.claimed, []);
576
+ assert.deepEqual(jobNames.failed, []);
472
577
  // Verify the directories were created.
473
578
  for (const category of ['queue', 'claimed', 'failed']) {
474
579
  const stat = await fs.stat(path.join(tmpDbDir, 'jobs', category));
@@ -481,8 +586,8 @@ test('getJobNames creates missing job directories and returns empty arrays', asy
481
586
  }
482
587
  });
483
588
 
484
- test('getJobNames returns an error when a job directory is a file, not a directory', async () => {
485
- const os = require('node:os');
589
+ test('getJobNames throws when a job directory is a file, not a directory', async () => {
590
+ const os = await import('node:os');
486
591
  const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'kilotest-jobnames-err-'));
487
592
  const tmpJobsDir = path.join(tmpRoot, 'db', 'jobs');
488
593
  await fs.mkdir(tmpJobsDir, {recursive: true});
@@ -491,10 +596,8 @@ test('getJobNames returns an error when a job directory is a file, not a directo
491
596
  const savedDbDir = process.env.DB_DIR;
492
597
  process.env.DB_DIR = path.join(tmpRoot, 'db');
493
598
  try {
494
- const {getJobNames} = require('./util');
495
- const result = await getJobNames();
496
- assert.equal(typeof result, 'string');
497
- assert.ok(result.startsWith('ERROR'));
599
+ const {getJobNames} = await import('./util.ts');
600
+ await assert.rejects(getJobNames(), /not readable/);
498
601
  }
499
602
  finally {
500
603
  process.env.DB_DIR = savedDbDir;
@@ -502,28 +605,46 @@ test('getJobNames returns an error when a job directory is a file, not a directo
502
605
  }
503
606
  });
504
607
 
505
- test('getObject returns an error for a file that is not valid JSON', async () => {
506
- const tmpFile = path.join(require('node:os').tmpdir(), 'kilotest-test-invalid.json');
507
- require('node:fs').writeFileSync(tmpFile, 'not json');
508
- const result = await getObject(tmpFile);
509
- assert.equal(typeof result, 'string');
510
- assert.ok(result.startsWith('ERROR'));
511
- require('node:fs').unlinkSync(tmpFile);
608
+ test('getObject throws for a file that is not valid JSON', async () => {
609
+ const tmpFile = path.join((await import('node:os')).tmpdir(), 'kilotest-test-invalid.json');
610
+ (await import('node:fs')).writeFileSync(tmpFile, 'not json');
611
+ try {
612
+ await assert.rejects(getObject(tmpFile), /not JSON/);
613
+ }
614
+ finally {
615
+ (await import('node:fs')).unlinkSync(tmpFile);
616
+ }
512
617
  });
513
618
 
514
- test('getRecs creates an empty recommendations file and returns an error when it is missing', async () => {
515
- const tmpDir = require('node:os').tmpdir() + '/kilotest-missing-recs-test';
516
- const fsSync = require('node:fs');
619
+ test('getTestRequests creates and returns an empty test-requests object when the file is missing', async () => {
620
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-missing-testrequests-test';
621
+ const fsSync = await import('node:fs');
517
622
  fsSync.mkdirSync(tmpDir + '/jobs', {recursive: true});
518
623
  const savedDbDir = process.env.DB_DIR;
519
624
  process.env.DB_DIR = tmpDir;
520
625
  try {
521
- const {getRecs} = require('./util');
522
- const result = await getRecs();
523
- assert.equal(typeof result, 'string');
524
- assert.ok(result.startsWith('ERROR'));
626
+ const {getTestRequests} = await import('./util.ts');
627
+ const result = await getTestRequests();
628
+ assert.deepEqual(result, {});
525
629
  // Verify the empty file was created.
526
- assert.ok(fsSync.existsSync(tmpDir + '/jobs/recs.json'));
630
+ assert.ok(fsSync.existsSync(tmpDir + '/jobs/testRequests.json'));
631
+ }
632
+ finally {
633
+ process.env.DB_DIR = savedDbDir;
634
+ fsSync.rmSync(tmpDir, {recursive: true});
635
+ }
636
+ });
637
+
638
+ test('getTestRequests throws when the test-requests file is not readable for a reason other than being missing', async () => {
639
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-testrequests-unreadable-test';
640
+ const fsSync = await import('node:fs');
641
+ // Create a directory where the test-requests file should be, causing EISDIR rather than ENOENT.
642
+ fsSync.mkdirSync(tmpDir + '/jobs/testRequests.json', {recursive: true});
643
+ const savedDbDir = process.env.DB_DIR;
644
+ process.env.DB_DIR = tmpDir;
645
+ try {
646
+ const {getTestRequests} = await import('./util.ts');
647
+ await assert.rejects(getTestRequests(), /not readable/);
527
648
  }
528
649
  finally {
529
650
  process.env.DB_DIR = savedDbDir;
@@ -531,27 +652,25 @@ test('getRecs creates an empty recommendations file and returns an error when it
531
652
  }
532
653
  });
533
654
 
534
- test('getRecs returns an error when the recommendations file is not JSON', async () => {
535
- const tmpDir = require('node:os').tmpdir() + '/kilotest-recs-test';
536
- require('node:fs').mkdirSync(tmpDir + '/jobs', {recursive: true});
537
- require('node:fs').writeFileSync(tmpDir + '/jobs/recs.json', 'not json');
655
+ test('getTestRequests throws when the test-requests file is not JSON', async () => {
656
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-testrequests-test';
657
+ (await import('node:fs')).mkdirSync(tmpDir + '/jobs', {recursive: true});
658
+ (await import('node:fs')).writeFileSync(tmpDir + '/jobs/testRequests.json', 'not json');
538
659
  const savedDbDir = process.env.DB_DIR;
539
660
  process.env.DB_DIR = tmpDir;
540
661
  try {
541
- const {getRecs} = require('./util');
542
- const result = await getRecs();
543
- assert.equal(typeof result, 'string');
544
- assert.ok(result.startsWith('ERROR'));
662
+ const {getTestRequests} = await import('./util.ts');
663
+ await assert.rejects(getTestRequests(), /not JSON/);
545
664
  }
546
665
  finally {
547
666
  process.env.DB_DIR = savedDbDir;
548
- require('node:fs').rmSync(tmpDir, {recursive: true});
667
+ (await import('node:fs')).rmSync(tmpDir, {recursive: true});
549
668
  }
550
669
  });
551
670
 
552
671
  test('getPOSTData resolves with parsed query for form-urlencoded requests', async () => {
553
- const {Readable} = require('node:stream');
554
- const {getPOSTData} = require('./util');
672
+ const {Readable} = await import('node:stream');
673
+ const {getPOSTData} = await import('./util.ts');
555
674
  const body = 'target=Page&why=Because';
556
675
  const req = Object.assign(new Readable({
557
676
  read() {
@@ -559,14 +678,14 @@ test('getPOSTData resolves with parsed query for form-urlencoded requests', asyn
559
678
  this.push(null);
560
679
  }
561
680
  }), {headers: {'content-type': 'application/x-www-form-urlencoded'}});
562
- const result = await getPOSTData(req);
681
+ const result: any = await getPOSTData(req as any);
563
682
  assert.equal(result.target, 'Page');
564
683
  assert.equal(result.why, 'Because');
565
684
  });
566
685
 
567
- test('isRecommendable returns "claimed" for a URL in a claimed job', async () => {
568
- const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
569
- const fsSync = require('node:fs');
686
+ test('getRequestability returns "claimed" for a URL in a claimed job', async () => {
687
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
688
+ const fsSync = await import('node:fs');
570
689
  fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
571
690
  fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
572
691
  fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
@@ -576,8 +695,8 @@ test('isRecommendable returns "claimed" for a URL in a claimed job', async () =>
576
695
  const savedDbDir = process.env.DB_DIR;
577
696
  process.env.DB_DIR = tmpDir;
578
697
  try {
579
- const {isRecommendable} = require('./util');
580
- const result = await isRecommendable('https://example.com/test');
698
+ const {getRequestability} = await import('./util.ts');
699
+ const result = await getRequestability('https://example.com/test');
581
700
  assert.equal(result, 'claimed');
582
701
  }
583
702
  finally {
@@ -586,9 +705,9 @@ test('isRecommendable returns "claimed" for a URL in a claimed job', async () =>
586
705
  }
587
706
  });
588
707
 
589
- test('isRecommendable returns "queued" for a URL in a queued job', async () => {
590
- const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
591
- const fsSync = require('node:fs');
708
+ test('getRequestability returns "queued" for a URL in a queued job', async () => {
709
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
710
+ const fsSync = await import('node:fs');
592
711
  fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
593
712
  fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
594
713
  fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
@@ -598,8 +717,8 @@ test('isRecommendable returns "queued" for a URL in a queued job', async () => {
598
717
  const savedDbDir = process.env.DB_DIR;
599
718
  process.env.DB_DIR = tmpDir;
600
719
  try {
601
- const {isRecommendable} = require('./util');
602
- const result = await isRecommendable('https://example.com/test');
720
+ const {getRequestability} = await import('./util.ts');
721
+ const result = await getRequestability('https://example.com/test');
603
722
  assert.equal(result, 'queued');
604
723
  }
605
724
  finally {
@@ -608,17 +727,17 @@ test('isRecommendable returns "queued" for a URL in a queued job', async () => {
608
727
  }
609
728
  });
610
729
 
611
- test('isRecommendable returns empty string for a URL with no matching jobs', async () => {
612
- const tmpDir = require('node:os').tmpdir() + '/kilotest-recommendable-test';
613
- const fsSync = require('node:fs');
730
+ test('getRequestability returns empty string for a URL with no matching jobs', async () => {
731
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-requestability-test';
732
+ const fsSync = await import('node:fs');
614
733
  fsSync.mkdirSync(tmpDir + '/jobs/claimed', {recursive: true});
615
734
  fsSync.mkdirSync(tmpDir + '/jobs/queue', {recursive: true});
616
735
  fsSync.mkdirSync(tmpDir + '/jobs/failed', {recursive: true});
617
736
  const savedDbDir = process.env.DB_DIR;
618
737
  process.env.DB_DIR = tmpDir;
619
738
  try {
620
- const {isRecommendable} = require('./util');
621
- const result = await isRecommendable('https://example.com/no-match');
739
+ const {getRequestability} = await import('./util.ts');
740
+ const result = await getRequestability('https://example.com/no-match');
622
741
  assert.equal(result, '');
623
742
  }
624
743
  finally {
@@ -633,22 +752,22 @@ test('isURL returns false for a malformed URL', () => {
633
752
 
634
753
  test('objectSort returns 0 for an unknown sort type', () => {
635
754
  const items = [{name: 'a'}, {name: 'b'}];
636
- const sorted = objectSort(items, 'name', 'unknownType');
755
+ const sorted = objectSort(items, 'name', 'unknownType' as any);
637
756
  assert.equal(sorted.length, 2);
638
757
  });
639
758
 
640
759
  test('getReport returns an error for an invalid report', async () => {
641
- const tmpDir = require('node:os').tmpdir() + '/kilotest-invalid-report-test';
642
- const fsSync = require('node:fs');
760
+ const tmpDir = (await import('node:os')).tmpdir() + '/kilotest-invalid-report-test';
761
+ const fsSync = await import('node:fs');
643
762
  fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
644
763
  fsSync.writeFileSync(tmpDir + '/reports/260101T0000-bad.json', JSON.stringify({notValid: true}));
645
764
  const savedDbDir = process.env.DB_DIR;
646
765
  process.env.DB_DIR = tmpDir;
647
766
  try {
648
- const {getReport} = require('./util');
649
- const result = await getReport('260101T0000', 'bad');
767
+ const {getReport} = await import('./util.ts');
768
+ const result = await getReport('260101T0000', 'bad') as any;
650
769
  assert.ok(result.error);
651
- assert.ok(result.error.includes('invalid'));
770
+ assert.ok(result.error.includes('not usable'));
652
771
  }
653
772
  finally {
654
773
  process.env.DB_DIR = savedDbDir;
@@ -657,11 +776,27 @@ test('getReport returns an error for an invalid report', async () => {
657
776
  });
658
777
 
659
778
  test('getReportData returns an error for a nonexistent report', async () => {
660
- const {getReportData} = require('./util');
661
- const result = await getReportData('990101T0000', 'xxx');
779
+ const {getReportData} = await import('./util.ts');
780
+ const result: any = await getReportData('990101T0000', 'xxx');
662
781
  assert.ok(result.error);
663
782
  });
664
783
 
784
+ test('getReportData falls back to the engine ID for an unknown prevented engine', async () => {
785
+ const {getReportData} = await import('./util.ts');
786
+ const prvJSON = await fs.readFile(path.join(reportsPath(), '260101T0006-prv.json'), 'utf8');
787
+ const report = JSON.parse(prvJSON);
788
+ report.jobData.preventions.unknownEngine = 'mystery failure';
789
+ const reportPath = path.join(reportsPath(), '260103T0000-unk.json');
790
+ await fs.writeFile(reportPath, JSON.stringify(report));
791
+ try {
792
+ const result: any = await getReportData('260103T0000', 'unk');
793
+ assert.ok(result.preventedEngineNames.includes('unknownEngine'));
794
+ }
795
+ finally {
796
+ await fs.unlink(reportPath);
797
+ }
798
+ });
799
+
665
800
  // TESTS FOR REMAINING BRANCH COVERAGE IN util.js
666
801
 
667
802
  test('getAgoDays returns null for an invalid Date object', () => {
@@ -685,15 +820,15 @@ test('getIssue returns an issue ID for a variable rule pattern match', () => {
685
820
  assert.equal(result, 'duplicateAttribute');
686
821
  });
687
822
 
688
- test('getTimeString returns null for an invalid time portion', () => {
689
- const {getDateTimeString} = require('./util');
823
+ test('getDateTimeString describes an invalid time stamp as unknown', async () => {
824
+ const {getDateTimeString} = await import('./util.ts');
690
825
  const result = getDateTimeString('999999T9999');
691
- assert.ok(result.includes('null'));
826
+ assert.equal(result, 'an unknown date at an unknown time');
692
827
  });
693
828
 
694
829
  test('getPOSTData resolves with parsed JSON for application/json requests', async () => {
695
- const {Readable} = require('node:stream');
696
- const {getPOSTData} = require('./util');
830
+ const {Readable} = await import('node:stream');
831
+ const {getPOSTData} = await import('./util.ts');
697
832
  const body = JSON.stringify({target: 'Page', why: 'Because'});
698
833
  const req = Object.assign(new Readable({
699
834
  read() {
@@ -701,13 +836,13 @@ test('getPOSTData resolves with parsed JSON for application/json requests', asyn
701
836
  this.push(null);
702
837
  }
703
838
  }), {headers: {'content-type': 'application/json'}});
704
- const result = await getPOSTData(req);
839
+ const result: any = await getPOSTData(req as any);
705
840
  assert.equal(result.target, 'Page');
706
841
  });
707
842
 
708
843
  test('getPOSTData resolves with parsed query for body-type form-urlencoded requests', async () => {
709
- const {Readable} = require('node:stream');
710
- const {getPOSTData} = require('./util');
844
+ const {Readable} = await import('node:stream');
845
+ const {getPOSTData} = await import('./util.ts');
711
846
  const body = 'target=Page&why=Because';
712
847
  const req = Object.assign(new Readable({
713
848
  read() {
@@ -715,131 +850,93 @@ test('getPOSTData resolves with parsed query for body-type form-urlencoded reque
715
850
  this.push(null);
716
851
  }
717
852
  }), {headers: {'body-type': 'application/x-www-form-urlencoded'}});
718
- const result = await getPOSTData(req);
853
+ const result: any = await getPOSTData(req as any);
719
854
  assert.equal(result.target, 'Page');
720
855
  assert.equal(result.why, 'Because');
721
856
  });
722
857
 
723
858
  test('getPOSTData resolves with null for an unknown content type', async () => {
724
- const {Readable} = require('node:stream');
725
- const {getPOSTData} = require('./util');
859
+ const {Readable} = await import('node:stream');
860
+ const {getPOSTData} = await import('./util.ts');
726
861
  const req = Object.assign(new Readable({
727
862
  read() {
728
863
  this.push(Buffer.from('data'));
729
864
  this.push(null);
730
865
  }
731
866
  }), {headers: {}});
732
- const result = await getPOSTData(req);
867
+ const result: any = await getPOSTData(req as any);
868
+ assert.equal(result, null);
869
+ });
870
+
871
+ test('getPOSTData resolves with null for a malformed JSON body', async () => {
872
+ const {Readable} = await import('node:stream');
873
+ const {getPOSTData} = await import('./util.ts');
874
+ const req = Object.assign(new Readable({
875
+ read() {
876
+ this.push(Buffer.from('{not valid json'));
877
+ this.push(null);
878
+ }
879
+ }), {headers: {'content-type': 'application/json'}});
880
+ const result: any = await getPOSTData(req as any);
733
881
  assert.equal(result, null);
734
882
  });
735
883
 
736
- test('getEngineNamesString falls back to the engine ID for an unknown engine', () => {
737
- const {getEngineNamesString} = require('./util');
884
+ test('getEngineNamesString falls back to the engine ID for an unknown engine', async () => {
885
+ const {getEngineNamesString} = await import('./util.ts');
738
886
  const result = getEngineNamesString(new Set(['unknownEngine']));
739
887
  assert.equal(result, 'unknownEngine');
740
888
  });
741
889
 
742
- test('getPathID returns the catalog pathID when catalogIndex is truthy', () => {
743
- const {getPathID} = require('./util');
744
- const catalog = {'0': {pathID: '/html/body/div'}};
890
+ test('getPathID returns the catalog pathID when catalogIndex is truthy', async () => {
891
+ const {getPathID} = await import('./util.ts');
892
+ const catalog = {'0': {tagName: 'div', pathID: '/html/body/div'}};
745
893
  assert.equal(getPathID(catalog, '0', '/fallback'), '/html/body/div');
746
894
  });
747
895
 
748
- test('getPathID returns the fallback pathID when catalogIndex is truthy but catalogItem has no pathID', () => {
749
- const {getPathID} = require('./util');
750
- const catalog = {'0': {tagName: 'div'}};
896
+ test('getPathID returns the fallback pathID when catalogIndex is truthy but catalogItem has no pathID', async () => {
897
+ const {getPathID} = await import('./util.ts');
898
+ const catalog = {'0': {tagName: 'div', pathID: ''}};
751
899
  assert.equal(getPathID(catalog, '0', '/fallback'), '/fallback');
752
900
  });
753
901
 
754
- test('getPathID returns /html when catalogIndex is truthy but catalogItem and pathID are both missing', () => {
755
- const {getPathID} = require('./util');
902
+ test('getPathID returns /html when catalogIndex is truthy but catalogItem and pathID are both missing', async () => {
903
+ const {getPathID} = await import('./util.ts');
756
904
  const catalog = {};
757
- assert.equal(getPathID(catalog, '0', null), '/html');
905
+ assert.equal(getPathID(catalog, '0', null as any), '/html');
758
906
  });
759
907
 
760
- test('getPathID returns /html when catalogIndex is falsy and pathID is null', () => {
761
- const {getPathID} = require('./util');
762
- assert.equal(getPathID({}, null, null), '/html');
908
+ test('getPathID returns /html when catalogIndex is falsy and pathID is null', async () => {
909
+ const {getPathID} = await import('./util.ts');
910
+ assert.equal(getPathID({}, null as any, null as any), '/html');
763
911
  });
764
912
 
765
- test('isValidReport returns false for a report with a test act using an unknown engine', () => {
766
- const {isValidReport} = require('./util');
913
+ test('isUsableReport returns false for a report with a test act using an unknown engine', async () => {
914
+ const {isUsableReport} = await import('./util.ts');
767
915
  const report = {
768
916
  target: {what: 'Test', url: 'https://example.com'},
769
917
  acts: [{type: 'test', which: 'unknownEngine'}],
770
918
  jobData: {endTime: '26-01-01T00:00'},
771
919
  catalog: {}
772
920
  };
773
- assert.equal(isValidReport(report), false);
921
+ assert.equal(isUsableReport(report), false);
774
922
  });
775
923
 
776
- test('isValidReport returns true for a report with a non-test act', () => {
777
- const {isValidReport} = require('./util');
924
+ test('isUsableReport returns true for a report with a non-test act', async () => {
925
+ const {isUsableReport} = await import('./util.ts');
778
926
  const report = {
779
927
  target: {what: 'Test', url: 'https://example.com'},
780
928
  acts: [{type: 'other'}],
781
929
  jobData: {endTime: '26-01-01T00:00'},
782
930
  catalog: {}
783
931
  };
784
- assert.equal(isValidReport(report), true);
785
- });
786
-
787
- test('annotateReport handles a test act with no standardResult instances', async () => {
788
- const tmpDir = require('node:os').tmpdir() + '/kilotest-no-instances-test';
789
- const fsSync = require('node:fs');
790
- fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
791
- const report = {
792
- target: {what: 'Test', url: 'https://example.com'},
793
- acts: [
794
- {type: 'test', which: 'axe', result: {standardResult: {instances: []}}},
795
- {type: 'other'}
796
- ],
797
- jobData: {endTime: '26-01-01T00:00'},
798
- catalog: {}
799
- };
800
- fsSync.writeFileSync(tmpDir + '/reports/260101T0000-ni.json', JSON.stringify(report));
801
- const savedDbDir = process.env.DB_DIR;
802
- process.env.DB_DIR = tmpDir;
803
- try {
804
- const result = await annotateReport('260101T0000', 'ni');
805
- assert.equal(result, '');
806
- }
807
- finally {
808
- process.env.DB_DIR = savedDbDir;
809
- fsSync.rmSync(tmpDir, {recursive: true});
810
- }
811
- });
812
-
813
- test('annotateReport handles a test act with no standardResult', async () => {
814
- const tmpDir = require('node:os').tmpdir() + '/kilotest-no-standard-test';
815
- const fsSync = require('node:fs');
816
- fsSync.mkdirSync(tmpDir + '/reports', {recursive: true});
817
- const report = {
818
- target: {what: 'Test', url: 'https://example.com'},
819
- acts: [
820
- {type: 'test', which: 'axe', result: {}}
821
- ],
822
- jobData: {endTime: '26-01-01T00:00'},
823
- catalog: {}
824
- };
825
- fsSync.writeFileSync(tmpDir + '/reports/260101T0000-ns.json', JSON.stringify(report));
826
- const savedDbDir = process.env.DB_DIR;
827
- process.env.DB_DIR = tmpDir;
828
- try {
829
- const result = await annotateReport('260101T0000', 'ns');
830
- assert.equal(result, '');
831
- }
832
- finally {
833
- process.env.DB_DIR = savedDbDir;
834
- fsSync.rmSync(tmpDir, {recursive: true});
835
- }
932
+ assert.equal(isUsableReport(report), true);
836
933
  });
837
934
 
838
935
  // UNIT TESTS FOR PREVIOUSLY UNTESTED EXPORTED FUNCTIONS
839
936
 
840
937
  test('createLock returns a function that runs tasks sequentially', async () => {
841
938
  const lock = createLock();
842
- const order = [];
939
+ const order: string[] = [];
843
940
  const p1 = lock(async () => {
844
941
  order.push('start 1');
845
942
  await new Promise(r => setTimeout(r, 10));
@@ -876,34 +973,38 @@ test('getTimeStamp returns an 11-character stamp from a Date', () => {
876
973
  assert.equal(stamp.slice(7), '1430');
877
974
  });
878
975
 
879
- test('getEngineList returns a sorted +-delimited list of engine names', () => {
880
- const result = getEngineList(new Set(['axe', 'wave', 'nuVal']));
976
+ test('getEngineNamesString returns a sorted +-delimited list of engine names', () => {
977
+ const result = getEngineNamesString(new Set(['axe', 'wave', 'nuVal']));
881
978
  const names = result.split(' + ');
882
979
  assert.ok(names.length === 3);
883
980
  assert.ok(names.includes('WAVE'));
884
981
  });
885
982
 
886
- test('recsLock is a function (the lock returned by createLock)', () => {
887
- assert.equal(typeof recsLock, 'function');
983
+ test('getEngineNamesString falls back to the ID for an unknown engine', () => {
984
+ assert.equal(getEngineNamesString(new Set(['unknownEngine'])), 'unknownEngine');
985
+ });
986
+
987
+ test('testRequestsLock is a function (the lock returned by createLock)', () => {
988
+ assert.equal(typeof testRequestsLock, 'function');
888
989
  });
889
990
 
890
- test('updateRecs adds a recommendation and returns success', async () => {
891
- await fs.writeFile(recsPath(), '{}\n');
892
- const result = await updateRecs('Test Page', 'https://example.com/test', 'because');
991
+ test('addTestRequest adds a request and returns success', async () => {
992
+ await fs.writeFile(testRequestsPath(), '{}\n');
993
+ const result = await addTestRequest('Test Page', 'https://example.com/test', 'because');
893
994
  assert.equal(result.error, undefined);
894
- const recs = JSON.parse(await fs.readFile(recsPath(), 'utf8'));
895
- assert.ok(recs['https://example.com/test']);
896
- assert.equal(recs['https://example.com/test'].length, 1);
897
- assert.equal(recs['https://example.com/test'][0].what, 'Test Page');
898
- await fs.writeFile(recsPath(), '{}\n');
995
+ const testRequests = JSON.parse(await fs.readFile(testRequestsPath(), 'utf8'));
996
+ assert.ok(testRequests['https://example.com/test']);
997
+ assert.equal(testRequests['https://example.com/test'].length, 1);
998
+ assert.equal(testRequests['https://example.com/test'][0].description, 'Test Page');
999
+ await fs.writeFile(testRequestsPath(), '{}\n');
899
1000
  });
900
1001
 
901
- test('updateRecs returns a duplicate error for a repeated recommendation', async () => {
902
- await fs.writeFile(recsPath(), '{}\n');
903
- await updateRecs('Test Page', 'https://example.com/test', 'because');
904
- const result = await updateRecs('Test Page', 'https://example.com/test', 'another reason');
1002
+ test('addTestRequest returns a duplicate error for a repeated request', async () => {
1003
+ await fs.writeFile(testRequestsPath(), '{}\n');
1004
+ await addTestRequest('Test Page', 'https://example.com/test', 'because');
1005
+ const result = await addTestRequest('Test Page', 'https://example.com/test', 'another reason');
905
1006
  assert.equal(result.error, 'duplicate');
906
- await fs.writeFile(recsPath(), '{}\n');
1007
+ await fs.writeFile(testRequestsPath(), '{}\n');
907
1008
  });
908
1009
 
909
1010
  test('getReportPath returns the path of a report file', () => {
@@ -913,6 +1014,7 @@ test('getReportPath returns the path of a report file', () => {
913
1014
 
914
1015
  test('getReportStats returns reportTime and reportSize for a valid report', async () => {
915
1016
  const stats = await getReportStats('260101T0000', 'mix');
1017
+ assert.ok(stats);
916
1018
  assert.ok(stats.reportTime instanceof Date);
917
1019
  assert.equal(typeof stats.reportSize, 'number');
918
1020
  assert.ok(stats.reportSize > 0);
@@ -923,27 +1025,18 @@ test('getReportStats returns null for a nonexistent report', async () => {
923
1025
  assert.equal(stats, null);
924
1026
  });
925
1027
 
926
- test('isHidden returns false for a non-hidden report', async () => {
927
- const result = await isHidden('260101T0000', 'mix');
928
- assert.equal(result, false);
929
- });
930
-
931
- test('isHidden returns true for a hidden report', async () => {
932
- const result = await isHidden('260101T0007', 'hid');
933
- assert.equal(result, true);
934
- });
935
1028
 
936
1029
  test('getReportExtract returns an extract for a valid report', async () => {
937
- const extract = await getReportExtract('260101T0000', 'mix');
1030
+ const extract = await getReportExtract('260101T0000', 'mix') as any;
938
1031
  assert.equal(extract.timeStamp, '260101T0000');
939
1032
  assert.equal(extract.jobID, 'mix');
940
- assert.equal(extract.what, 'Mixed Outcomes Page');
1033
+ assert.equal(extract.description, 'Mixed Outcomes Page');
941
1034
  assert.equal(extract.url, 'https://example.com/mixed');
942
1035
  assert.ok(extract.reportTime);
943
1036
  });
944
1037
 
945
1038
  test('getReportExtract returns an error for a nonexistent report', async () => {
946
- const extract = await getReportExtract('999999T9999', 'xxx');
1039
+ const extract = await getReportExtract('999999T9999', 'xxx') as any;
947
1040
  assert.ok(extract.error);
948
1041
  });
949
1042
 
@@ -955,11 +1048,11 @@ test('getReportExtracts returns extracts of all available reports', async () =>
955
1048
  assert.ok(ids.includes('260101T0001-ct'));
956
1049
  });
957
1050
 
958
- test('getLatestReportExtracts returns only the latest report for each page', async () => {
959
- const latest = await getLatestReportExtracts();
960
- const mixReports = latest.filter(e => e.what === 'Mixed Outcomes Page');
1051
+ test('getReportExtracts with onlyLatest returns only the latest report for each page', async () => {
1052
+ const latest = await getReportExtracts(true);
1053
+ const mixReports = latest.filter(e => e.description === 'Mixed Outcomes Page');
961
1054
  assert.equal(mixReports.length, 1);
962
- assert.equal(mixReports[0].timeStamp, '260202T0000');
1055
+ assert.equal(mixReports[0]!.timeStamp, '260202T0000');
963
1056
  });
964
1057
 
965
1058
  test('getMultiReportWhats returns descriptions that have multiple reports', async () => {