@jrpool/kilotest 46.0.8 → 46.0.14

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 (60) hide show
  1. package/.devin/config.local.json +4 -1
  2. package/.github/workflows/{smoke-test.yml → ci.yml} +8 -2
  3. package/README.md +14 -8
  4. package/alerts.test.js +137 -0
  5. package/api/listDiagnoses.test.js +44 -0
  6. package/api/listIssues.js +1 -1
  7. package/api/listIssues.test.js +10 -0
  8. package/api/listReports.test.js +6 -2
  9. package/api/listViolators.test.js +35 -0
  10. package/api/requestTest.test.js +7 -0
  11. package/api/routes.test.js +63 -0
  12. package/api/util.js +9 -11
  13. package/api/util.test.js +68 -2
  14. package/apiIntegration.test.js +13 -3
  15. package/docs/AI-TOOLS.md +41 -158
  16. package/docs/{Smoke Test.json → CI.json} +4 -3
  17. package/index.html +3 -3
  18. package/index.js +64 -77
  19. package/index.test.js +1152 -0
  20. package/indexBalances.test.js +188 -0
  21. package/indexFallback.test.js +33 -0
  22. package/indexServe.test.js +59 -0
  23. package/indexWorkers.test.js +79 -0
  24. package/lhm.plugin.json +3218 -0
  25. package/llms-full.txt +6 -7
  26. package/llms.txt +1 -1
  27. package/mcp.js +2 -2
  28. package/mcp.test.js +281 -0
  29. package/openapi.yaml +1 -1
  30. package/package.json +10 -5
  31. package/routing.test.js +13 -3
  32. package/scripts/generate-openapi.js +1 -1
  33. package/test/fixtures/buildFixtures.js +83 -0
  34. package/test/fixtures/db/reports/260101T0008-mul.json +137 -0
  35. package/test/fixtures/db/reports/260101T0009-brd.json +147 -0
  36. package/test/setup.cjs +14 -0
  37. package/util.js +16 -6
  38. package/util.test.js +886 -3
  39. package/web/ai0BalanceForm/ai0BalanceForm.test.js +88 -0
  40. package/web/expungeReportsForm/expungeReportsForm.test.js +114 -0
  41. package/web/hideReportForm/hideReportForm.test.js +82 -0
  42. package/web/listDiagnoses/listDiagnoses.test.js +160 -0
  43. package/web/listIssues/listIssues.test.js +109 -0
  44. package/web/listReports/listReports.test.js +139 -3
  45. package/web/listRules/listRules.test.js +103 -0
  46. package/web/listTopIssues/listTopIssues.test.js +126 -0
  47. package/web/listViolators/index.js +1 -2
  48. package/web/listViolators/listViolators.test.js +256 -0
  49. package/web/pruneReportsForm/pruneReportsForm.test.js +86 -0
  50. package/web/reannotate/reannotate.test.js +144 -0
  51. package/web/reannotateForm/reannotateForm.test.js +134 -0
  52. package/web/renewWCAG/renewWCAG.test.js +124 -0
  53. package/web/requestRetest/requestRetest.test.js +119 -0
  54. package/web/requestRetestForm/requestRetestForm.test.js +62 -0
  55. package/web/requestTest/requestTest.test.js +115 -0
  56. package/web/requestTestForm/requestTestForm.test.js +40 -0
  57. package/web/rewindReportsForm/rewindReportsForm.test.js +86 -0
  58. package/web/tutorial/index.html +3 -3
  59. package/web/tutorial/tutorial.test.js +97 -0
  60. package/web/unhideReportForm/unhideReportForm.test.js +135 -0
