@jrpool/kilotest 46.1.2 → 48.0.0

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 (153) 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.ts +144 -0
  21. package/api/requestRetest.ts +119 -0
  22. package/api/requestTest.test.ts +153 -0
  23. package/api/requestTest.ts +123 -0
  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} +35 -14
  28. package/api/{util.js → util.ts} +54 -51
  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 +360 -0
  35. package/docs/SERVICE.md +170 -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} +397 -152
  48. package/{index.js → index.ts} +386 -285
  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.ts +1129 -0
  69. package/util.ts +1017 -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} +74 -58
  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 +71 -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} +57 -39
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +62 -0
  105. package/web/requestRetest/requestRetest.test.ts +130 -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.ts +52 -0
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
  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} +18 -22
  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/requestRetest.js +0 -96
  128. package/api/requestRetest.test.js +0 -80
  129. package/api/requestTest.js +0 -106
  130. package/api/requestTest.test.js +0 -92
  131. package/api/version.js +0 -8
  132. package/indexFallback.test.js +0 -33
  133. package/indexWorkers.test.js +0 -79
  134. package/jsconfig.json +0 -8
  135. package/pm2.config.js +0 -18
  136. package/smokeTest.js +0 -128
  137. package/util.js +0 -744
  138. package/util.test.js +0 -968
  139. package/web/enqueue/index.js +0 -67
  140. package/web/enqueueForm/index.js +0 -49
  141. package/web/manage/index.js +0 -22
  142. package/web/pruneReportsForm/index.js +0 -123
  143. package/web/reannotate/index.js +0 -58
  144. package/web/renewWCAGForm/index.js +0 -22
  145. package/web/requestRetest/index.js +0 -31
  146. package/web/requestRetest/requestRetest.test.js +0 -119
  147. package/web/requestTest/index.js +0 -24
  148. package/web/requestTestForm/index.js +0 -21
  149. package/web/rewindReportsForm/index.js +0 -121
  150. package/web/tutorial/index.js +0 -63
  151. package/web/tutorial/tutorial.test.js +0 -97
  152. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  153. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,46 +1,49 @@
