@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.
- package/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +5 -1
- package/.github/workflows/ci.yml +0 -3
- package/.github/workflows/periodic-smoke-tests.yml +30 -0
- package/.github/workflows/publish-mcp.yml +4 -4
- package/{alerts.test.js → alerts.test.ts} +17 -29
- package/{alerts.js → alerts.ts} +10 -13
- package/api/{getReport.test.js → getReport.test.ts} +9 -10
- package/api/{getReport.js → getReport.ts} +23 -24
- package/api/{listDiagnoses.test.js → listDiagnoses.test.ts} +26 -27
- package/api/{listDiagnoses.js → listDiagnoses.ts} +46 -70
- package/api/{listIssues.test.js → listIssues.test.ts} +21 -14
- package/api/{listIssues.js → listIssues.ts} +75 -74
- package/api/{listReports.test.js → listReports.test.ts} +17 -16
- package/api/listReports.ts +87 -0
- package/api/{listViolators.test.js → listViolators.test.ts} +19 -20
- package/api/{listViolators.js → listViolators.ts} +45 -70
- package/api/requestFeature.test.ts +54 -0
- package/api/{requestFeature.js → requestFeature.ts} +18 -15
- package/api/requestRetest.test.ts +144 -0
- package/api/requestRetest.ts +119 -0
- package/api/requestTest.test.ts +153 -0
- package/api/requestTest.ts +123 -0
- package/api/{routes.test.js → routes.test.ts} +5 -5
- package/api/{routes.js → routes.ts} +6 -6
- package/api/{schemas.js → schemas.ts} +69 -50
- package/api/{util.test.js → util.test.ts} +35 -14
- package/api/{util.js → util.ts} +54 -51
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +12 -13
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +360 -0
- package/docs/SERVICE.md +170 -17
- package/docs/decisions/002-coverage-tool.md +40 -0
- package/docs/decisions/003-adr-threshold.md +34 -0
- package/docs/decisions/004-docs-directory.md +23 -0
- package/docs/decisions/005-observability.md +28 -0
- package/docs/decisions/006-git-workflow.md +28 -0
- package/docs/decisions/007-internal-testing.md +34 -0
- package/docs/decisions/008-dependencies.md +28 -0
- package/docs/decisions/009-qai-integration.md +40 -0
- package/docs/decisions/QAI-HISTORICAL-CONTEXT.md +42 -0
- package/eslint.config.mjs +27 -0
- package/index.html +3 -3
- package/{index.test.js → index.test.ts} +397 -152
- package/{index.js → index.ts} +386 -285
- package/{indexBalances.test.js → indexBalances.test.ts} +21 -17
- package/{indexServe.test.js → indexServe.test.ts} +9 -9
- package/linkValidation.test.ts +190 -0
- package/{mcp.test.js → mcp.test.ts} +53 -26
- package/{mcp.js → mcp.ts} +29 -20
- package/memory/MEMORY.md +7 -0
- package/memory/user_maintainer_context.md +24 -0
- package/openapi.yaml +115 -68
- package/package.json +35 -15
- package/pm2.config.cjs +29 -0
- package/{routes.test.js → routes.test.ts} +8 -7
- package/{routing.test.js → routing.test.ts} +11 -12
- package/scripts/{generate-openapi.js → generate-openapi.ts} +13 -13
- package/serve.ts +15 -0
- package/smokeTest.ts +234 -0
- package/test/dbFixture.ts +21 -0
- package/test/fixtures/{buildFixtures.js → buildFixtures.ts} +14 -13
- package/test/{setup.cjs → setup.ts} +2 -2
- package/tsconfig.json +16 -0
- package/util.test.ts +1129 -0
- package/util.ts +1017 -0
- package/web/ai0BalanceForm/{ai0BalanceForm.test.js → ai0BalanceForm.test.ts} +15 -15
- package/web/ai0BalanceForm/{index.js → index.ts} +10 -18
- package/web/enqueue/index.ts +56 -0
- package/web/enqueueForm/index.html +9 -9
- package/web/enqueueForm/index.ts +43 -0
- package/web/expungeReportsForm/{expungeReportsForm.test.js → expungeReportsForm.test.ts} +18 -20
- package/web/expungeReportsForm/index.ts +18 -0
- package/web/hideReportForm/{hideReportForm.test.js → hideReportForm.test.ts} +14 -14
- package/web/hideReportForm/{index.js → index.ts} +17 -21
- package/web/listDiagnoses/{index.js → index.ts} +48 -58
- package/web/listDiagnoses/{listDiagnoses.test.js → listDiagnoses.test.ts} +39 -34
- package/web/listIssues/{index.js → index.ts} +67 -92
- package/web/listIssues/{listIssues.test.js → listIssues.test.ts} +45 -51
- package/web/listReports/index.html +3 -3
- package/web/listReports/{index.js → index.ts} +74 -58
- package/web/listReports/{listReports.test.js → listReports.test.ts} +41 -43
- package/web/listRules/{index.js → index.ts} +18 -23
- package/web/listRules/{listRules.test.js → listRules.test.ts} +26 -19
- package/web/listTopIssues/{index.js → index.ts} +36 -47
- package/web/listTopIssues/{listTopIssues.test.js → listTopIssues.test.ts} +26 -17
- package/web/listViolators/{index.js → index.ts} +51 -71
- package/web/listViolators/{listViolators.test.js → listViolators.test.ts} +43 -36
- package/web/manage/index.ts +22 -0
- package/web/pruneReportsForm/index.ts +18 -0
- package/web/pruneReportsForm/{pruneReportsForm.test.js → pruneReportsForm.test.ts} +11 -11
- package/web/reannotate/index.ts +71 -0
- package/web/reannotate/{reannotate.test.js → reannotate.test.ts} +19 -22
- package/web/reannotateForm/{index.js → index.ts} +41 -50
- package/web/reannotateForm/{reannotateForm.test.js → reannotateForm.test.ts} +13 -16
- package/web/renewWCAG/{index.js → index.ts} +9 -9
- package/web/renewWCAG/{renewWCAG.test.js → renewWCAG.test.ts} +17 -20
- package/web/renewWCAGForm/index.ts +22 -0
- package/web/{expungeReportsForm/index.js → reportDeletion.ts} +57 -39
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +62 -0
- package/web/requestRetest/requestRetest.test.ts +130 -0
- package/web/requestRetestForm/{index.js → index.ts} +10 -16
- package/web/requestRetestForm/{requestRetestForm.test.js → requestRetestForm.test.ts} +11 -12
- package/web/requestTest/index.html +7 -7
- package/web/requestTest/index.ts +52 -0
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +39 -24
- package/web/requestTestForm/index.html +4 -4
- package/web/requestTestForm/index.ts +21 -0
- package/web/requestTestForm/{requestTestForm.test.js → requestTestForm.test.ts} +9 -9
- package/web/rewindReportsForm/index.ts +18 -0
- package/web/rewindReportsForm/{rewindReportsForm.test.js → rewindReportsForm.test.ts} +11 -11
- package/web/tutorialAI/index.html +184 -0
- package/web/tutorialAI/index.ts +80 -0
- package/web/tutorialAI/tutorialAI.test.ts +192 -0
- package/web/{tutorial → tutorialWeb}/index.html +9 -8
- package/web/tutorialWeb/index.ts +78 -0
- package/web/tutorialWeb/tutorialWeb.test.ts +195 -0
- package/web/unhideReportForm/{index.js → index.ts} +18 -22
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -89
- package/api/requestFeature.test.js +0 -58
- package/api/requestRetest.js +0 -96
- package/api/requestRetest.test.js +0 -80
- package/api/requestTest.js +0 -106
- package/api/requestTest.test.js +0 -92
- package/api/version.js +0 -8
- package/indexFallback.test.js +0 -33
- package/indexWorkers.test.js +0 -79
- package/jsconfig.json +0 -8
- package/pm2.config.js +0 -18
- package/smokeTest.js +0 -128
- package/util.js +0 -744
- package/util.test.js +0 -968
- package/web/enqueue/index.js +0 -67
- package/web/enqueueForm/index.js +0 -49
- package/web/manage/index.js +0 -22
- package/web/pruneReportsForm/index.js +0 -123
- package/web/reannotate/index.js +0 -58
- package/web/renewWCAGForm/index.js +0 -22
- package/web/requestRetest/index.js +0 -31
- package/web/requestRetest/requestRetest.test.js +0 -119
- package/web/requestTest/index.js +0 -24
- package/web/requestTestForm/index.js +0 -21
- package/web/rewindReportsForm/index.js +0 -121
- package/web/tutorial/index.js +0 -63
- package/web/tutorial/tutorial.test.js +0 -97
- /package/test/fixtures/db/jobs/{recs.json → testRequests.json} +0 -0
- /package/web/{tutorial → tutorialWeb}/images/newsletter-form.png +0 -0
|
@@ -1,46 +1,49 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listViolators.test.
|
|
3
|
-
UI tests for web/listViolators/index.
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
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.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
};
|
|
37
|
+
} as any);
|
|
35
38
|
|
|
36
|
-
const {answer} =
|
|
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 =
|
|
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
|
|
78
|
-
const result = await answer('
|
|
79
|
-
assert.equal(result.status, '
|
|
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 =
|
|
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 =
|
|
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
|
|
262
|
-
|
|
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.
|
|
3
|
-
Unit tests for web/pruneReportsForm/index.
|
|
2
|
+
pruneReportsForm.test.ts
|
|
3
|
+
Unit tests for web/pruneReportsForm/index.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
3
|
-
Unit tests for web/reannotate/index.
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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} =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
124
|
+
// Back up all fixture reports, because reannotation modifies them in place.
|
|
127
125
|
const backups = await backupReports();
|
|
128
126
|
try {
|
|
129
|
-
|
|
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.
|
|
2
|
+
index.ts
|
|
3
3
|
Serves a form to reannotate reports.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 (
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
-
Unit tests for web/reannotateForm/index.
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
121
|
+
assert.ok(result.message.includes('not usable'), `Expected message about unusable report, got: ${result.message}`);
|
|
125
122
|
}
|
|
126
123
|
finally {
|
|
127
124
|
try {
|