@@ -0,0 +1,86 @@
1
+ /*
2
+ pruneReportsForm.test.js
3
+ Unit tests for web/pruneReportsForm/index.js.
4
+ */
5
+
6
+ // IMPORTS
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');
15
+
16
+ // SETUP AND TEARDOWN
17
+
18
+ const savedAuthCode = process.env.AUTH_CODE;
19
+ const savedDBDir = process.env.DB_DIR;
20
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
21
+
22
+ before(() => {
23
+ process.env.AUTH_CODE = 'test-auth-code';
24
+ process.env.DB_DIR = fixtureDBDir;
25
+ });
26
+
27
+ after(async () => {
28
+ if (savedAuthCode !== undefined) {
29
+ process.env.AUTH_CODE = savedAuthCode;
30
+ }
31
+ if (savedDBDir !== undefined) {
32
+ process.env.DB_DIR = savedDBDir;
33
+ }
34
+ });
35
+
36
+ // TESTS
37
+
38
+ test('pruneReportsForm displays a list of reports when no submission', async () => {
39
+ const result = await answer(null, '');
40
+ assert.equal(result.status, 'ok');
41
+ assert.ok(result.answerPage);
42
+ const html = parse(result.answerPage);
43
+ assert.ok(html.querySelector('title'));
44
+ });
45
+
46
+ test('pruneReportsForm returns an error for an invalid auth code', async () => {
47
+ const result = await answer(null, 'authCode=wrong&report=260101T0000-mix');
48
+ assert.equal(result.status, 'error');
49
+ assert.equal(result.message, 'Invalid authorization code');
50
+ });
51
+
52
+ test('pruneReportsForm returns an error when deleting a nonexistent report', async () => {
53
+ const result = await answer(null, 'authCode=test-auth-code&report=999999T9999-nope');
54
+ assert.equal(result.status, 'error');
55
+ assert.ok(result.message.includes('Deleting superseded reports'));
56
+ });
57
+
58
+ test('pruneReportsForm deletes a superseded report with valid auth code', async () => {
59
+ // 260101T0000-mix is superseded by 260202T0000-new (same URL).
60
+ const reportPath = path.join(reportsPath(), '260101T0000-mix.json');
61
+ const backup = await fs.readFile(reportPath, 'utf8');
62
+ try {
63
+ const result = await answer(null, 'authCode=test-auth-code&report=260101T0000-mix');
64
+ assert.equal(result.status, 'ok');
65
+ await fs.access(reportPath).then(
66
+ () => {throw new Error('Report should have been deleted');},
67
+ () => {}
68
+ );
69
+ }
70
+ finally {
71
+ await fs.writeFile(reportPath, backup);
72
+ }
73
+ });
74
+
75
+ test('pruneReportsForm returns an error when a report file is corrupt', async () => {
76
+ const reportPath = path.join(reportsPath(), '260101T0001-ct.json');
77
+ const backup = await fs.readFile(reportPath, 'utf8');
78
+ try {
79
+ await fs.writeFile(reportPath, 'not valid json');
80
+ const result = await answer(null, '');
81
+ assert.equal(result.status, 'error');
82
+ }
83
+ finally {
84
+ await fs.writeFile(reportPath, backup);
85
+ }
86
+ });
@@ -0,0 +1,144 @@
1
+ /*
2
+ reannotate.test.js
3
+ Unit tests for web/reannotate/index.js, covering the reannotation logic branches.
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 os = require('node:os');
12
+ const path = require('node:path');
13
+ const {parse} = require('node-html-parser');
14
+
15
+ // CONSTANTS
16
+
17
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
18
+ const reportsDir = path.join(fixtureDBDir, 'reports');
19
+
20
+ // SETUP AND TEARDOWN
21
+
22
+ before(async () => {
23
+ process.env.DB_DIR = fixtureDBDir;
24
+ process.env.AUTH_CODE = 'test-auth-code';
25
+ });
26
+
27
+ beforeEach(async () => {
28
+ // Ensure DB_DIR points to the fixture DB for each test.
29
+ process.env.DB_DIR = fixtureDBDir;
30
+ });
31
+
32
+ after(async () => {
33
+ // Restore DB_DIR to the fixture DB.
34
+ process.env.DB_DIR = fixtureDBDir;
35
+ });
36
+
37
+ // HELPER
38
+
39
+ // Backs up all report files and returns a map of fileName to content.
40
+ const backupReports = async () => {
41
+ const reportFiles = await fs.readdir(reportsDir);
42
+ const backups = {};
43
+ for (const file of reportFiles) {
44
+ backups[file] = await fs.readFile(path.join(reportsDir, file), 'utf8');
45
+ }
46
+ return backups;
47
+ };
48
+
49
+ // Restores all report files from a backup map.
50
+ const restoreReports = async backups => {
51
+ for (const [file, content] of Object.entries(backups)) {
52
+ await fs.writeFile(path.join(reportsDir, file), content);
53
+ }
54
+ };
55
+
56
+ // TESTS
57
+
58
+ test('answer returns an error for an invalid authorization code', async () => {
59
+ const {answer} = require('./index');
60
+ const result = await answer('wrong-code');
61
+ assert.equal(result.status, 'error');
62
+ assert.equal(result.message, 'Invalid authorization code');
63
+ });
64
+
65
+ test('answer returns an error when no reports are available', async () => {
66
+ // Create a temporary DB directory with an empty reports directory.
67
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'reannotate-test-'));
68
+ await fs.mkdir(path.join(tempDir, 'reports'));
69
+ const originalDBDir = process.env.DB_DIR;
70
+ process.env.DB_DIR = tempDir;
71
+ try {
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');
75
+ const result = await answer('test-auth-code');
76
+ assert.equal(result.status, 'error');
77
+ assert.equal(result.message, 'Got data on no available reports');
78
+ }
79
+ finally {
80
+ process.env.DB_DIR = originalDBDir;
81
+ try {
82
+ await fs.rm(tempDir, {recursive: true});
83
+ }
84
+ catch {
85
+ // Ignore cleanup errors for the temp directory.
86
+ }
87
+ }
88
+ });
89
+
90
+ test('answer returns an error when a report fails annotation', async () => {
91
+ // Back up all fixture reports, because annotateReport modifies good reports
92
+ // in place before it reaches the bad one and returns an error.
93
+ const backups = await backupReports();
94
+ // Create a temporary report file that passes getReportExtract but fails
95
+ // getReport validation (acts is not an array).
96
+ const badReportName = '990101T0000-bad.json';
97
+ const badReportPath = path.join(reportsDir, badReportName);
98
+ const badReport = {
99
+ id: '990101T0000-bad',
100
+ target: {what: 'Bad Report', url: 'https://example.com/bad'},
101
+ acts: 'not-an-array',
102
+ jobData: {endTime: '26-01-01T00:00'},
103
+ catalog: {}
104
+ };
105
+ await fs.writeFile(badReportPath, JSON.stringify(badReport));
106
+ try {
107
+ delete require.cache[require.resolve('./index')];
108
+ const {answer} = require('./index');
109
+ const result = await answer('test-auth-code');
110
+ assert.equal(result.status, 'error');
111
+ assert.ok(result.message.includes('invalid'), `Expected message about invalid report, got: ${result.message}`);
112
+ }
113
+ finally {
114
+ try {
115
+ await fs.unlink(badReportPath);
116
+ }
117
+ catch {
118
+ // Ignore cleanup errors for the temporary bad report.
119
+ }
120
+ // Restore all fixture reports modified by annotateReport.
121
+ await restoreReports(backups);
122
+ }
123
+ });
124
+
125
+ test('answer returns ok with an answer page when all reports annotate successfully', async () => {
126
+ // Back up all fixture reports, because annotateReport modifies them in place.
127
+ const backups = await backupReports();
128
+ try {
129
+ delete require.cache[require.resolve('./index')];
130
+ const {answer} = require('./index');
131
+ const result = await answer('test-auth-code');
132
+ assert.equal(result.status, 'ok');
133
+ assert.ok(result.answerPage, 'answerPage should be present');
134
+ // Verify the HTML is well-formed.
135
+ const doc = parse(result.answerPage);
136
+ const h1 = doc.querySelector('h1');
137
+ assert.ok(h1);
138
+ assert.ok(h1.textContent.includes('Reannotation order'));
139
+ }
140
+ finally {
141
+ // Restore all fixture reports.
142
+ await restoreReports(backups);
143
+ }
144
+ });
@@ -0,0 +1,134 @@
1
+ /*
2
+ reannotateForm.test.js
3
+ Unit tests for web/reannotateForm/index.js, covering error, no-reclassified,
4
+ and success branches of answer and populateQuery.
5
+ */
6
+
7
+ // IMPORTS
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');
15
+
16
+ // CONSTANTS
17
+
18
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
19
+ const reportsDir = path.join(fixtureDBDir, 'reports');
20
+ const emptyReportName = '260101T0005-emp.json';
21
+
22
+ // SETUP AND TEARDOWN
23
+
24
+ before(async () => {
25
+ process.env.DB_DIR = fixtureDBDir;
26
+ process.env.AUTH_CODE = 'test-auth-code';
27
+ });
28
+
29
+ beforeEach(async () => {
30
+ // Ensure DB_DIR points to the fixture DB for each test.
31
+ process.env.DB_DIR = fixtureDBDir;
32
+ });
33
+
34
+ after(async () => {
35
+ // Restore DB_DIR to the fixture DB.
36
+ process.env.DB_DIR = fixtureDBDir;
37
+ });
38
+
39
+ // TESTS
40
+
41
+ test('answer returns ok with reclassified rules listed in the page', async () => {
42
+ // The fixture reports have instances whose issueIDs differ from the
43
+ // current rule specs, so reclassified rules should be listed.
44
+ delete require.cache[require.resolve('./index')];
45
+ const {answer} = require('./index');
46
+ const result = await answer();
47
+ assert.equal(result.status, 'ok');
48
+ assert.ok(result.answerPage, 'answerPage should be present');
49
+ // Verify the placeholders were replaced.
50
+ assert.ok(!result.answerPage.includes('__how__'));
51
+ assert.ok(!result.answerPage.includes('__reannotateForm__'));
52
+ assert.ok(!result.answerPage.includes('__stillUnclassified__'));
53
+ assert.ok(!result.answerPage.includes('__reClassified__'));
54
+ // Verify the how text mentions reclassified rules.
55
+ assert.ok(result.answerPage.includes('reclassified'));
56
+ // Verify the HTML is well-formed.
57
+ const doc = parse(result.answerPage);
58
+ const h1 = doc.querySelector('h1');
59
+ assert.ok(h1);
60
+ assert.ok(h1.textContent.includes('Reannotation order'));
61
+ // Verify the form is present.
62
+ const form = doc.querySelector('form');
63
+ assert.ok(form);
64
+ assert.equal(form.getAttribute('action'), '/reannotate.html');
65
+ });
66
+
67
+ test('answer returns ok with no-reclassified message when reports have no violations', async () => {
68
+ // Create a temporary DB directory with only the empty report (no instances).
69
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'reannotateForm-test-'));
70
+ const tempReportsDir = path.join(tempDir, 'reports');
71
+ await fs.mkdir(tempReportsDir);
72
+ // Copy the empty report (no standard instances) to the temp directory.
73
+ const emptyReportContent = await fs.readFile(path.join(reportsDir, emptyReportName), 'utf8');
74
+ await fs.writeFile(path.join(tempReportsDir, emptyReportName), emptyReportContent);
75
+ const originalDBDir = process.env.DB_DIR;
76
+ process.env.DB_DIR = tempDir;
77
+ try {
78
+ delete require.cache[require.resolve('./index')];
79
+ const {answer} = require('./index');
80
+ const result = await answer();
81
+ assert.equal(result.status, 'ok');
82
+ assert.ok(result.answerPage, 'answerPage should be present');
83
+ // Verify the how text says reannotation is normally not necessary.
84
+ assert.ok(
85
+ result.answerPage.includes('normally not necessary'),
86
+ 'Expected the no-reclassified message in the answer page'
87
+ );
88
+ // Verify no reclassified list items are present.
89
+ assert.ok(!result.answerPage.includes('reclassified</q> rule indicates'));
90
+ // Verify the HTML is well-formed.
91
+ const doc = parse(result.answerPage);
92
+ const form = doc.querySelector('form');
93
+ assert.ok(form);
94
+ }
95
+ finally {
96
+ process.env.DB_DIR = originalDBDir;
97
+ try {
98
+ await fs.rm(tempDir, {recursive: true});
99
+ }
100
+ catch {
101
+ // Ignore cleanup errors for the temp directory.
102
+ }
103
+ }
104
+ });
105
+
106
+ test('answer returns an error when a report fails to load', async () => {
107
+ // Create a temporary report file that passes getReportExtract but fails
108
+ // getReport validation (acts is not an array).
109
+ const badReportName = '990101T0000-bad.json';
110
+ const badReportPath = path.join(reportsDir, badReportName);
111
+ const badReport = {
112
+ id: '990101T0000-bad',
113
+ target: {what: 'Bad Report', url: 'https://example.com/bad'},
114
+ acts: 'not-an-array',
115
+ jobData: {endTime: '26-01-01T00:00'},
116
+ catalog: {}
117
+ };
118
+ await fs.writeFile(badReportPath, JSON.stringify(badReport));
119
+ try {
120
+ delete require.cache[require.resolve('./index')];
121
+ const {answer} = require('./index');
122
+ const result = await answer();
123
+ assert.equal(result.status, 'error');
124
+ assert.ok(result.message.includes('invalid'), `Expected message about invalid report, got: ${result.message}`);
125
+ }
126
+ finally {
127
+ try {
128
+ await fs.unlink(badReportPath);
129
+ }
130
+ catch {
131
+ // Ignore cleanup errors for the temporary bad report.
132
+ }
133
+ }
134
+ });
@@ -0,0 +1,124 @@
1
+ /*
2
+ renewWCAG.test.js
3
+ Unit tests for web/renewWCAG/index.js, covering auth, fetch status, and success branches.
4
+ */
5
+
6
+ // IMPORTS
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');
13
+
14
+ // CONSTANTS
15
+
16
+ const fixtureDBDir = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
17
+ const wcagMapPath = path.join(__dirname, '..', '..', 'wcagMap.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
+ let originalFetch;
27
+ let wcagMapBackup;
28
+
29
+ afterEach(async () => {
30
+ // Restore the original fetch after each test.
31
+ if (originalFetch !== undefined) {
32
+ global.fetch = originalFetch;
33
+ originalFetch = undefined;
34
+ }
35
+ // Restore the wcagMap.json file if it was backed up.
36
+ if (wcagMapBackup !== undefined) {
37
+ await fs.writeFile(wcagMapPath, wcagMapBackup);
38
+ wcagMapBackup = undefined;
39
+ }
40
+ });
41
+
42
+ after(async () => {
43
+ // Ensure fetch is restored.
44
+ if (originalFetch !== undefined) {
45
+ global.fetch = originalFetch;
46
+ originalFetch = undefined;
47
+ }
48
+ });
49
+
50
+ // HELPER
51
+
52
+ // Replaces global.fetch with a mock that returns the given status and body.
53
+ const mockFetch = (status, body = '') => {
54
+ originalFetch = global.fetch;
55
+ // @ts-expect-error: Replacing the real function with a mock for testing.
56
+ global.fetch = async () => ({
57
+ status,
58
+ text: async () => body
59
+ });
60
+ };
61
+
62
+ // TESTS
63
+
64
+ test('answer returns an error for an invalid authorization code', async () => {
65
+ const {answer} = require('./index');
66
+ const result = await answer('wrong-code');
67
+ assert.equal(result.status, 'error');
68
+ assert.equal(result.message, 'Authorization code invalid');
69
+ });
70
+
71
+ test('answer returns an error when the WCAG map source returns a non-200 status', async () => {
72
+ mockFetch(404);
73
+ delete require.cache[require.resolve('./index')];
74
+ const {answer} = require('./index');
75
+ const result = await answer('test-auth-code');
76
+ assert.equal(result.status, 'error');
77
+ assert.equal(result.message, 'WCAG map source not retrieved');
78
+ });
79
+
80
+ test('answer returns ok and rewrites the WCAG map when the source returns valid entries', async () => {
81
+ // Back up the existing wcagMap.json before the test overwrites it.
82
+ wcagMapBackup = await fs.readFile(wcagMapPath, 'utf8');
83
+ // Create mock HTML with entries matching the regex in index.js.
84
+ const mockHTML = [
85
+ '<a href="understanding/contrast-minimum"><span class="secno">1.4.3 </span>',
86
+ '<a href="understanding/non-text-content"><span class="secno">1.1.1 </span>',
87
+ '<a href="understanding/target-size-minimum"><span class="secno">2.5.8 </span>'
88
+ ].join('\n');
89
+ mockFetch(200, mockHTML);
90
+ delete require.cache[require.resolve('./index')];
91
+ const {answer} = require('./index');
92
+ const result = await answer('test-auth-code');
93
+ assert.equal(result.status, 'ok');
94
+ assert.ok(result.answerPage, 'answerPage should be present');
95
+ // Verify the HTML is well-formed.
96
+ const doc = parse(result.answerPage);
97
+ const h1 = doc.querySelector('h1');
98
+ assert.ok(h1);
99
+ assert.ok(h1.textContent.includes('WCAG map renewed'));
100
+ // Verify the wcagMap.json was updated with the entries.
101
+ const updatedMap = JSON.parse(await fs.readFile(wcagMapPath, 'utf8'));
102
+ assert.equal(updatedMap['1.4.3'], 'understanding/contrast-minimum');
103
+ assert.equal(updatedMap['1.1.1'], 'understanding/non-text-content');
104
+ assert.equal(updatedMap['2.5.8'], 'understanding/target-size-minimum');
105
+ });
106
+
107
+ test('answer returns an error when matchAll returns no entries', async () => {
108
+ // Mock matchAll to return null, covering the dead else branch on lines 48-55.
109
+ const originalMatchAll = String.prototype.matchAll;
110
+ String.prototype.matchAll = function() {
111
+ return null;
112
+ };
113
+ mockFetch(200, '<html></html>');
114
+ try {
115
+ delete require.cache[require.resolve('./index')];
116
+ const {answer} = require('./index');
117
+ const result = await answer('test-auth-code');
118
+ assert.equal(result.status, 'error');
119
+ assert.equal(result.message, 'No entries found in WCAG map source');
120
+ }
121
+ finally {
122
+ String.prototype.matchAll = originalMatchAll;
123
+ }
124
+ });
@@ -0,0 +1,119 @@
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
+ });
@@ -0,0 +1,62 @@
1
+ /*
2
+ requestRetestForm.test.js
3
+ Unit tests for web/requestRetestForm/index.js.
4
+ */
5
+
6
+ // IMPORTS
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');
13
+
14
+ // SETUP AND TEARDOWN
15
+
16
+ const savedDBDir = process.env.DB_DIR;
17
+
18
+ before(() => {
19
+ process.env.DB_DIR = path.join(__dirname, '..', '..', 'test', 'fixtures', 'db');
20
+ });
21
+
22
+ after(() => {
23
+ if (savedDBDir !== undefined) {
24
+ process.env.DB_DIR = savedDBDir;
25
+ }
26
+ else {
27
+ delete process.env.DB_DIR;
28
+ }
29
+ });
30
+
31
+ // TESTS
32
+
33
+ test('requestRetestForm returns ok with the page name for a valid report', async () => {
34
+ const result = await answer('260202T0000/new');
35
+ assert.equal(result.status, 'ok');
36
+ assert.ok(result.answerPage);
37
+ const html = parse(result.answerPage);
38
+ const title = html.querySelector('title').text;
39
+ assert.ok(title.includes('Mixed Outcomes Page'));
40
+ });
41
+
42
+ test('requestRetestForm populates the form action with the timeStamp and jobID', async () => {
43
+ const result = await answer('260202T0000/new');
44
+ const html = parse(result.answerPage);
45
+ const form = html.querySelector('form');
46
+ assert.equal(form.getAttribute('action'), '/requestRetest.html/260202T0000/new');
47
+ });
48
+
49
+ test('requestRetestForm shows the fallback message for a nonexistent report', async () => {
50
+ const result = await answer('999999T9999/nope');
51
+ assert.equal(result.status, 'ok');
52
+ assert.ok(result.answerPage.includes('The specified page is not available for retesting'));
53
+ });
54
+
55
+ test('requestRetestForm includes the ago and dateTime placeholders replaced', async () => {
56
+ const result = await answer('260202T0000/new');
57
+ assert.ok(!result.answerPage.includes('__ago__'));
58
+ assert.ok(!result.answerPage.includes('__dateTime__'));
59
+ assert.ok(!result.answerPage.includes('__target__'));
60
+ assert.ok(!result.answerPage.includes('__timeStamp__'));
61
+ assert.ok(!result.answerPage.includes('__jobID__'));
62
+ });