1
1
  /*
2
- listViolators.test.js
3
- UI tests for web/listViolators/index.js using the fixture corpus.
2
+ listViolators.test.ts
3
+ UI tests for web/listViolators/index.ts using the fixture corpus.
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 {parse} = require('node-html-parser');
8
+ import {test, before, after, mock} 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 {parse} from 'node-html-parser';
13
13
 
14
- // Monkey-patch util functions before requiring index, so that index.js
15
- // destructures the patched versions.
16
- const util = require('../../util');
17
- const realGetReport = util.getReport;
18
- const realGetPageDataStrings = util.getPageDataStrings;
14
+ // Mock the util functions called by index.ts before requiring it, so that
15
+ // it imports the mocked versions. Other exports delegate to the real module.
16
+ import * as realUtil from '../../util.ts';
19
17
  let getReportCallCount = 0;
20
18
  let failGetReportOnCall = -1;
21
- let pageDataStringsOverride = null;
22
- util.getReport = async (...args) => {
23
- getReportCallCount++;
24
- if (getReportCallCount === failGetReportOnCall) {
25
- return {error: 'Report is invalid (test override)'};
26
- }
27
- return realGetReport(...args);
28
- };
29
- util.getPageDataStrings = async (...args) => {
30
- if (pageDataStringsOverride !== null) {
31
- return pageDataStringsOverride;
19
+ let pageDataStringsOverride: any = null;
20
+ mock.module('../../util.ts', {
21
+ exports: {
22
+ ...realUtil,
23
+ getReport: async (...args: any[]) => {
24
+ getReportCallCount++;
25
+ if (getReportCallCount === failGetReportOnCall) {
26
+ return {error: 'Report is invalid (test override)'};
27
+ }
28
+ return (realUtil.getReport as any)(...args);
29
+ },
30
+ getPageDataStrings: async (...args: any[]) => {
31
+ if (pageDataStringsOverride !== null) {
32
+ return pageDataStringsOverride;
33
+ }
34
+ return (realUtil.getPageDataStrings as any)(...args);
35
+ }
32
36
  }
33
- return realGetPageDataStrings(...args);
34
- };
37
+ } as any);
35
38
 
36
- const {answer} = require('./index');
39
+ const {answer} = await import('./index.ts') as any;
37
40
 
38
41
  // SETUP AND TEARDOWN
39
42
 
40
43
  const savedDBDir = process.env.DB_DIR;
41
44
 
42
- before(() => {
43
- process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
45
+ before(async () => {
46
+ process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
44
47
  });
45
48
 
46
49
  after(() => {
@@ -74,11 +77,14 @@ test('listViolators includes links to listDiagnoses for violators', async () =>
74
77
  assert.ok(diagnosisLinks.length > 0);
75
78
  });
76
79
 
77
- test('listViolators returns an error status for a hidden report', async () => {
78
- const result = await answer('linkNoText/260101T0007/hid');
79
- assert.equal(result.status, 'error');
80
+ test('listViolators excludes cantTell instances from the violator count', async () => {
81
+ const result = await answer('focusIndicationBad/260101T0000/mix');
82
+ assert.equal(result.status, 'ok');
83
+ assert.ok(result.answerPage.includes('0 violators were'));
84
+ assert.ok(!result.answerPage.includes('<h3>Element'));
80
85
  });
81
86
 
87
+
82
88
  test('listViolators returns an error status for a nonexistent report', async () => {
83
89
  const result = await answer('linkNoText/999999T9999/xyz');
84
90
  assert.equal(result.status, 'error');
@@ -103,7 +109,7 @@ test('listViolators returns an error when getReport fails after getPageDataStrin
103
109
  });
104
110
 
105
111
  test('listViolators includes take-me-there links for text-linkable violators', async () => {
106
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
112
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
107
113
  const reportsDir = path.join(dbDir, 'reports');
108
114
  const reportPath = path.join(reportsDir, '260101T0003-tlk.json');
109
115
  try {
@@ -184,7 +190,7 @@ test('listViolators handles instances with missing catalogIndex and acts with no
184
190
  // - An instance with no catalogIndex (covers || '0' on line 85)
185
191
  // - A catalog entry with no tagName (covers ?? pathID fallback on lines 87-88)
186
192
  // - Multiple violators (covers plural count on line 101)
187
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
193
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
188
194
  const reportPath = path.join(dbDir, 'reports', '260101T0004-nvi.json');
189
195
  const report = {
190
196
  id: '260101T0004-nvi',
@@ -257,9 +263,10 @@ test('listViolators handles instances with missing catalogIndex and acts with no
257
263
 
258
264
  test('listViolators uses HTML fallback when catalog has no tagName and pathID is missing', async () => {
259
265
  // Create a report where an instance has no pathID and the catalog
260
- // entry has no tagName, so the ?? 'HTML' fallback is hit.
261
- // Also uses a catalogIndex not in the catalog to cover || {} on line 144.
262
- const dbDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
266
+ // entry has no tagName, so the ?? 'HTML' tagName fallback is hit.
267
+ // Also uses a catalogIndex not in the catalog, so catalogItem is undefined
268
+ // and catalogItem?.textLinkable is falsy (no take-me-there link is added).
269
+ const dbDir = (await import('../../test/dbFixture.ts')).fixtureDBDir;
263
270
  const reportPath = path.join(dbDir, 'reports', '260101T0005-htm.json');
264
271
  const report = {
265
272
  id: '260101T0005-htm',
@@ -0,0 +1,22 @@
1
+ /*
2
+ index.ts
3
+ Lists the actions that managers can take.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+
11
+ // FUNCTIONS
12
+
13
+ // Returns the answer page.
14
+ export const answer = async (): Promise<{status: string; answerPage: string}> => {
15
+ // Get the answer page.
16
+ const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
17
+ // Return it.
18
+ return {
19
+ status: 'ok',
20
+ answerPage
21
+ };
22
+ };
@@ -0,0 +1,18 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for deleting superseded reports.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {reportDeletionForm} from '../reportDeletion.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ // Returns a form for deleting non-latest reports.
13
+ export const answer = async (_: any, search: string) => reportDeletionForm(import.meta.dirname, search, {
14
+ intro: 'Choose the superseded reports to delete.',
15
+ emptyIntro: 'Each target has only 1 report, so there are no superseded reports to delete.',
16
+ failurePrefix: 'Deleting superseded reports failed',
17
+ isDeletable: (specs, index) => specs[index + 1]?.url === specs[index]?.url
18
+ });
@@ -1,23 +1,23 @@
1
1
  /*
2
- pruneReportsForm.test.js
3
- Unit tests for web/pruneReportsForm/index.js.
2
+ pruneReportsForm.test.ts
3
+ Unit tests for web/pruneReportsForm/index.ts.
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('fs/promises');
12
- const {parse} = require('node-html-parser');
13
- const {answer} = require('./index');
14
- const {reportsPath} = require('../../util');
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 'fs/promises';
12
+ import {parse} from 'node-html-parser';
13
+ import {answer} from './index.ts';
14
+ import {reportsPath} from '../../util.ts';
15
15
 
16
16
  // SETUP AND TEARDOWN
17
17
 
18
18
  const savedAuthCode = process.env.AUTH_CODE;
19
19
  const savedDBDir = process.env.DB_DIR;
20
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
21
21
 
22
22
  before(() => {
23
23
  process.env.AUTH_CODE = 'test-auth-code';
@@ -52,7 +52,7 @@ test('pruneReportsForm returns an error for an invalid auth code', async () => {
52
52
  test('pruneReportsForm returns an error when deleting a nonexistent report', async () => {
53
53
  const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
54
54
  assert.equal(result.status, 'error');
55
- assert.ok(result.message.includes('Deleting superseded reports'));
55
+ assert.ok(result.message?.includes('Deleting superseded reports'));
56
56
  });
57
57
 
58
58
  test('pruneReportsForm deletes a superseded report with valid auth code', async () => {
@@ -0,0 +1,71 @@
1
+ /*
2
+ index.ts
3
+ Reannotates all available latest reports.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {
9
+ annotateReportObject,
10
+ getJSON,
11
+ getReport,
12
+ getReportPath,
13
+ isReportError,
14
+ readdirOrCreate,
15
+ reportsPath
16
+ } from '../../util.ts';
17
+ import fs from 'node:fs/promises';
18
+ import path from 'node:path';
19
+
20
+ // FUNCTIONS
21
+
22
+ // Implements a reannotation order and returns an acknowledgement page.
23
+ export const answer = async (authCode: string) => {
24
+ // If the authorization code is valid:
25
+ if (authCode === process.env.AUTH_CODE) {
26
+ // Get the names of the stored report files.
27
+ const reportFileNames = (await readdirOrCreate(reportsPath(), 'Reports directory'))
28
+ .filter(fileName => fileName.endsWith('.json'));
29
+ // If any exist:
30
+ if (reportFileNames.length) {
31
+ // For each report file:
32
+ for (const reportFileName of reportFileNames) {
33
+ const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-') as [string, string];
34
+ // Get the report.
35
+ const report = await getReport(timeStamp, jobID);
36
+ // If this failed:
37
+ if (isReportError(report)) {
38
+ // Return an error page.
39
+ return {
40
+ status: 'error',
41
+ message: report.error
42
+ };
43
+ }
44
+ // Otherwise, i.e. if it succeeded, reannotate it in place.
45
+ await annotateReportObject(report);
46
+ // Save the reannotated report.
47
+ await fs.writeFile(getReportPath(timeStamp, jobID), getJSON(report));
48
+ }
49
+ }
50
+ // Otherwise, i.e. if it failed:
51
+ else {
52
+ // Return an error page.
53
+ return {
54
+ status: 'error',
55
+ message: 'Got data on no available reports'
56
+ };
57
+ }
58
+ // If every annotation succeeded, get the answer page.
59
+ const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
60
+ // Return it.
61
+ return {
62
+ status: 'ok',
63
+ answerPage
64
+ };
65
+ }
66
+ // Otherwise, i.e. if the authorization code is invalid, return an error page.
67
+ return {
68
+ status: 'error',
69
+ message: 'Invalid authorization code'
70
+ };
71
+ };
@@ -1,20 +1,20 @@
1
1
  /*
2
- reannotate.test.js
3
- Unit tests for web/reannotate/index.js, covering the reannotation logic branches.
2
+ reannotate.test.ts
3
+ Unit tests for web/reannotate/index.ts, covering the reannotation logic branches.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after, beforeEach} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const fs = require('node:fs/promises');
11
- const os = require('node:os');
12
- const path = require('node:path');
13
- const {parse} = require('node-html-parser');
8
+ import {test, before, after, beforeEach} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs/promises';
11
+ import os from 'node:os';
12
+ import path from 'node:path';
13
+ import {parse} from 'node-html-parser';
14
14
 
15
15
  // CONSTANTS
16
16
 
17
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
18
18
  const reportsDir = path.join(fixtureDBDir, 'reports');
19
19
 
20
20
  // SETUP AND TEARDOWN
@@ -39,7 +39,7 @@ after(async () => {
39
39
  // Backs up all report files and returns a map of fileName to content.
40
40
  const backupReports = async () => {
41
41
  const reportFiles = await fs.readdir(reportsDir);
42
- const backups = {};
42
+ const backups: Record<string, string> = {};
43
43
  for (const file of reportFiles) {
44
44
  backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
45
45
  }
@@ -47,7 +47,7 @@ const backupReports = async () => {
47
47
  };
48
48
 
49
49
  // Restores all report files from a backup map.
50
- const restoreReports = async backups => {
50
+ const restoreReports = async (backups: Record<string, string>) => {
51
51
  for (const [file, content] of Object.entries(backups)) {
52
52
  await fs.writeFile(path.join(reportsDir, file), content);
53
53
  }
@@ -56,7 +56,7 @@ const restoreReports = async backups => {
56
56
  // TESTS
57
57
 
58
58
  test('answer returns an error for an invalid authorization code', async () => {
59
- const {answer} = require('./index');
59
+ const {answer} = await import('./index.ts');
60
60
  const result = await answer('wrong-code');
61
61
  assert.equal(result.status, 'error');
62
62
  assert.equal(result.message, 'Invalid authorization code');
@@ -70,8 +70,7 @@ test('answer returns an error when no reports are available', async () => {
70
70
  process.env.DB_DIR = tempDir;
71
71
  try {
72
72
  // Re-require the module so it picks up the new DB_DIR.
73
- delete require.cache[require.resolve('./index')];
74
- const {answer} = require('./index');
73
+ const {answer} = await import('./index.ts');
75
74
  const result = await answer('test-auth-code');
76
75
  assert.equal(result.status, 'error');
77
76
  assert.equal(result.message, 'Got data on no available reports');
@@ -88,7 +87,7 @@ test('answer returns an error when no reports are available', async () => {
88
87
  });
89
88
 
90
89
  test('answer returns an error when a report fails annotation', {timeout: 2000}, async () => {
91
- // Back up all fixture reports, because annotateReport modifies good reports
90
+ // Back up all fixture reports, because reannotation modifies good reports
92
91
  // in place before it reaches the bad one and returns an error.
93
92
  const backups = await backupReports();
94
93
  // Create a temporary report file that passes getReportExtract but fails
@@ -104,11 +103,10 @@ test('answer returns an error when a report fails annotation', {timeout: 2000},
104
103
  };
105
104
  await fs.writeFile(badReportPath, JSON.stringify(badReport));
106
105
  try {
107
- delete require.cache[require.resolve('./index')];
108
- const {answer} = require('./index');
106
+ const {answer} = await import('./index.ts');
109
107
  const result = await answer('test-auth-code');
110
108
  assert.equal(result.status, 'error');
111
- assert.ok(result.message.includes('invalid'), `Expected message about invalid report, got: ${result.message}`);
109
+ assert.ok(result.message?.includes('not usable'), `Expected message about unusable report, got: ${result.message}`);
112
110
  }
113
111
  finally {
114
112
  try {
@@ -117,17 +115,16 @@ test('answer returns an error when a report fails annotation', {timeout: 2000},
117
115
  catch {
118
116
  // Ignore cleanup errors for the temporary bad report.
119
117
  }
120
- // Restore all fixture reports modified by annotateReport.
118
+ // Restore all fixture reports modified by reannotation.
121
119
  await restoreReports(backups);
122
120
  }
123
121
  });
124
122
 
125
123
  test('answer returns ok with an answer page when all reports annotate successfully', {timeout: 2000}, async () => {
126
- // Back up all fixture reports, because annotateReport modifies them in place.
124
+ // Back up all fixture reports, because reannotation modifies them in place.
127
125
  const backups = await backupReports();
128
126
  try {
129
- delete require.cache[require.resolve('./index')];
130
- const {answer} = require('./index');
127
+ const {answer} = await import('./index.ts');
131
128
  const result = await answer('test-auth-code');
132
129
  assert.equal(result.status, 'ok');
133
130
  assert.ok(result.answerPage, 'answerPage should be present');
@@ -1,21 +1,21 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Serves a form to reannotate reports.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getIssue, getReport, getReportExtracts} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
8
+ import {
9
+ getIssue, getReport, getReportExtracts, getTestActInstances, isReportError, populateTemplate
10
+ } from '../../util.ts';
11
11
 
12
12
  // FUNCTIONS
13
13
 
14
14
  // Adds parameters to a query for the answer page.
15
- const populateQuery = async query => {
15
+ const populateQuery = async (query: Record<string, any>) => {
16
16
  // Initialize data on rule classification.
17
- const reClassified = {};
18
- const stillUnclassified = {};
17
+ const reClassified: Record<string, Record<string, Set<string>>> = {};
18
+ const stillUnclassified: Record<string, Record<string, Set<string>>> = {};
19
19
  // Get an extract of all available reports.
20
20
  const reportExtracts = await getReportExtracts();
21
21
  // For each report:
@@ -23,50 +23,45 @@ const populateQuery = async query => {
23
23
  const {jobID, timeStamp} = reportExtract;
24
24
  // Get it.
25
25
  const report = await getReport(timeStamp, jobID);
26
- const {acts = [], error, id} = report;
27
26
  // If this failed:
28
- if (error) {
27
+ if (isReportError(report)) {
29
28
  // Populate the query with the reason.
30
- query.error = error;
29
+ query.error = report.error;
31
30
  // Stop populating the query.
32
31
  return;
33
32
  }
34
- // Otherwise, i.e. if it succeeded, for each act in the report:
35
- acts.forEach(act => {
36
- const {result, type, which} = act;
37
- // If it is a test act with standard instances:
38
- if (type === 'test' && result?.standardResult?.instances?.length) {
39
- // For each standard instance:
40
- result.standardResult.instances.forEach(instance => {
41
- const {ruleID} = instance;
42
- // Get the issue ID of the rule, or null if none.
43
- const issueID = getIssue(which, ruleID);
44
- // If the issue ID of the instance differs from that of the rule:
45
- if ((instance.issueID || null) !== issueID) {
46
- // Add the rule and the report to the rules with changed issue IDs.
47
- reClassified[which] ??= {};
48
- reClassified[which][ruleID] ??= new Set();
49
- reClassified[which][ruleID].add(id);
50
- }
51
- // Otherwise, if the instance and the rule both have no issue ID:
52
- else if (!issueID){
53
- // Add the rule and the report to the rules that are still unclassified.
54
- stillUnclassified[which] ??= {};
55
- stillUnclassified[which][ruleID] ??= new Set();
56
- stillUnclassified[which][ruleID].add(id);
57
- }
58
- });
33
+ const id = report.id as string;
34
+ // Otherwise, i.e. if it succeeded, for each standard instance of each test act
35
+ // (all outcomes, including cantTell — classification maps rules to issues regardless of outcome):
36
+ getTestActInstances(report).forEach(({act, instance}) => {
37
+ const {ruleID} = instance;
38
+ const which = act.which!;
39
+ // Get the issue ID of the rule, or null if none.
40
+ const issueID = getIssue(which, ruleID);
41
+ // If the issue ID of the instance differs from that of the rule:
42
+ if ((instance.issueID || null) !== issueID) {
43
+ // Add the rule and the report to the rules with changed issue IDs.
44
+ reClassified[which] ??= {};
45
+ reClassified[which][ruleID] ??= new Set();
46
+ reClassified[which][ruleID].add(id);
47
+ }
48
+ // Otherwise, if the instance and the rule both have no issue ID:
49
+ else if (!issueID){
50
+ // Add the rule and the report to the rules that are still unclassified.
51
+ stillUnclassified[which] ??= {};
52
+ stillUnclassified[which][ruleID] ??= new Set();
53
+ stillUnclassified[which][ruleID].add(id);
59
54
  }
60
55
  });
61
56
  };
62
- const stillUnclassifiedLines = [];
63
- const reClassifiedLines = [];
57
+ const stillUnclassifiedLines: string[] = [];
58
+ const reClassifiedLines: string[] = [];
64
59
  const margin = ' '.repeat(6);
65
60
  // For each rule engine reporting any violations of still unclassified rules:
66
61
  Object.keys(stillUnclassified).forEach(engineID => {
67
62
  // For each such rule:
68
- Object.keys(stillUnclassified[engineID]).forEach(ruleID => {
69
- const reportIDs = Array.from(stillUnclassified[engineID][ruleID]);
63
+ Object.keys(stillUnclassified[engineID]!).forEach(ruleID => {
64
+ const reportIDs = Array.from(stillUnclassified[engineID]![ruleID]!);
70
65
  // Add a line to the lines on the rule.
71
66
  stillUnclassifiedLines.push(
72
67
  `${margin}<li>${engineID}: ${ruleID} (${reportIDs.join(', ')})</li>`
@@ -78,8 +73,8 @@ const populateQuery = async query => {
78
73
  // For each rule engine reporting any discrepancies in rule classification:
79
74
  Object.keys(reClassified).forEach(engineID => {
80
75
  // For each such rule:
81
- Object.keys(reClassified[engineID]).forEach(ruleID => {
82
- const reportIDs = Array.from(reClassified[engineID][ruleID]);
76
+ Object.keys(reClassified[engineID]!).forEach(ruleID => {
77
+ const reportIDs = Array.from(reClassified[engineID]![ruleID]!);
83
78
  // Add a line to the lines on the rule.
84
79
  reClassifiedLines.push(
85
80
  `${margin}<li>${engineID}: ${ruleID} (${reportIDs.join(', ')})</li>`
@@ -88,7 +83,7 @@ const populateQuery = async query => {
88
83
  });
89
84
  // Add the lines to the query.
90
85
  query.reClassified = reClassifiedLines.join('\n');
91
- const formLines = [];
86
+ const formLines: string[] = [];
92
87
  if (reClassifiedLines.length) {
93
88
  query.how = 'Each <q>reclassified</q> rule indicates that report annotations are out of date. To update them, submit your authorization code.';
94
89
  }
@@ -104,8 +99,8 @@ const populateQuery = async query => {
104
99
  query.reannotateForm = formLines.join('\n');
105
100
  };
106
101
  // Returns a page disclosing newly classified rules and a form to reannotate reports.
107
- exports.answer = async () => {
108
- const query = {};
102
+ export const answer = async () => {
103
+ const query: Record<string, any> = {};
109
104
  // Create a query to replace placeholders.
110
105
  await populateQuery(query);
111
106
  // If the query reports an error:
@@ -116,12 +111,8 @@ exports.answer = async () => {
116
111
  message: query.error
117
112
  };
118
113
  }
119
- // Otherwise, i.e. if the query does not report an error, get the template.
120
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
121
- // Replace its placeholders.
122
- Object.keys(query).forEach(param => {
123
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
124
- });
114
+ // Otherwise, i.e. if the query does not report an error, get the populated template.
115
+ const answerPage = await populateTemplate(import.meta.dirname, query);
125
116
  // Return the populated page.
126
117
  return {
127
118
  status: 'ok',
@@ -1,21 +1,21 @@
1
1
  /*
2
- reannotateForm.test.js
3
- Unit tests for web/reannotateForm/index.js, covering error, no-reclassified,
2
+ reannotateForm.test.ts
3
+ Unit tests for web/reannotateForm/index.ts, covering error, no-reclassified,
4
4
  and success branches of answer and populateQuery.
5
5
  */
