@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,67 +0,0 @@
1
- /*
2
- index.js
3
- Implements a test request approval.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {
9
- getJSON, getNowStamp, getRandomString, getRecs, isURL, jobsPath, recsLock
10
- } = require('../../util');
11
- const fs = require('fs/promises');
12
- const path = require('path');
13
-
14
- // FUNCTIONS
15
-
16
- // Implements a test request approval and returns a revised request page.
17
- exports.answer = async (url, what, authCode) => {
18
- // If the arguments are valid:
19
- if (isURL(url) && what && authCode === process.env.AUTH_CODE) {
20
- // Get the job template.
21
- const jobTemplateJSON = await fs.readFile(path.join(__dirname, '..', '..', 'job.json'), 'utf8');
22
- const job = JSON.parse(jobTemplateJSON);
23
- const nowStamp = getNowStamp();
24
- // Populate the template with job properties.
25
- const jobIDSuffix = getRandomString(3);
26
- const jobName = `${nowStamp}-${jobIDSuffix}`;
27
- job.id = jobName;
28
- job.creationTimeStamp = nowStamp;
29
- job.executionTimeStamp = nowStamp;
30
- job.target.what = what;
31
- job.target.url = url;
32
- const query = {
33
- target: what,
34
- jobName
35
- };
36
- // Save the job in the queue.
37
- await fs.writeFile(
38
- path.join(jobsPath(), 'queue', `${jobName}.json`), getJSON(job)
39
- );
40
- console.log(`Retest queued for ${what} as job ${jobName}`);
41
- // Isolate this revision.
42
- await recsLock(async () => {
43
- // Get the recommendations.
44
- const recs = await getRecs();
45
- // Delete the recommendations to test the target.
46
- delete recs[url];
47
- // Save the revised recommendations.
48
- await fs.writeFile(path.join(jobsPath(), 'recs.json'), getJSON(recs));
49
- });
50
- // Get the answer template.
51
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
52
- // Replace its placeholders.
53
- Object.keys(query).forEach(param => {
54
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
55
- });
56
- // Return the populated page.
57
- return {
58
- status: 'ok',
59
- answerPage
60
- };
61
- }
62
- // Otherwise, i.e. if the authorization code is invalid, return an error page.
63
- return {
64
- status: 'error',
65
- message: 'Invalid authorization code'
66
- };
67
- };
@@ -1,49 +0,0 @@
1
- /*
2
- index.js
3
- Serves a form for approving or rejecting a test request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getRecs} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
11
-
12
- // FUNCTIONS
13
-
14
- // Returns a test order form.
15
- exports.answer = async () => {
16
- const recs = await getRecs();
17
- const urls = Object.keys(recs);
18
- const margin = ' '.repeat(12);
19
- const lines = [];
20
- // For each page with any recommendations:
21
- urls.forEach(url => {
22
- // Add a radio button and its URL to the lines.
23
- lines.push(`${margin}<h2><input type="radio" name="target" value="${url}" required> ${url}</h2>`);
24
- // Get the recommended target names for the page.
25
- const targetNames = new Set(recs[url].map(rec => rec.what));
26
- // For each recommended target name:
27
- targetNames.forEach(what => {
28
- const radio = `<input type="radio" name="target" value="${url}\t${what}" required>`;
29
- // Add a radio button and the recommended page name to the lines.
30
- lines.push(`${margin} <p>${radio} ${what}</p>`);
31
- });
32
- });
33
- const query = {
34
- recs: lines.join('\n'),
35
- noRecs: urls.length ? '' : 'No recommendations exist now.',
36
- disabled: urls.length ? '' : ' disabled'
37
- };
38
- // Get the order form template.
39
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
40
- // Replace its placeholders.
41
- Object.keys(query).forEach(param => {
42
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
43
- });
44
- // Return the populated page.
45
- return {
46
- status: 'ok',
47
- answerPage
48
- };
49
- };
@@ -1,22 +0,0 @@
1
- /*
2
- index.js
3
- Lists the actions that managers can take.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const fs = require('fs/promises');
9
- const path = require('path');
10
-
11
- // FUNCTIONS
12
-
13
- // Returns the answer page.
14
- exports.answer = async () => {
15
- // Get the answer page.
16
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
17
- // Return it.
18
- return {
19
- status: 'ok',
20
- answerPage
21
- };
22
- };
@@ -1,123 +0,0 @@
1
- /*
2
- index.js
3
- Serves a form for deleting superseded reports.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getReportData, reportsPath} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
11
-
12
- // FUNCTIONS
13
-
14
- // Returns a form for deleting non-latest reports.
15
- exports.answer = async (_, search) => {
16
- const searchParams = new URLSearchParams(search);
17
- const authCode = searchParams?.get('authCode');
18
- const jobNames = searchParams?.getAll('report');
19
- // If the form has displayed itself after a submission and any reports are to be deleted:
20
- if (jobNames?.length) {
21
- // If the authorization code is valid:
22
- if (authCode === process.env.AUTH_CODE) {
23
- try {
24
- // For each report to be deleted:
25
- for (const jobName of jobNames) {
26
- // Delete it.
27
- await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
28
- }
29
- }
30
- // If this failed:
31
- catch (error) {
32
- // Return why.
33
- return {
34
- status: 'error',
35
- message: `Deleting superseded reports failed (${error.message})`
36
- }
37
- }
38
- }
39
- // Otherwise, i.e. if the authorization code is invalid:
40
- else {
41
- // Return this.
42
- return {
43
- status: 'error',
44
- message: 'Invalid authorization code'
45
- }
46
- }
47
- }
48
- const reportNames = await fs.readdir(reportsPath());
49
- // Initialize an array of report summaries.
50
- const reportSpecs = [];
51
- // For each report:
52
- for (const reportName of reportNames) {
53
- const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
54
- // Get a summary of it.
55
- const reportFacts = await getReportData(timeStamp, jobID);
56
- const {error, issueCount, preventedEngineCount, url} = reportFacts;
57
- // If this failed:
58
- if (error) {
59
- // Return why.
60
- return {
61
- status: 'error',
62
- message: error.message
63
- }
64
- }
65
- // Otherwise, i.e. if it succeeded, add the summary to the array.
66
- reportSpecs.push({
67
- timeStamp,
68
- jobID,
69
- issueCount,
70
- preventedEngineCount,
71
- url
72
- });
73
- }
74
- // Sort the summaries by URL and then by time stamp.
75
- reportSpecs.sort((a, b) => {
76
- if (a.url === b.url) {
77
- return a.timeStamp.localeCompare(b.timeStamp);
78
- }
79
- return a.url.localeCompare(b.url);
80
- });
81
- const lines = [];
82
- const margin = ' '.repeat(12);
83
- let anyDeletable = false;
84
- // For each summary:
85
- reportSpecs.forEach((spec, index) => {
86
- const {timeStamp, jobID, issueCount, preventedEngineCount, url} = spec;
87
- const jobName = `${timeStamp}-${jobID}`;
88
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
89
- // If its report is a non-latest report:
90
- if (reportSpecs[index + 1]?.url === url) {
91
- // Add a line with a deletion checkbox.
92
- lines.push(
93
- `${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
94
- );
95
- anyDeletable = true;
96
- }
97
- // Otherwise, i.e. if its report is a latest report:
98
- else {
99
- // Add a line without a deletion checkbox.
100
- lines.push(`${margin}<p>${specString}</p>`);
101
- }
102
- });
103
- const intro = anyDeletable
104
- ? 'Choose the superseded reports to delete.'
105
- : 'Each target has only 1 report, so there are no superseded reports to delete.';
106
- const disabled = anyDeletable ? '' : ' disabled';
107
- const query = {
108
- reports: lines.join('\n'),
109
- intro,
110
- disabled
111
- };
112
- // Get the order form template.
113
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
114
- // Replace its placeholders.
115
- Object.keys(query).forEach(param => {
116
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
117
- });
118
- // Return the populated page.
119
- return {
120
- status: 'ok',
121
- answerPage
122
- };
123
- };
@@ -1,58 +0,0 @@
1
- /*
2
- index.js
3
- Reannotates all available latest reports.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {annotateReport, getReportExtracts} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
11
-
12
- // FUNCTIONS
13
-
14
- // Implements a reannotation order and returns an acknowledgement page.
15
- exports.answer = async authCode => {
16
- // If the authorization code is valid:
17
- if (authCode === process.env.AUTH_CODE) {
18
- // Get data on the available reports.
19
- const reportExtracts = await getReportExtracts();
20
- // If any exist:
21
- if (reportExtracts.length) {
22
- // For each report:
23
- for (const reportExtract of reportExtracts) {
24
- const {timeStamp, jobID} = reportExtract;
25
- // Reannotate it.
26
- const annotationError = await annotateReport(timeStamp, jobID);
27
- // If this failed:
28
- if (annotationError) {
29
- // Return an error page.
30
- return {
31
- status: 'error',
32
- message: annotationError
33
- };
34
- }
35
- }
36
- }
37
- // Otherwis, i.e. if it failed:
38
- else {
39
- // Return an error page.
40
- return {
41
- status: 'error',
42
- message: 'Got data on no available reports'
43
- };
44
- }
45
- // If every annotation succeeded, get the answer page.
46
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
47
- // Return it.
48
- return {
49
- status: 'ok',
50
- answerPage
51
- };
52
- }
53
- // Otherwise, i.e. if the authorization code is invalid, return an error page.
54
- return {
55
- status: 'error',
56
- message: 'Invalid authorization code'
57
- };
58
- };
@@ -1,22 +0,0 @@
1
- /*
2
- index.js
3
- Serves a form for renewing the WCAG map.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const fs = require('fs/promises');
9
- const path = require('path');
10
-
11
- // FUNCTIONS
12
-
13
- // Returns a form for renewing the WCAG map.
14
- exports.answer = async () => {
15
- // Get the map renewal form.
16
- const answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
17
- // Return it.
18
- return {
19
- status: 'ok',
20
- answerPage
21
- };
22
- };
@@ -1,31 +0,0 @@
1
- /*
2
- index.js
3
- Records a retest request.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getLatestReportExtracts, processTestRequest} = require('../../util');
9
-
10
- // FUNCTIONS
11
-
12
- exports.answer = async (pageArgs, why) => {
13
- const [timeStamp, jobID] = pageArgs.split('/');
14
- // Get data on the latest available reports.
15
- const reportExtracts = await getLatestReportExtracts();
16
- // Get data on the report whose page is to be retested.
17
- const reportExtract = reportExtracts.find(
18
- reportExtract => reportExtract.timeStamp === timeStamp && reportExtract.jobID === jobID
19
- );
20
- const {error, url, what} = reportExtract;
21
- // If this failed:
22
- if (error) {
23
- // Return why.
24
- return {
25
- status: 'error',
26
- message: error
27
- };
28
- }
29
- // Otherwise, i.e. if it succeeded, process the request.
30
- return await processTestRequest('retest', __dirname, what, url, why);
31
- };
@@ -1,119 +0,0 @@
1
- /*
2
- requestRetest.test.js
3
- Unit tests for web/requestRetest/index.js, covering the success path of answer.
4
- */
5
-
6
- // IMPORTS
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 path = require('node:path');
12
- const {parse} = require('node-html-parser');
13
-
14
- // CONSTANTS
15
-
16
- const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
- const recsPath = path.join(fixtureDBDir, 'jobs', 'recs.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 recs.json and clean job directories before each test.
28
- await fs.writeFile(recsPath, '{}\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 recs.json and clean job directories after all tests.
46
- await fs.writeFile(recsPath, '{}\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} = require('./index');
67
- // Use the 260101T0001-ct report (All CantTell Page, https://example.com/canttell).
68
- const result = 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 throws when the report does not exist', async () => {
84
- const {answer} = require('./index');
85
- // Use a nonexistent report timestamp and jobID.
86
- await assert.rejects(
87
- () => answer('990101T0000/xxx', 'Because changes were made'),
88
- /Cannot destructure property 'error'/
89
- );
90
- });
91
-
92
- test('answer returns an error for an invalid retest recommendation', async () => {
93
- const {answer} = require('./index');
94
- // Use a valid report but a too-short reason.
95
- const result = await answer('260101T0001/ct', 'why');
96
- assert.equal(result.status, 'error');
97
- assert.equal(result.message, 'Invalid recommendation');
98
- });
99
-
100
- test('answer returns an error when the report extract has an error', async () => {
101
- // Mock getLatestReportExtracts to return an extract with an error.
102
- const util = require('../../util');
103
- const original = util.getLatestReportExtracts;
104
- util.getLatestReportExtracts = async () => [
105
- {timeStamp: '260101T0001', jobID: 'ct', error: 'Report data unavailable'}
106
- ];
107
- try {
108
- // Re-require to pick up the mocked function.
109
- delete require.cache[require.resolve('./index')];
110
- const {answer} = require('./index');
111
- const result = await answer('260101T0001/ct', 'Because changes were made');
112
- assert.equal(result.status, 'error');
113
- assert.equal(result.message, 'Report data unavailable');
114
- }
115
- finally {
116
- util.getLatestReportExtracts = original;
117
- delete require.cache[require.resolve('./index')];
118
- }
119
- });
@@ -1,21 +0,0 @@
1
- /*
2
- index.js
3
- Serves a form for requesting a test.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const fs = require('fs/promises');
9
- const path = require('path');
10
-
11
- // FUNCTIONS
12
-
13
- exports.answer = async () => {
14
- // Get the test request form template.
15
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
16
- // Return it.
17
- return {
18
- status: 'ok',
19
- answerPage
20
- };
21
- };
@@ -1,121 +0,0 @@
1
- /*
2
- index.js
3
- Serves a form for deleting latest superseding reports.
4
- */
5
-
6
- // IMPORTS
7
-
8
- const {getReportData, reportsPath} = require('../../util');
9
- const fs = require('fs/promises');
10
- const path = require('path');
11
-
12
- // FUNCTIONS
13
-
14
- // Returns a form for deleting latest superseding reports.
15
- exports.answer = async (_, search) => {
16
- const searchParams = new URLSearchParams(search);
17
- const authCode = searchParams?.get('authCode');
18
- const jobNames = searchParams?.getAll('report');
19
- // If the form has been displayed by itself after a submission and any reports are to be deleted:
20
- if (jobNames?.length) {
21
- // If the authorization code is valid:
22
- if (authCode === process.env.AUTH_CODE) {
23
- try {
24
- // For each report to be deleted:
25
- for (const jobName of jobNames) {
26
- // Delete it.
27
- await fs.unlink(path.join(reportsPath(), `${jobName}.json`));
28
- }
29
- }
30
- // If this failed:
31
- catch (error) {
32
- // Return why.
33
- return {
34
- status: 'error',
35
- message: `Deleting latest superseding reports failed (${error.message})`
36
- }
37
- }
38
- }
39
- // Otherwise, i.e. if the authorization code is invalid:
40
- else {
41
- // Report the error.
42
- return {
43
- status: 'error',
44
- message: 'Invalid authorization code'
45
- }
46
- }
47
- }
48
- const reportNames = await fs.readdir(reportsPath());
49
- const reportSpecs = [];
50
- // For each report:
51
- for (const reportName of reportNames) {
52
- const [timeStamp, jobID] = reportName.slice(0, -5).split('-');
53
- // Get a summary of it.
54
- const reportFacts = await getReportData(timeStamp, jobID);
55
- const {error, issueCount, preventedEngineCount, url} = reportFacts;
56
- // If this failed:
57
- if (error) {
58
- // Return why.
59
- return {
60
- status: 'error',
61
- message: error
62
- }
63
- }
64
- reportSpecs.push({
65
- timeStamp,
66
- jobID,
67
- issueCount,
68
- preventedEngineCount,
69
- url
70
- });
71
- }
72
- // Sort the summaries by URL and then by time stamp.
73
- reportSpecs.sort((a, b) => {
74
- if (a.url === b.url) {
75
- return a.timeStamp.localeCompare(b.timeStamp);
76
- }
77
- return a.url.localeCompare(b.url);
78
- });
79
- const lines = [];
80
- const margin = ' '.repeat(12);
81
- let anyDeletable = false;
82
- // For each summary:
83
- reportSpecs.forEach((spec, index) => {
84
- const {timeStamp, jobID, issueCount, preventedEngineCount, url} = spec;
85
- const jobName = `${timeStamp}-${jobID}`;
86
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
87
- // If its report is the latest report on a target with at least 2 reports:
88
- if (reportSpecs[index - 1]?.url === url && reportSpecs[index + 1]?.url !== url) {
89
- // Add a line with a deletion checkbox.
90
- lines.push(
91
- `${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
92
- );
93
- anyDeletable = true;
94
- }
95
- // Otherwise, i.e. if its report is a superseded report or the only report on its target:
96
- else {
97
- // Add a line without a deletion checkbox.
98
- lines.push(`${margin}<p>${specString}</p>`);
99
- }
100
- });
101
- const intro = anyDeletable
102
- ? 'Choose the latest superseding reports to delete.'
103
- : 'Each target has only 1 report, so there are no reports to delete.';
104
- const disabled = anyDeletable ? '' : ' disabled';
105
- const query = {
106
- reports: lines.join('\n'),
107
- intro,
108
- disabled
109
- };
110
- // Get the order form template.
111
- let answerPage = await fs.readFile(path.join(__dirname, 'index.html'), 'utf8');
112
- // Replace its placeholders.
113
- Object.keys(query).forEach(param => {
114
- answerPage = answerPage.replace(new RegExp(`__${param}__`, 'g'), query[param]);
115
- });
116
- // Return the populated page.
117
- return {
118
- status: 'ok',
119
- answerPage
120
- };
121
- };