@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.
- package/.claude/settings.local.json +13 -1
- package/.devin/config.local.json +8 -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.js → requestRetest.test.ts} +33 -28
- package/api/{requestRetest.js → requestRetest.ts} +32 -25
- package/api/{requestTest.test.js → requestTest.test.ts} +35 -30
- package/api/{requestTest.js → requestTest.ts} +35 -28
- 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} +31 -11
- package/api/{util.js → util.ts} +75 -46
- package/api/version.ts +8 -0
- package/{apiIntegration.test.js → apiIntegration.test.ts} +13 -14
- package/balances.ts +87 -0
- package/{checkinfo.js → checkinfo.ts} +24 -20
- package/docs/AI-TOOLS.md +4 -4
- package/docs/BACKLOG.md +356 -0
- package/docs/SERVICE.md +144 -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} +462 -137
- package/{index.js → index.ts} +388 -283
- 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.js → util.test.ts} +350 -257
- package/util.ts +921 -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} +48 -53
- 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 +70 -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} +56 -38
- package/web/requestRetest/index.html +7 -7
- package/web/requestRetest/index.ts +31 -0
- package/web/requestRetest/requestRetest.test.ts +116 -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.js → index.ts} +5 -5
- package/web/requestTest/{requestTest.test.js → requestTest.test.ts} +21 -21
- 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} +17 -21
- package/web/unhideReportForm/{unhideReportForm.test.js → unhideReportForm.test.ts} +16 -16
- package/aceconfig.js +0 -28
- package/api/listReports.js +0 -87
- package/api/requestFeature.test.js +0 -58
- 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 -126
- package/util.js +0 -733
- 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/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,21 +1,20 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listViolators.test.
|
|
3
|
-
Tests for api/listViolators.
|
|
2
|
+
listViolators.test.ts
|
|
3
|
+
Tests for api/listViolators.ts using the fixture corpus, with emphasis on outcome handling.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const {response} = require('./listViolators');
|
|
8
|
+
import {test, before, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {response} from './listViolators.ts';
|
|
12
11
|
|
|
13
12
|
// SETUP AND TEARDOWN
|
|
14
13
|
|
|
15
14
|
const savedDBDir = process.env.DB_DIR;
|
|
16
15
|
|
|
17
|
-
before(() => {
|
|
18
|
-
process.env.DB_DIR =
|
|
16
|
+
before(async () => {
|
|
17
|
+
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
19
18
|
});
|
|
20
19
|
|
|
21
20
|
after(() => {
|
|
@@ -31,7 +30,7 @@ after(() => {
|
|
|
31
30
|
|
|
32
31
|
test('listViolators returns 1 violator for linkNoText in the mixed report, excluding cantTell', async () => {
|
|
33
32
|
const body = await response(['linkNoText', '260101T0000', 'mix']);
|
|
34
|
-
const violators = body['response content']['basics about all elements exhibiting the issue'];
|
|
33
|
+
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
35
34
|
assert.equal(violators.length, 1);
|
|
36
35
|
assert.equal(violators[0].identifier, '0');
|
|
37
36
|
assert.equal(violators[0]['tag name'], 'A');
|
|
@@ -41,40 +40,40 @@ test('listViolators returns 1 violator for linkNoText in the mixed report, exclu
|
|
|
41
40
|
|
|
42
41
|
test('listViolators returns 0 violators for an issue when all instances are cantTell', async () => {
|
|
43
42
|
const body = await response(['focusIndicationBad', '260101T0001', 'ct']);
|
|
44
|
-
const violators = body['response content']['basics about all elements exhibiting the issue'];
|
|
43
|
+
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
45
44
|
assert.equal(violators.length, 0);
|
|
46
45
|
});
|
|
47
46
|
|
|
48
47
|
test('listViolators treats missing outcome as a violation', async () => {
|
|
49
48
|
const body = await response(['linkNoText', '260101T0002', 'no']);
|
|
50
|
-
const violators = body['response content']['basics about all elements exhibiting the issue'];
|
|
49
|
+
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
51
50
|
assert.equal(violators.length, 1);
|
|
52
51
|
assert.equal(violators[0].identifier, '0');
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
test('listViolators returns an error for an unknown issue ID', async () => {
|
|
56
55
|
const body = await response(['nonexistentIssue', '260101T0000', 'mix']);
|
|
57
|
-
const basics = body['response content']['basics about the issue'];
|
|
56
|
+
const basics = body['response content']['basics about the issue'] as any;
|
|
58
57
|
assert.ok(basics.error);
|
|
59
58
|
});
|
|
60
59
|
|
|
61
60
|
test('listViolators returns an error for a nonexistent report', async () => {
|
|
62
61
|
const body = await response(['linkNoText', '999999T9999', 'xyz']);
|
|
63
|
-
const basics = body['response content']['basics about the report'];
|
|
62
|
+
const basics = body['response content']['basics about the report'] as any;
|
|
64
63
|
assert.ok(basics.error);
|
|
65
64
|
});
|
|
66
65
|
|
|
67
66
|
test('listViolators includes reporter facts for the issue', async () => {
|
|
68
67
|
const body = await response(['linkNoText', '260101T0000', 'mix']);
|
|
69
|
-
const details = body['response content']['details about the issue'];
|
|
68
|
+
const details = body['response content']['details about the issue'] as any;
|
|
70
69
|
const reporters = details['rule engines reporting violations belonging to the issue'];
|
|
71
|
-
const names = reporters.map(r => r.name).sort();
|
|
70
|
+
const names = reporters.map((r: any) => r.name).sort();
|
|
72
71
|
assert.deepEqual(names, ['Alfa', 'Axe']);
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
test('listViolators sorts by catalogIndex when two violators have the same reporter count', async () => {
|
|
76
75
|
const body = await response(['linkNoText', '260101T0008', 'mul']);
|
|
77
|
-
const violators = body['response content']['basics about all elements exhibiting the issue'];
|
|
76
|
+
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
78
77
|
assert.equal(violators.length, 3);
|
|
79
78
|
// The violator with 2 reporters (catalogIndex 2) sorts first.
|
|
80
79
|
assert.equal(violators[0].identifier, '2');
|
|
@@ -88,14 +87,14 @@ test('listViolators sorts by catalogIndex when two violators have the same repor
|
|
|
88
87
|
|
|
89
88
|
test('listViolators returns guideline layer for an issue with a short WCAG code', async () => {
|
|
90
89
|
const body = await response(['duplicateID', '260101T0009', 'brd']);
|
|
91
|
-
const issueBasics = body['response content']['basics about the issue'];
|
|
90
|
+
const issueBasics = body['response content']['basics about the issue'] as any;
|
|
92
91
|
assert.equal(issueBasics['related WCAG standard'].layer, 'guideline');
|
|
93
92
|
});
|
|
94
93
|
|
|
95
94
|
test('listViolators returns null tag name and text for a violator not in the catalog', async () => {
|
|
96
95
|
const body = await response(['duplicateID', '260101T0009', 'brd']);
|
|
97
|
-
const violators = body['response content']['basics about all elements exhibiting the issue'];
|
|
98
|
-
const orphan = violators.find(v => v.identifier === '3');
|
|
96
|
+
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
97
|
+
const orphan = violators.find((v: any) => v.identifier === '3');
|
|
99
98
|
assert.ok(orphan);
|
|
100
99
|
assert.equal(orphan['tag name'], null);
|
|
101
100
|
assert.equal(orphan['inner text'], null);
|
|
@@ -103,6 +102,6 @@ test('listViolators returns null tag name and text for a violator not in the cat
|
|
|
103
102
|
|
|
104
103
|
test('listViolators returns an error for a falsy issue ID', async () => {
|
|
105
104
|
const body = await response(['', '260101T0009', 'brd']);
|
|
106
|
-
const issueBasics = body['response content']['basics about the issue'];
|
|
105
|
+
const issueBasics = body['response content']['basics about the issue'] as any;
|
|
107
106
|
assert.ok(issueBasics.error);
|
|
108
107
|
});
|
|
@@ -1,104 +1,79 @@
|
|
|
1
1
|
/*
|
|
2
|
-
listViolators.
|
|
2
|
+
listViolators.ts
|
|
3
3
|
Returns details about one issue in one report and basics about all the violators of the issue.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {
|
|
9
10
|
getIssueSpec,
|
|
10
11
|
getReportBasics,
|
|
11
12
|
getResponseMetadata,
|
|
12
13
|
getRuleEnginesFacts,
|
|
13
14
|
getToolsFacts,
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
15
|
+
getThisHost
|
|
16
|
+
} from './util.ts';
|
|
17
|
+
import {getReport, getTestActInstances, isReportError} from '../util.ts';
|
|
18
|
+
import {listViolatorsResponseSchema} from './schemas.ts';
|
|
19
|
+
|
|
20
|
+
// TYPES
|
|
21
|
+
|
|
22
|
+
// The response content defined by the response schema.
|
|
23
|
+
type ResponseContent = z.infer<typeof listViolatorsResponseSchema>['response content'];
|
|
17
24
|
|
|
18
25
|
// FUNCTIONS
|
|
19
26
|
|
|
20
27
|
// Returns the response body.
|
|
21
|
-
|
|
28
|
+
export const response = async (args: string[]) => {
|
|
22
29
|
const [issueID = '', timeStamp = '', jobID = ''] = args;
|
|
30
|
+
const thisHost = getThisHost();
|
|
31
|
+
// Get the report.
|
|
32
|
+
const report = await getReport(timeStamp, jobID);
|
|
23
33
|
// Initialize the response content.
|
|
24
|
-
const responseContent = {
|
|
25
|
-
'basics about the report':
|
|
26
|
-
'basics about the issue':
|
|
34
|
+
const responseContent: ResponseContent = {
|
|
35
|
+
'basics about the report': isReportError(report) ? report : await getReportBasics(timeStamp, jobID),
|
|
36
|
+
'basics about the issue': {
|
|
37
|
+
'error': 'No information about the specified issue is available'
|
|
38
|
+
},
|
|
27
39
|
'details about the issue': null,
|
|
28
40
|
'basics about all elements exhibiting the issue': null
|
|
29
41
|
};
|
|
30
|
-
// Get the report.
|
|
31
|
-
const report = await getReport(timeStamp, jobID);
|
|
32
|
-
// If this failed:
|
|
33
|
-
if (report.error) {
|
|
34
|
-
// Add this to the response content.
|
|
35
|
-
responseContent['basics about the report'] = report;
|
|
36
|
-
}
|
|
37
|
-
// Otherwise, i.e. if it succeeded:
|
|
38
|
-
else {
|
|
39
|
-
// Get the basics about the report (which may be only an error message).
|
|
40
|
-
const reportBasics = await getReportBasics(timeStamp, jobID);
|
|
41
|
-
// Add them to the response content.
|
|
42
|
-
responseContent['basics about the report'] = reportBasics;
|
|
43
|
-
}
|
|
44
42
|
// Get the specification of the issue.
|
|
45
43
|
const issueSpec = issueID ? getIssueSpec(issueID) : null;
|
|
46
|
-
// If the issue is ignorable
|
|
47
|
-
if (
|
|
48
|
-
// Add this to the response content.
|
|
49
|
-
responseContent['basics about the issue'] = {
|
|
50
|
-
'error': 'No information about the specified issue is available'
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
// Otherwise, i.e. if it is non-ignorable and fully classified:
|
|
54
|
-
else {
|
|
44
|
+
// If the issue is non-ignorable and fully classified:
|
|
45
|
+
if (issueSpec) {
|
|
55
46
|
const {summary, wcag, weight, why} = issueSpec;
|
|
56
|
-
//
|
|
57
|
-
|
|
47
|
+
// Add the basics about it to the response content.
|
|
48
|
+
responseContent['basics about the issue'] = {
|
|
58
49
|
'identifier': issueID,
|
|
59
50
|
summary,
|
|
60
51
|
'impact on a user': why,
|
|
61
52
|
'related WCAG standard': {
|
|
62
|
-
'layer': (wcag.length > 4 ? 'success criterion' : 'guideline'),
|
|
53
|
+
'layer': (wcag.length > 4 ? 'success criterion' : 'guideline') as 'success criterion' | 'guideline',
|
|
63
54
|
'identifier': wcag
|
|
64
55
|
},
|
|
65
|
-
priority: ['lowest', 'low', 'high', 'highest'][weight - 1]
|
|
56
|
+
priority: ['lowest', 'low', 'high', 'highest'][weight - 1] as 'lowest' | 'low' | 'high' | 'highest'
|
|
66
57
|
};
|
|
67
|
-
// Add the basics about the issue to the response content.
|
|
68
|
-
responseContent['basics about the issue'] = issueBasics;
|
|
69
58
|
// If the report is available:
|
|
70
|
-
if (!report
|
|
59
|
+
if (!isReportError(report)) {
|
|
71
60
|
// Initialize data about the instances of the issue.
|
|
72
|
-
const reporterIDs = new Set();
|
|
73
|
-
const violators = {};
|
|
74
|
-
// For each
|
|
75
|
-
report.
|
|
76
|
-
const {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
reporterIDs.add(which);
|
|
89
|
-
// If the instance has a catalog index:
|
|
90
|
-
if (catalogIndex) {
|
|
91
|
-
// Ensure the catalog index is in the violators data.
|
|
92
|
-
violators[catalogIndex] ??= {
|
|
93
|
-
catalogIndex,
|
|
94
|
-
reporters: new Set()
|
|
95
|
-
};
|
|
96
|
-
// Ensure the reporter ID is in the data about the violator.
|
|
97
|
-
violators[catalogIndex].reporters.add(which);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
61
|
+
const reporterIDs: Set<string> = new Set();
|
|
62
|
+
const violators: Record<string, {catalogIndex: string | number; reporters: Set<string>}> = {};
|
|
63
|
+
// For each violating standard instance of the issue in the report:
|
|
64
|
+
getTestActInstances(report, {violationsOnly: true, issueID}).forEach(({act, instance}) => {
|
|
65
|
+
const {catalogIndex} = instance;
|
|
66
|
+
// Ensure the rule-engine ID is in the reporters data.
|
|
67
|
+
reporterIDs.add(act.which!);
|
|
68
|
+
// If the instance has a catalog index:
|
|
69
|
+
if (catalogIndex) {
|
|
70
|
+
// Ensure the catalog index is in the violators data.
|
|
71
|
+
violators[catalogIndex] ??= {
|
|
72
|
+
catalogIndex,
|
|
73
|
+
reporters: new Set()
|
|
74
|
+
};
|
|
75
|
+
// Ensure the reporter ID is in the data about the violator.
|
|
76
|
+
violators[catalogIndex].reporters.add(act.which!);
|
|
102
77
|
}
|
|
103
78
|
});
|
|
104
79
|
// Get details about the reporters of the issue.
|
|
@@ -131,7 +106,7 @@ exports.response = async args => {
|
|
|
131
106
|
'count of rule engines reporting that the element exhibited the issue': reporters.size,
|
|
132
107
|
'how to get details about the element': {
|
|
133
108
|
URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${timeStamp}/${jobID}`,
|
|
134
|
-
'request method': 'GET'
|
|
109
|
+
'request method': 'GET' as const
|
|
135
110
|
},
|
|
136
111
|
'web users can get details about the element at': `${thisHost}/listDiagnoses.html/${issueID}/${timeStamp}/${jobID}/${catalogIndex}`
|
|
137
112
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/*
|
|
2
|
+
requestFeature.test.ts
|
|
3
|
+
Tests for api/requestFeature.ts with mocked side effects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {test, beforeEach, after} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
|
|
11
|
+
// SETUP AND TEARDOWN
|
|
12
|
+
|
|
13
|
+
let logged: any[] = [];
|
|
14
|
+
const originalLog = console.log;
|
|
15
|
+
|
|
16
|
+
// Capture console.log so the alert sendAlert emits can be observed. The alert
|
|
17
|
+
// configuration is empty in tests, so sendAlert logs a WARNING instead of sending.
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
logged = [];
|
|
20
|
+
console.log = (...args) => logged.push(args.join(' '));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
import {response} from './requestFeature.ts';
|
|
24
|
+
|
|
25
|
+
after(() => {
|
|
26
|
+
console.log = originalLog;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// TESTS
|
|
30
|
+
|
|
31
|
+
test('requestFeature rejects an empty feature request', async () => {
|
|
32
|
+
const body = await response(['']);
|
|
33
|
+
const details = body['response content']['details about your request'] as any;
|
|
34
|
+
assert.ok(details.error);
|
|
35
|
+
assert.ok(!logged.some(line => line.startsWith('WARNING (MCP feature request received)')));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('requestFeature accepts a non-empty feature request and notifies the manager', async () => {
|
|
39
|
+
const body = await response(['Add a dark mode toggle']);
|
|
40
|
+
const details = body['response content']['details about your request'] as any;
|
|
41
|
+
assert.equal(details.error, undefined);
|
|
42
|
+
assert.ok(details['date and time received']);
|
|
43
|
+
assert.equal(details.disposition, 'received and logged; manager notified');
|
|
44
|
+
assert.ok(logged.some(line =>
|
|
45
|
+
line.startsWith('WARNING (MCP feature request received)')
|
|
46
|
+
&& line.includes('Add a dark mode toggle')
|
|
47
|
+
));
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('requestFeature includes tool name and metadata', async () => {
|
|
51
|
+
const body = await response(['Some feature']);
|
|
52
|
+
assert.equal(body['tool name'], 'requestFeature');
|
|
53
|
+
assert.ok(body['response metadata'].identifier);
|
|
54
|
+
});
|
|
@@ -1,31 +1,34 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestFeature.
|
|
2
|
+
requestFeature.ts
|
|
3
3
|
Processes a request to add or improve a feature and returns an acknowledgement.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
+
import {sendAlert} from '../alerts.ts';
|
|
11
|
+
import {requestFeatureResponseSchema} from './schemas.ts';
|
|
12
|
+
|
|
13
|
+
// TYPES
|
|
14
|
+
|
|
15
|
+
// The response content defined by the response schema.
|
|
16
|
+
type ResponseContent = z.infer<typeof requestFeatureResponseSchema>['response content'];
|
|
10
17
|
|
|
11
18
|
// FUNCTIONS
|
|
12
19
|
|
|
13
20
|
// Returns the response body.
|
|
14
|
-
|
|
21
|
+
export const response = async (args: string[]) => {
|
|
15
22
|
const [feature = ''] = args;
|
|
23
|
+
const thisHost = getThisHost();
|
|
16
24
|
// Initialize the response content.
|
|
17
|
-
const responseContent = {
|
|
18
|
-
'details about your request':
|
|
19
|
-
};
|
|
20
|
-
// If the requested feature or improvement is empty:
|
|
21
|
-
if (!feature) {
|
|
22
|
-
// Add this to the response content.
|
|
23
|
-
responseContent['details about your request'] = {
|
|
25
|
+
const responseContent: ResponseContent = {
|
|
26
|
+
'details about your request': {
|
|
24
27
|
error: 'request invalid: request is empty'
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
//
|
|
28
|
-
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// If the requested feature or improvement exists:
|
|
31
|
+
if (feature) {
|
|
29
32
|
// Notify the manager.
|
|
30
33
|
await sendAlert('MCP feature request received', feature);
|
|
31
34
|
// Add the disposition to the response content.
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestRetest.test.
|
|
3
|
-
Tests for api/requestRetest.
|
|
2
|
+
requestRetest.test.ts
|
|
3
|
+
Tests for api/requestRetest.ts using the fixture corpus.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import {test, before, beforeEach, after} 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 {fixtureDBDir} from '../test/dbFixture.ts';
|
|
12
13
|
|
|
13
14
|
// SETUP AND TEARDOWN
|
|
14
15
|
|
|
15
16
|
const savedDBDir = process.env.DB_DIR;
|
|
16
|
-
|
|
17
|
+
const testRequestsPath = path.join(fixtureDBDir, 'jobs', 'testRequests.json');
|
|
18
|
+
let logged: any[] = [];
|
|
19
|
+
const originalLog = console.log;
|
|
17
20
|
|
|
18
21
|
before(() => {
|
|
19
|
-
process.env.DB_DIR =
|
|
20
|
-
// @ts-expect-error: Replacing the real function with a mock for testing.
|
|
21
|
-
apiUtil.processTestRequest = async (testType, what, url, reason) => {
|
|
22
|
-
processTestRequestCalls.push({testType, what, url, reason});
|
|
23
|
-
};
|
|
22
|
+
process.env.DB_DIR = fixtureDBDir;
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
// Reset the
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
// Reset the test-requests file and capture console.log, so the alert that
|
|
26
|
+
// processTestRequest emits can be observed and tests are order-independent.
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await fs.writeFile(testRequestsPath, '{}\n');
|
|
29
|
+
logged = [];
|
|
30
|
+
console.log = (...args) => logged.push(args.join(' '));
|
|
29
31
|
});
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
const {response} = require('./requestRetest');
|
|
33
|
+
import {response} from './requestRetest.ts';
|
|
33
34
|
|
|
34
35
|
after(() => {
|
|
36
|
+
console.log = originalLog;
|
|
35
37
|
if (savedDBDir !== undefined) {
|
|
36
38
|
process.env.DB_DIR = savedDBDir;
|
|
37
39
|
}
|
|
@@ -44,37 +46,40 @@ after(() => {
|
|
|
44
46
|
|
|
45
47
|
test('requestRetest rejects a nonexistent report', async () => {
|
|
46
48
|
const body = await response(['999999T9999', 'xyz', 'A reason that is long enough.']);
|
|
47
|
-
assert.ok(body['response content']['details about your request'].error);
|
|
48
|
-
assert.
|
|
49
|
+
assert.ok((body['response content']['details about your request'] as any).error);
|
|
50
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
test('requestRetest rejects a superseded report', async () => {
|
|
52
54
|
const body = await response(['260101T0000', 'mix', 'A reason that is long enough.']);
|
|
53
|
-
const details = body['response content']['details about your request'];
|
|
55
|
+
const details = body['response content']['details about your request'] as any;
|
|
54
56
|
assert.ok(details.error.includes('later report'));
|
|
55
|
-
assert.
|
|
57
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
56
58
|
});
|
|
57
59
|
|
|
58
60
|
test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
59
61
|
const body = await response(['260101T0001', 'ct', 'short']);
|
|
60
|
-
const details = body['response content']['details about your request'];
|
|
62
|
+
const details = body['response content']['details about your request'] as any;
|
|
61
63
|
assert.ok(details.error.includes('reason'));
|
|
62
|
-
assert.
|
|
64
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
63
65
|
});
|
|
64
66
|
|
|
65
67
|
test('requestRetest rejects a reason longer than 100 characters', async () => {
|
|
66
68
|
const longReason = 'x'.repeat(101);
|
|
67
69
|
const body = await response(['260101T0001', 'ct', longReason]);
|
|
68
|
-
const details = body['response content']['details about your request'];
|
|
70
|
+
const details = body['response content']['details about your request'] as any;
|
|
69
71
|
assert.ok(details.error.includes('reason'));
|
|
70
|
-
assert.
|
|
72
|
+
assert.ok(!logged.some(line => line.includes('request in the API')));
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
test('requestRetest accepts a valid retest request for the latest report of a page', async () => {
|
|
74
76
|
const body = await response(['260202T0000', 'new', 'A reason that is long enough.']);
|
|
75
|
-
const details = body['response content']['details about your request'];
|
|
77
|
+
const details = body['response content']['details about your request'] as any;
|
|
76
78
|
assert.equal(details.error, undefined);
|
|
77
79
|
assert.equal(details['page to be retested'].description, 'Mixed Outcomes Page');
|
|
78
|
-
assert.
|
|
79
|
-
|
|
80
|
+
assert.ok(logged.some(line =>
|
|
81
|
+
line.startsWith('WARNING (Kilotest: new retest request in the API)')
|
|
82
|
+
&& line.includes('Mixed Outcomes Page')
|
|
83
|
+
&& line.includes('https://example.com/mixed')
|
|
84
|
+
));
|
|
80
85
|
});
|
|
@@ -1,71 +1,78 @@
|
|
|
1
1
|
/*
|
|
2
|
-
requestRetest.
|
|
2
|
+
requestRetest.ts
|
|
3
3
|
Processes a request to retest a page and returns an acknowledgement.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import {z} from 'zod';
|
|
9
|
+
import {getResponseMetadata, getThisHost, getToolsFacts, processTestRequest} from './util.ts';
|
|
10
|
+
import {getReportExtracts, getReportExtract} from '../util.ts';
|
|
11
|
+
import {requestRetestResponseSchema} from './schemas.ts';
|
|
12
|
+
|
|
13
|
+
// TYPES
|
|
14
|
+
|
|
15
|
+
// The response content defined by the response schema.
|
|
16
|
+
type ResponseContent = z.infer<typeof requestRetestResponseSchema>['response content'];
|
|
10
17
|
|
|
11
18
|
// FUNCTIONS
|
|
12
19
|
|
|
13
20
|
// Returns the response body.
|
|
14
|
-
|
|
21
|
+
export const response = async (args: string[]) => {
|
|
15
22
|
const [timeStamp = '', jobID = '', reason = ''] = args;
|
|
16
|
-
|
|
17
|
-
const responseContent = {
|
|
18
|
-
'details about your request': {},
|
|
19
|
-
'disposition of your request': null
|
|
20
|
-
};
|
|
23
|
+
const thisHost = getThisHost();
|
|
21
24
|
const reasonLength = reason.length;
|
|
22
25
|
// Get data on the report.
|
|
23
26
|
const reportExtract = await getReportExtract(timeStamp, jobID);
|
|
24
|
-
|
|
27
|
+
// Initialize the response-content properties.
|
|
28
|
+
let requestDetails: ResponseContent['details about your request'];
|
|
29
|
+
let requestDisposition: ResponseContent['disposition of your request'] = null;
|
|
25
30
|
// If this failed:
|
|
26
|
-
if (reportExtract
|
|
27
|
-
|
|
28
|
-
responseContent['details about your request'] = {
|
|
31
|
+
if ('error' in reportExtract) {
|
|
32
|
+
requestDetails = {
|
|
29
33
|
error: 'request invalid: the specified existing report is not an available report'
|
|
30
34
|
};
|
|
31
35
|
}
|
|
32
36
|
// Otherwise, if the report has been superseded:
|
|
33
37
|
else if (
|
|
34
|
-
(await
|
|
38
|
+
(await getReportExtracts(true))
|
|
35
39
|
.every(extract => extract.timeStamp !== timeStamp || extract.jobID !== jobID)
|
|
36
40
|
) {
|
|
37
|
-
|
|
38
|
-
responseContent['details about your request'] = {
|
|
41
|
+
requestDetails = {
|
|
39
42
|
error: 'request invalid: a later report about the page exists'
|
|
40
43
|
};
|
|
41
44
|
}
|
|
42
45
|
// Otherwise, if the encoded reason is too short or too long:
|
|
43
46
|
else if (reasonLength < 20 || reasonLength > 100) {
|
|
44
|
-
|
|
45
|
-
responseContent['details about your request'] = {
|
|
47
|
+
requestDetails = {
|
|
46
48
|
error: 'request invalid: your reason is not between 20 and 100 characters long'
|
|
47
49
|
}
|
|
48
50
|
}
|
|
49
51
|
// Otherwise, i.e. if getting the report succeeded and the request is valid:
|
|
50
52
|
else {
|
|
51
53
|
// Process the request.
|
|
52
|
-
await processTestRequest('retest',
|
|
53
|
-
// Add details about the request
|
|
54
|
-
|
|
54
|
+
await processTestRequest('retest', reportExtract.description, reportExtract.url, reason);
|
|
55
|
+
// Add details about the request.
|
|
56
|
+
requestDetails = {
|
|
55
57
|
'date and time received': new Date().toISOString(),
|
|
56
58
|
'page to be retested': {
|
|
57
|
-
'description':
|
|
58
|
-
'URL': url
|
|
59
|
+
'description': reportExtract.description,
|
|
60
|
+
'URL': reportExtract.url
|
|
59
61
|
},
|
|
60
62
|
'reason why the page should be retested': reason
|
|
61
63
|
};
|
|
62
|
-
// Add information about the disposition of the request
|
|
63
|
-
|
|
64
|
+
// Add information about the disposition of the request.
|
|
65
|
+
requestDisposition = {
|
|
64
66
|
'what happens next': 'Your request is likely to be approved and processed within 1 hour to 1 day.',
|
|
65
67
|
'how you can check for completion': 'You can call the listReports tool to learn whether the page has been retested and a new report is available.',
|
|
66
68
|
'how a web user can check for completion': `A web user can visit ${thisHost}/listReports.html to learn whether the page has been retested and a new report is available.`
|
|
67
69
|
};
|
|
68
70
|
}
|
|
71
|
+
// Create the response content.
|
|
72
|
+
const responseContent: ResponseContent = {
|
|
73
|
+
'details about your request': requestDetails,
|
|
74
|
+
'disposition of your request': requestDisposition
|
|
75
|
+
};
|
|
69
76
|
// Create a response body.
|
|
70
77
|
const body = {
|
|
71
78
|
'tool collection': getToolsFacts(),
|