6
6
 
7
7
  // IMPORTS
8
8
 
9
- const {test, before, after, beforeEach} = require('node:test');
10
- const assert = require('node:assert/strict');
11
- const fs = require('node:fs/promises');
12
- const os = require('node:os');
13
- const path = require('node:path');
14
- const {parse} = require('node-html-parser');
9
+ import {test, before, after, beforeEach} from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import fs from 'node:fs/promises';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+ import {parse} from 'node-html-parser';
15
15
 
16
16
  // CONSTANTS
17
17
 
18
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
18
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
19
19
  const reportsDir = path.join(fixtureDBDir, 'reports');
20
20
  const emptyReportName = '260101T0005-emp.json';
21
21
 
@@ -41,8 +41,7 @@ after(async () => {
41
41
  test('answer returns ok with reclassified rules listed in the page', async () => {
42
42
  // The fixture reports have instances whose issueIDs differ from the
43
43
  // current rule specs, so reclassified rules should be listed.
44
- delete require.cache[require.resolve('./index')];
45
- const {answer} = require('./index');
44
+ const {answer} = await import('./index.ts');
46
45
  const result = await answer();
47
46
  assert.equal(result.status, 'ok');
48
47
  assert.ok(result.answerPage, 'answerPage should be present');
@@ -75,8 +74,7 @@ test('answer returns ok with no-reclassified message when reports have no violat
75
74
  const originalDBDir = process.env.DB_DIR;
76
75
  process.env.DB_DIR = tempDir;
77
76
  try {
78
- delete require.cache[require.resolve('./index')];
79
- const {answer} = require('./index');
77
+ const {answer} = await import('./index.ts');
80
78
  const result = await answer();
81
79
  assert.equal(result.status, 'ok');
82
80
  assert.ok(result.answerPage, 'answerPage should be present');
@@ -117,11 +115,10 @@ test('answer returns an error when a report fails to load', async () => {
117
115
  };
118
116
  await fs.writeFile(badReportPath, JSON.stringify(badReport));
119
117
  try {
120
- delete require.cache[require.resolve('./index')];
121
- const {answer} = require('./index');
118
+ const {answer} = await import('./index.ts');
122
119
  const result = await answer();
123
120
  assert.equal(result.status, 'error');
124
- assert.ok(result.message.includes('invalid'), `Expected message about invalid report, got: ${result.message}`);
121
+ assert.ok(result.message.includes('not usable'), `Expected message about unusable report, got: ${result.message}`);
125
122
  }
126
123
  finally {
127
124
  try {