@jrpool/kilotest 46.1.2 → 47.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/.claude/settings.local.json +13 -1
  2. package/.devin/config.local.json +5 -1
  3. package/.github/workflows/ci.yml +0 -3
  4. package/.github/workflows/periodic-smoke-tests.yml +30 -0
  5. package/.github/workflows/publish-mcp.yml +4 -4
  6. package/{alerts.test.js → alerts.test.ts} +17 -29
  7. package/{alerts.js → alerts.ts} +10 -13
  8. package/api/{getReport.test.js → getReport.test.ts} +9 -10
  9. package/api/{getReport.js → getReport.ts} +23 -24
  10. package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
  11. package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
  12. package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
  13. package/api/{listIssues.js → listIssues.ts} +75 -74
  14. package/api/{listReports.test.js → listReports.test.ts} +17 -16
  15. package/api/listReports.ts +87 -0
  16. package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
  17. package/api/{listViolators.js → listViolators.ts} +45 -70
  18. package/api/requestFeature.test.ts +54 -0
  19. package/api/{requestFeature.js → requestFeature.ts} +18 -15
  20. package/api/{requestRetest.test.js → requestRetest.test.ts} +33 -28
  21. package/api/{requestRetest.js → requestRetest.ts} +32 -25
  22. package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
  23. package/api/{requestTest.js → requestTest.ts} +35 -28
  24. package/api/{routes.test.js → routes.test.ts} +5 -5
  25. package/api/{routes.js → routes.ts} +6 -6
  26. package/api/{schemas.js → schemas.ts} +69 -50
  27. package/api/{util.test.js → util.test.ts} +31 -11
  28. package/api/{util.js → util.ts} +61 -36
  29. package/api/version.ts +8 -0
  30. package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
  31. package/balances.ts +87 -0
  32. package/{checkinfo.js → checkinfo.ts} +24 -20
  33. package/docs/AI-TOOLS.md +4 -4
  34. package/docs/BACKLOG.md +356 -0
  35. package/docs/SERVICE.md +144 -17
  36. package/docs/decisions/002-coverage-tool.md +40 -0
  37. package/docs/decisions/003-adr-threshold.md +34 -0
  38. package/docs/decisions/004-docs-directory.md +23 -0
  39. package/docs/decisions/005-observability.md +28 -0
  40. package/docs/decisions/006-git-workflow.md +28 -0
  41. package/docs/decisions/007-internal-testing.md +34 -0
  42. package/docs/decisions/008-dependencies.md +28 -0
  43. package/docs/decisions/009-qai-integration.md +40 -0
  44. package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
  45. package/eslint.config.mjs +27 -0
  46. package/index.html +3 -3
  47. package/{index.test.js → index.test.ts} +380 -143
  48. package/{index.js → index.ts} +382 -283
  49. package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
  50. package/{indexServe.test.js → indexServe.test.ts} +9 -9
  51. package/linkValidation.test.ts +190 -0
  52. package/{mcp.test.js → mcp.test.ts} +53 -26
  53. package/{mcp.js → mcp.ts} +29 -20
  54. package/memory/MEMORY.md +7 -0
  55. package/memory/user_maintainer_context.md +24 -0
  56. package/openapi.yaml +115 -68
  57. package/package.json +35 -15
  58. package/pm2.config.cjs +29 -0
  59. package/{routes.test.js → routes.test.ts} +8 -7
  60. package/{routing.test.js → routing.test.ts} +11 -12
  61. package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
  62. package/serve.ts +15 -0
  63. package/smokeTest.ts +234 -0
  64. package/test/dbFixture.ts +21 -0
  65. package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
  66. package/test/{setup.cjs → setup.ts} +2 -2
  67. package/tsconfig.json +16 -0
  68. package/{util.test.js → util.test.ts} +350 -257
  69. package/util.ts +921 -0
  70. package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
  71. package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
  72. package/web/enqueue/index.ts +56 -0
  73. package/web/enqueueForm/index.html +9 -9
  74. package/web/enqueueForm/index.ts +43 -0
  75. package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
  76. package/web/expungeReportsForm/index.ts +18 -0
  77. package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
  78. package/web/hideReportForm/{index.js → index.ts} +17 -21
  79. package/web/listDiagnoses/{index.js → index.ts} +48 -58
  80. package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
  81. package/web/listIssues/{index.js → index.ts} +67 -92
  82. package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
  83. package/web/listReports/index.html +3 -3
  84. package/web/listReports/{index.js → index.ts} +48 -53
  85. package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
  86. package/web/listRules/{index.js → index.ts} +18 -23
  87. package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
  88. package/web/listTopIssues/{index.js → index.ts} +36 -47
  89. package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
  90. package/web/listViolators/{index.js → index.ts} +51 -71
  91. package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
  92. package/web/manage/index.ts +22 -0
  93. package/web/pruneReportsForm/index.ts +18 -0
  94. package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
  95. package/web/reannotate/index.ts +70 -0
  96. package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
  97. package/web/reannotateForm/{index.js → index.ts} +41 -50
  98. package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
  99. package/web/renewWCAG/{index.js → index.ts} +9 -9
  100. package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
  101. package/web/renewWCAGForm/index.ts +22 -0
  102. package/web/{expungeReportsForm/index.js → reportDeletion.ts} +56 -38
  103. package/web/requestRetest/index.html +7 -7
  104. package/web/requestRetest/index.ts +31 -0
  105. package/web/requestRetest/requestRetest.test.ts +116 -0
  106. package/web/requestRetestForm/{index.js → index.ts} +10 -16
  107. package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
  108. package/web/requestTest/index.html +7 -7
  109. package/web/requestTest/{index.js → index.ts} +5 -5
  110. package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
  111. package/web/requestTestForm/index.html +4 -4
  112. package/web/requestTestForm/index.ts +21 -0
  113. package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
  114. package/web/rewindReportsForm/index.ts +18 -0
  115. package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
  116. package/web/tutorialAI/index.html +184 -0
  117. package/web/tutorialAI/index.ts +80 -0
  118. package/web/tutorialAI/tutorialAI.test.ts +192 -0
  119. package/web/{tutorial → tutorialWeb}/index.html +9 -8
  120. package/web/tutorialWeb/index.ts +78 -0
  121. package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
  122. package/web/unhideReportForm/{index.js → index.ts} +17 -21
  123. package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
  124. package/aceconfig.js +0 -28
  125. package/api/listReports.js +0 -89
  126. package/api/requestFeature.test.js +0 -58
  127. package/api/version.js +0 -8
  128. package/indexFallback.test.js +0 -33
  129. package/indexWorkers.test.js +0 -79
  130. package/jsconfig.json +0 -8
  131. package/pm2.config.js +0 -18
  132. package/smokeTest.js +0 -128
  133. package/util.js +0 -744
  134. package/web/enqueue/index.js +0 -67
  135. package/web/enqueueForm/index.js +0 -49
  136. package/web/manage/index.js +0 -22
  137. package/web/pruneReportsForm/index.js +0 -123
  138. package/web/reannotate/index.js +0 -58
  139. package/web/renewWCAGForm/index.js +0 -22
  140. package/web/requestRetest/index.js +0 -31
  141. package/web/requestRetest/requestRetest.test.js +0 -119
  142. package/web/requestTestForm/index.js +0 -21
  143. package/web/rewindReportsForm/index.js +0 -121
  144. package/web/tutorial/index.js +0 -63
  145. package/web/tutorial/tutorial.test.js +0 -97
  146. /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
  147. /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
@@ -1,18 +1,18 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Renews the WCAG map.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const fs = require('fs/promises');
9
- const path = require('path');
10
- const {getJSON} = require('../../util');
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+ import {getJSON} from '../../util.ts';
11
11
 
12
12
  // FUNCTIONS
13
13
 
14
14
  // Renews the WCAG map and serves an acknowledgment.
15
- exports.answer = async authCode => {
15
+ export const answer = async (authCode: string) => {
16
16
  // If the authorization code is valid:
17
17
  if (authCode === process.env.AUTH_CODE) {
18
18
  // Get the map source response.
@@ -29,16 +29,16 @@ exports.answer = async authCode => {
29
29
  // If there are entries:
30
30
  if (mapEntries) {
31
31
  // Initialize the WCAG map.
32
- const wcagMap = {};
32
+ const wcagMap: Record<string, string> = {};
33
33
  // For each entry:
34
34
  for (const entry of mapEntries) {
35
35
  // Add it to the map.
36
- wcagMap[entry[2]] = entry[1];
36
+ wcagMap[entry[2]!] = entry[1]!;
37
37
  }
38
38
  // Save the map, replacing any existing one.
39
- await fs.writeFile(path.join(__dirname, '..', '..', 'wcagMap.json'), getJSON(wcagMap));
39
+ await fs.writeFile(path.join(import.meta.dirname, '..', '..', 'wcagMap.json'), getJSON(wcagMap));
40
40
  // Get the acknowledgment page.
41
- const answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
41
+ const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
42
42
  // Return it.
43
43
  return {
44
44
  status: 'ok',
@@ -1,20 +1,20 @@
1
1
  /*
2
- renewWCAG.test.js
3
- Unit tests for web/renewWCAG/index.js, covering auth, fetch status, and success branches.
2
+ renewWCAG.test.ts
3
+ Unit tests for web/renewWCAG/index.ts, covering auth, fetch status, and success branches.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {test, before, after, afterEach} = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const fs = require('node:fs/promises');
11
- const path = require('node:path');
12
- const {parse} = require('node-html-parser');
8
+ import {test, before, after, afterEach} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import fs from 'node:fs/promises';
11
+ import path from 'node:path';
12
+ import {parse} from 'node-html-parser';
13
13
 
14
14
  // CONSTANTS
15
15
 
16
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
- const wcagMapPath = path.join(__dirname, '..', '..', 'wcagMap.json');
16
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
17
+ const wcagMapPath = path.join(import.meta.dirname, '..', '..', 'wcagMap.json');
18
18
 
19
19
  // SETUP AND TEARDOWN
20
20
 
@@ -23,8 +23,8 @@ before(async () => {
23
23
  process.env.AUTH_CODE = 'test-auth-code';
24
24
  });
25
25
 
26
- let originalFetch;
27
- let wcagMapBackup;
26
+ let originalFetch: any;
27
+ let wcagMapBackup: any;
28
28
 
29
29
  afterEach(async () => {
30
30
  // Restore the original fetch after each test.
@@ -50,7 +50,7 @@ after(async () => {
50
50
  // HELPER
51
51
 
52
52
  // Replaces global.fetch with a mock that returns the given status and body.
53
- const mockFetch = (status, body = '') => {
53
+ const mockFetch = (status: number, body = '') => {
54
54
  originalFetch = global.fetch;
55
55
  // @ts-expect-error: Replacing the real function with a mock for testing.
56
56
  global.fetch = async () => ({
@@ -62,7 +62,7 @@ const mockFetch = (status, body = '') => {
62
62
  // TESTS
63
63
 
64
64
  test('answer returns an error for an invalid authorization code', async () => {
65
- const {answer} = require('./index');
65
+ const {answer} = await import('./index.ts');
66
66
  const result = await answer('wrong-code');
67
67
  assert.equal(result.status, 'error');
68
68
  assert.equal(result.message, 'Authorization code invalid');
@@ -70,8 +70,7 @@ test('answer returns an error for an invalid authorization code', async () => {
70
70
 
71
71
  test('answer returns an error when the WCAG map source returns a non-200 status', async () => {
72
72
  mockFetch(404);
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, 'WCAG map source not retrieved');
@@ -87,8 +86,7 @@ test('answer returns ok and rewrites the WCAG map when the source returns valid
87
86
  '<a href="understanding/target-size-minimum"><span class="secno">2.5.8 </span>'
88
87
  ].join('\n');
89
88
  mockFetch(200, mockHTML);
90
- delete require.cache[require.resolve('./index')];
91
- const {answer} = require('./index');
89
+ const {answer} = await import('./index.ts');
92
90
  const result = await answer('test-auth-code');
93
91
  assert.equal(result.status, 'ok');
94
92
  assert.ok(result.answerPage, 'answerPage should be present');
@@ -107,13 +105,12 @@ test('answer returns ok and rewrites the WCAG map when the source returns valid
107
105
  test('answer returns an error when matchAll returns no entries', async () => {
108
106
  // Mock matchAll to return null, covering the dead else branch on lines 48-55.
109
107
  const originalMatchAll = String.prototype.matchAll;
110
- String.prototype.matchAll = function() {
108
+ String.prototype.matchAll = function(): any {
111
109
  return null;
112
110
  };
113
111
  mockFetch(200, '<html></html>');
114
112
  try {
115
- delete require.cache[require.resolve('./index')];
116
- const {answer} = require('./index');
113
+ const {answer} = await import('./index.ts');
117
114
  const result = await answer('test-auth-code');
118
115
  assert.equal(result.status, 'error');
119
116
  assert.equal(result.message, 'No entries found in WCAG map source');
@@ -0,0 +1,22 @@
1
+ /*
2
+ index.ts
3
+ Serves a form for renewing the WCAG map.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import fs from 'node:fs/promises';
9
+ import path from 'node:path';
10
+
11
+ // FUNCTIONS
12
+
13
+ // Returns a form for renewing the WCAG map.
14
+ export const answer = async (): Promise<{status: string; answerPage: string}> => {
15
+ // Get the map renewal form.
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
+ };
@@ -1,18 +1,44 @@
1
1
  /*
2
- index.js
3
- Serves a form for deleting sole reports.
2
+ reportDeletion.ts
3
+ Shared implementation of the forms for deleting reports.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getReportData, objectSort, reportsPath} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
8
+ import {
9
+ errorMessage, getReportData, objectSort, populateTemplate, reportsPath
10
+ } from '../util.ts';
11
+ import fs from 'node:fs/promises';
12
+ import path from 'node:path';
13
+
14
+ // TYPES
15
+
16
+ // A summary of an available report, as shown in a deletion form.
17
+ type ReportSpec = {
18
+ timeStamp: string;
19
+ jobID: string;
20
+ issueCount: number;
21
+ preventedEngineCount: number;
22
+ url: string;
23
+ };
24
+
25
+ // The distinctive properties of a report-deletion form.
26
+ export type DeletionSpec = {
27
+ intro: string;
28
+ emptyIntro: string;
29
+ failurePrefix: string;
30
+ isDeletable: (specs: ReportSpec[], index: number) => boolean;
31
+ };
11
32
 
12
33
  // FUNCTIONS
13
34
 
14
- // Returns a form for deleting sole reports.
15
- exports.answer = async (_, search) => {
35
+ // Returns a form for deleting the reports that the predicate marks as deletable.
36
+ export const reportDeletionForm = async (
37
+ dirName: string,
38
+ search: string,
39
+ deletionSpec: DeletionSpec
40
+ ): Promise<{status: string; message?: string; answerPage?: string}> => {
41
+ const {emptyIntro, failurePrefix, intro, isDeletable} = deletionSpec;
16
42
  const searchParams = new URLSearchParams(search);
17
43
  const authCode = searchParams?.get('authCode');
18
44
  const jobNames = searchParams?.getAll('report');
@@ -28,12 +54,12 @@ exports.answer = async (_, search) => {
28
54
  }
29
55
  }
30
56
  // If this failed:
31
- catch (error) {
57
+ catch (error: unknown) {
32
58
  // Return why.
33
59
  return {
34
60
  status: 'error',
35
- message: `Deleting sole reports failed (${error.message})`
36
- }
61
+ message: `${failurePrefix} (${errorMessage(error)})`
62
+ };
37
63
  }
38
64
  }
39
65
  // Otherwise, i.e. if the authorization code is invalid:
@@ -42,25 +68,27 @@ exports.answer = async (_, search) => {
42
68
  return {
43
69
  status: 'error',
44
70
  message: 'Invalid authorization code'
45
- }
71
+ };
46
72
  }
47
73
  }
48
74
  const reportNames = await fs.readdir(reportsPath());
49
- const reportSpecs = [];
75
+ // Initialize an array of report summaries.
76
+ const reportSpecs: ReportSpec[] = [];
50
77
  // For each report:
51
78
  for (const reportName of reportNames) {
52
- const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
79
+ const [timeStamp, jobID] = reportName.slice(0, -5).split('-') as [string, string];
53
80
  // Get a summary of it.
54
81
  const reportFacts = await getReportData(timeStamp, jobID);
55
- const {error, issueCount, preventedEngineCount, url} = reportFacts;
56
82
  // If this failed:
57
- if (error) {
83
+ if (reportFacts.error !== undefined) {
58
84
  // Return why.
59
85
  return {
60
86
  status: 'error',
61
- message: error
62
- }
87
+ message: reportFacts.error
88
+ };
63
89
  }
90
+ const {issueCount, preventedEngineCount, url} = reportFacts;
91
+ // Otherwise, i.e. if it succeeded, add the summary to the array.
64
92
  reportSpecs.push({
65
93
  timeStamp,
66
94
  jobID,
@@ -69,49 +97,39 @@ exports.answer = async (_, search) => {
69
97
  url
70
98
  });
71
99
  }
72
- // Sort the summaries primarily URL and secondarily by time stamp.
100
+ // Sort the summaries primarily by URL and secondarily by time stamp.
73
101
  objectSort(reportSpecs, 'timeStamp', 'alpha');
74
102
  objectSort(reportSpecs, 'url', 'alpha');
75
- const lines = [];
103
+ const lines: string[] = [];
76
104
  const margin = ' '.repeat(12);
77
105
  let anyDeletable = false;
78
106
  // For each summary:
79
- reportSpecs.forEach((spec, index) => {
80
- const {timeStamp, jobID, issueCount, preventedEngineCount, url} = spec;
107
+ reportSpecs.forEach((reportSpec, index) => {
108
+ const {timeStamp, jobID, issueCount, preventedEngineCount, url} = reportSpec;
81
109
  const jobName = `${timeStamp}-${jobID}`;
82
110
  const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
83
- // If its report is the sole report on a target:
84
- if (reportSpecs[index - 1]?.url !== url && reportSpecs[index + 1]?.url !== url) {
111
+ // If its report is deletable:
112
+ if (isDeletable(reportSpecs, index)) {
85
113
  // Add a line with a deletion checkbox.
86
114
  lines.push(
87
115
  `${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
88
116
  );
89
117
  anyDeletable = true;
90
118
  }
91
- // Otherwise, i.e. if its report is not the sole report on a target:
119
+ // Otherwise, i.e. if its report is not deletable:
92
120
  else {
93
121
  // Add a line without a deletion checkbox.
94
122
  lines.push(`${margin}<p>${specString}</p>`);
95
123
  }
96
124
  });
97
- const intro = anyDeletable
98
- ? 'Choose the sole reports to delete.'
99
- : 'Each target has at least 2 reports, so there are no reports to delete.';
100
- const disabled = anyDeletable ? '' : ' disabled';
101
- const query = {
125
+ const query: Record<string, string> = {
102
126
  reports: lines.join('\n'),
103
- intro,
104
- disabled
127
+ intro: anyDeletable ? intro : emptyIntro,
128
+ disabled: anyDeletable ? '' : ' disabled'
105
129
  };
106
- // Get the order form template.
107
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
108
- // Replace its placeholders.
109
- Object.keys(query).forEach(param => {
110
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
111
- });
112
130
  // Return the populated page.
113
131
  return {
114
132
  status: 'ok',
115
- answerPage
133
+ answerPage: await populateTemplate(dirName, query)
116
134
  };
117
135
  };
@@ -6,18 +6,18 @@
6
6
  <meta name="publisher" content="Jonathan Robert Pool">
7
7
  <meta name="creator" content="Jonathan Robert Pool">
8
8
  <meta name="keywords" content="report,accessibility,a11y">
9
- <title>__target__ retest recommendation received | Kilotest</title>
9
+ <title>__target__ retest request received | Kilotest</title>
10
10
  <link rel="icon" href="/favicon.ico">
11
11
  <link rel="stylesheet" href="/style.css">
12
12
  </head>
13
13
  <body>
14
14
  <main>
15
- <h1><a href="/">Kilotest</a>: Retest recommendation received for __target__</h1>
16
- <p>Your recommendation to retest the __target__ page has been received.</p>
17
- <p>The reason for your recommendation has been recorded as <q>__why__</q>.</p>
18
- <p>Thank you for your recommendation.</p>
19
- <p>If a <a href="/enqueueForm.html">Kilotest manager approves this recommendation</a>, a test order will be added to the queue.</p>
20
- <p>In most cases, if you check Kilotest again in 1 day, you will find that the recommended test has been performed.</p>
15
+ <h1><a href="/">Kilotest</a>: Retest request received for __target__</h1>
16
+ <p>Your request to retest the __target__ page has been received.</p>
17
+ <p>The reason for your request has been recorded as <q>__why__</q>.</p>
18
+ <p>Thank you for your request.</p>
19
+ <p>If a <a href="/enqueueForm.html">Kilotest manager approves this request</a>, a test order will be added to the queue.</p>
20
+ <p>In most cases, if you check Kilotest again in 1 day, you will find that the requested test has been performed.</p>
21
21
  </main>
22
22
  </body>
23
23
  </html>
@@ -0,0 +1,31 @@
1
+ /*
2
+ index.ts
3
+ Records a retest request.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {getReportExtracts, processTestRequest} from '../../util.ts';
9
+
10
+ // FUNCTIONS
11
+
12
+ export const answer = async (pageArgs: string, why: string) => {
13
+ const [timeStamp, jobID] = pageArgs.split('/');
14
+ // Get data on the latest available reports.
15
+ const reportExtracts = await getReportExtracts(true);
16
+ // Get data on the report whose page is to be retested.
17
+ const reportExtract = reportExtracts.find(
18
+ (extract) => extract.timeStamp === timeStamp && extract.jobID === jobID
19
+ );
20
+ // If no matching report was found:
21
+ if (!reportExtract) {
22
+ // Return why.
23
+ return {
24
+ status: 'error',
25
+ message: `No report found for ${timeStamp}-${jobID}`
26
+ };
27
+ }
28
+ const {url, description} = reportExtract;
29
+ // Otherwise, i.e. if it succeeded, process the request.
30
+ return await processTestRequest('retest', import.meta.dirname, description, url, why);
31
+ };
@@ -0,0 +1,116 @@
1
+ /*
2
+ requestRetest.test.ts
3
+ Unit tests for web/requestRetest/index.ts, covering the success path of answer.
4
+ */
5
+
6
+ // IMPORTS
7
+
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 path from 'node:path';
12
+ import {parse} from 'node-html-parser';
13
+
14
+ // CONSTANTS
15
+
16
+ import {fixtureDBDir} from '../../test/dbFixture.ts';
17
+ const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
18
+
19
+ // SETUP AND TEARDOWN
20
+
21
+ before(async () => {
22
+ process.env.DB_DIR = fixtureDBDir;
23
+ process.env.AUTH_CODE = 'test-auth-code';
24
+ });
25
+
26
+ beforeEach(async () => {
27
+ // Reset testRequests.json and clean job directories before each test.
28
+ await fs.writeFile(testRequestsPath, '{}\n');
29
+ for (const sub of ['claimed', 'queue', 'failed']) {
30
+ const dir = path.join(fixtureDBDir, 'jobs', sub);
31
+ await fs.mkdir(dir, {recursive: true});
32
+ const files = await fs.readdir(dir).catch(() => []);
33
+ for (const file of files) {
34
+ try {
35
+ await fs.unlink(path.join(dir, file));
36
+ }
37
+ catch {
38
+ // Ignore cleanup errors for files that may not exist.
39
+ }
40
+ }
41
+ }
42
+ });
43
+
44
+ after(async () => {
45
+ // Restore testRequests.json and clean job directories after all tests.
46
+ await fs.writeFile(testRequestsPath, '{}\n');
47
+ for (const sub of ['claimed', 'queue', 'failed']) {
48
+ const dir = path.join(fixtureDBDir, 'jobs', sub);
49
+ await fs.mkdir(dir, {recursive: true});
50
+ const files = await fs.readdir(dir).catch(() => []);
51
+ for (const file of files) {
52
+ try {
53
+ await fs.unlink(path.join(dir, file));
54
+ }
55
+ catch {
56
+ // Ignore cleanup errors for files that may not exist.
57
+ }
58
+ }
59
+ }
60
+ });
61
+
62
+ // TESTS
63
+
64
+ test('answer returns ok with a populated answer page for a valid retest request', async () => {
65
+ // Require the module after DB_DIR is set.
66
+ const {answer} = await import('./index.ts');
67
+ // Use the 260101T0001-ct report (All CantTell Page, https://example.com/canttell).
68
+ const result: any = await answer('260101T0001/ct', 'Because changes were made');
69
+ assert.equal(result.status, 'ok');
70
+ assert.ok(result.answerPage, 'answerPage should be present');
71
+ // Verify the placeholders were replaced with the report target.
72
+ assert.ok(result.answerPage.includes('All CantTell Page'));
73
+ assert.ok(result.answerPage.includes('Because changes were made'));
74
+ assert.ok(!result.answerPage.includes('__target__'));
75
+ assert.ok(!result.answerPage.includes('__why__'));
76
+ // Verify the HTML is well-formed.
77
+ const doc = parse(result.answerPage);
78
+ const h1 = doc.querySelector('h1');
79
+ assert.ok(h1);
80
+ assert.ok(h1.textContent.includes('All CantTell Page'));
81
+ });
82
+
83
+ test('answer returns an error when the report does not exist', async () => {
84
+ const {answer} = await import('./index.ts');
85
+ // Use a nonexistent report timestamp and jobID.
86
+ const result: any = await answer('990101T0000/xxx', 'Because changes were made');
87
+ assert.equal(result.status, 'error');
88
+ assert.ok(result.message?.includes('No report found'), `Expected no-report-found message, got: ${result.message}`);
89
+ });
90
+
91
+ test('answer returns an error for an invalid retest request', async () => {
92
+ const {answer} = await import('./index.ts');
93
+ // Use a valid report but a too-short reason.
94
+ const result = await answer('260101T0001/ct', 'why');
95
+ assert.equal(result.status, 'error');
96
+ assert.equal(result.message, 'Invalid request');
97
+ });
98
+
99
+ test('answer returns an error when no extracts are available for the report', async (t) => {
100
+ // Mock getReportExtracts to return an empty array, delegating other exports.
101
+ // (getReportExtracts no longer returns error objects; reports not in the list
102
+ // are treated as not found.)
103
+ const realUtil = await import('../../util.ts');
104
+ t.mock.module('../../util.ts', {
105
+ exports: {
106
+ ...realUtil,
107
+ getReportExtracts: async () => []
108
+ }
109
+ } as any);
110
+ // Import a fresh instance of index.ts so it binds to the mocked module.
111
+ // The query string makes the specifier unique, bypassing the module cache.
112
+ const {answer} = await import('./index.ts?mockNoExtracts' as any);
113
+ const result: any = await answer('260101T0001/ct', 'Because changes were made');
114
+ assert.equal(result.status, 'error');
115
+ assert.ok(result.message?.includes('No report found'), `Expected no-report-found message, got: ${result.message}`);
116
+ });
@@ -1,38 +1,36 @@
1
1
  /*
2
- index.js
2
+ index.ts
3
3
  Serves a form for requesting a retest.
4
4
  */
5
5
 
6
6
  // IMPORTS
7
7
 
8
- const {getAgoString, getDateTimeString, getLatestReportExtracts} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
8
+ import {getAgoString, getDateTimeString, getReportExtracts, populateTemplate} from '../../util.ts';
11
9
 
12
10
  // FUNCTIONS
13
11
 
14
12
  // Returns a retest recommendation form.
15
- exports.answer = async pageArgs => {
16
- const [timeStamp, jobID] = pageArgs.split('/');
13
+ export const answer = async (pageArgs: string) => {
14
+ const [timeStamp, jobID] = pageArgs.split('/') as [string, string];
17
15
  // Get data on the latest available reports.
18
- const reportExtracts = await getLatestReportExtracts();
16
+ const reportExtracts = await getReportExtracts(true);
19
17
  // Get data on the report whose page is to be retested.
20
18
  const reportExtract = reportExtracts.find(
21
- reportExtract => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
19
+ (reportExtract: any) => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
22
20
  );
23
21
  // Initialize the page description.
24
- let target;
22
+ let target: string;
25
23
  // If getting the data succeeded:
26
24
  if (reportExtract) {
27
25
  // Update the page description.
28
- target = reportExtract.what;
26
+ target = reportExtract.description;
29
27
  }
30
28
  // Otherwise, i.e. if it failed:
31
29
  else {
32
30
  // Make the form report the failure.
33
31
  target = 'The specified page is not available for retesting';
34
32
  }
35
- const query = {
33
+ const query: Record<string, string> = {
36
34
  target,
37
35
  timeStamp,
38
36
  jobID,
@@ -40,11 +38,7 @@ exports.answer = async pageArgs => {
40
38
  dateTime: getDateTimeString(timeStamp)
41
39
  };
42
40
  // Get the recommendation form template.
43
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
44
- // Replace its placeholders.
45
- Object.keys(query).forEach(param => {
46
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
47
- });
41
+ const answerPage = await populateTemplate(import.meta.dirname, query);
48
42
  // Return the populated page.
49
43
  return {
50
44
  status: 'ok',
@@ -1,22 +1,21 @@
1
1
  /*
2
- requestRetestForm.test.js
3
- Unit tests for web/requestRetestForm/index.js.
2
+ requestRetestForm.test.ts
3
+ Unit tests for web/requestRetestForm/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 {parse} = require('node-html-parser');
12
- const {answer} = require('./index');
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {parse} from 'node-html-parser';
11
+ import {answer} from './index.ts';
13
12
 
14
13
  // SETUP AND TEARDOWN
15
14
 
16
15
  const savedDBDir = process.env.DB_DIR;
17
16
 
18
- before(() => {
19
- process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
+ before(async () => {
18
+ process.env.DB_DIR = (await import('../../test/dbFixture.ts')).fixtureDBDir;
20
19
  });
21
20
 
22
21
  after(() => {
@@ -35,15 +34,15 @@ test('requestRetestForm returns ok with the page name for a valid report', async
35
34
  assert.equal(result.status, 'ok');
36
35
  assert.ok(result.answerPage);
37
36
  const html = parse(result.answerPage);
38
- const title = html.querySelector('title').text;
39
- assert.ok(title.includes('Mixed Outcomes Page'));
37
+ const title = html.querySelector('title')?.text;
38
+ assert.ok(title?.includes('Mixed Outcomes Page'));
40
39
  });
41
40
 
42
41
  test('requestRetestForm populates the form action with the timeStamp and jobID', async () => {
43
42
  const result = await answer('260202T0000/new');
44
43
  const html = parse(result.answerPage);
45
44
  const form = html.querySelector('form');
46
- assert.equal(form.getAttribute('action'), '/requestRetest.html/260202T0000/new');
45
+ assert.equal(form?.getAttribute('action'), '/requestRetest.html/260202T0000/new');
47
46
  });
48
47
 
49
48
  test('requestRetestForm shows the fallback message for a nonexistent report', async () => {
@@ -6,18 +6,18 @@
6
6
  <meta name="publisher" content="Jonathan Robert Pool">
7
7
  <meta name="creator" content="Jonathan Robert Pool">
8
8
  <meta name="keywords" content="report,accessibility,a11y">
9
- <title>__target__ test recommendation received | Kilotest</title>
9
+ <title>__target__ test request received | Kilotest</title>
10
10
  <link rel="icon" href="/favicon.ico">
11
11
  <link rel="stylesheet" href="/style.css">
12
12
  </head>
13
13
  <body>
14
14
  <main>
15
- <h1><a href="/">Kilotest</a>: Test recommendation received for __target__</h1>
16
- <p>Your recommendation to test the __target__ page has been received.</p>
17
- <p>The reason for your recommendation has been recorded as <q>__why__</q>.</p>
18
- <p>Thank you for your recommendation.</p>
19
- <p>If a <a href="/enqueueForm.html">Kilotest manager approves this recommendation</a>, a test order will be added to the queue.</p>
20
- <p>In most cases, if you check Kilotest again in 1 day, you will find that the recommended job has been performed.</p>
15
+ <h1><a href="/">Kilotest</a>: Test request received for __target__</h1>
16
+ <p>Your request to test the __target__ page has been received.</p>
17
+ <p>The reason for your request has been recorded as <q>__why__</q>.</p>
18
+ <p>Thank you for your request.</p>
19
+ <p>If a <a href="/enqueueForm.html">Kilotest manager approves this request</a>, a test order will be added to the queue.</p>
20
+ <p>In most cases, if you check Kilotest again in 1 day, you will find that the requested job has been performed.</p>
21
21
  </main>
22
22
  </body>
23
23
  </html>