@jrpool/kilotest 47.0.1 → 49.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/scheduled_tasks.lock +1 -0
- package/api/requestFeature.test.ts +4 -2
- package/api/requestFeature.ts +1 -1
- package/api/requestRetest.test.ts +66 -7
- package/api/requestRetest.ts +55 -39
- package/api/requestTest.test.ts +63 -7
- package/api/requestTest.ts +36 -26
- package/api/util.test.ts +6 -5
- package/api/util.ts +2 -24
- package/docs/BACKLOG.md +228 -16
- package/docs/SERVICE.md +84 -7
- package/index.test.ts +354 -25
- package/index.ts +174 -8
- package/linkValidation.test.ts +1 -0
- package/mcp.test.ts +30 -0
- package/mcp.ts +9 -0
- package/package.json +1 -1
- package/smokeTest.ts +16 -0
- package/test/dbFixture.ts +8 -0
- package/util.test.ts +426 -127
- package/util.ts +389 -196
- package/web/ai0BalanceForm/ai0BalanceForm.test.ts +16 -10
- package/web/ai0BalanceForm/index.html +1 -1
- package/web/ai0BalanceForm/index.ts +10 -6
- package/web/enqueue/index.ts +5 -2
- package/web/expungeReportsForm/expungeReportsForm.test.ts +19 -11
- package/web/expungeReportsForm/index.html +1 -1
- package/web/expungeReportsForm/index.ts +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +24 -9
- package/web/hideReportForm/index.html +1 -1
- package/web/hideReportForm/index.ts +9 -6
- package/web/listReports/index.ts +33 -12
- package/web/listReports/listReports.test.ts +1 -1
- package/web/listTopIssues/index.ts +1 -1
- package/web/manage/index.html +2 -1
- package/web/metrics/index.html +19 -0
- package/web/metrics/index.ts +126 -0
- package/web/metrics/metrics.test.ts +211 -0
- package/web/pruneReportsForm/index.html +1 -1
- package/web/pruneReportsForm/index.ts +1 -1
- package/web/pruneReportsForm/pruneReportsForm.test.ts +18 -10
- package/web/reannotate/index.ts +7 -3
- package/web/reannotate/reannotate.test.ts +1 -1
- package/web/renewWCAG/index.ts +4 -1
- package/web/renewWCAG/renewWCAG.test.ts +1 -1
- package/web/reportDeletion.ts +12 -7
- package/web/requestRetest/index.html +3 -3
- package/web/requestRetest/index.ts +47 -16
- package/web/requestRetest/requestRetest.test.ts +38 -24
- package/web/requestTest/index.html +4 -4
- package/web/requestTest/index.ts +37 -9
- package/web/requestTest/requestTest.test.ts +21 -6
- package/web/rewindReportsForm/index.html +1 -1
- package/web/rewindReportsForm/index.ts +1 -1
- package/web/rewindReportsForm/rewindReportsForm.test.ts +18 -10
- package/web/showHiddenReportsForm/index.html +24 -0
- package/web/showHiddenReportsForm/index.ts +45 -0
- package/web/showHiddenReportsForm/showHiddenReportsForm.test.ts +103 -0
- package/web/tutorialAI/index.ts +1 -1
- package/web/tutorialWeb/index.ts +1 -1
- package/web/unhideReportForm/index.html +1 -1
- package/web/unhideReportForm/index.ts +11 -8
- package/web/unhideReportForm/unhideReportForm.test.ts +19 -11
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"f44e6b44-375e-4007-879a-19e4e5ed34a0","pid":90042,"procStart":"Sun Sep 20 03:19:03 2026","acquiredAt":1789874948676}
|
|
@@ -32,7 +32,9 @@ test('requestFeature rejects an empty feature request', async () => {
|
|
|
32
32
|
const body = await response(['']);
|
|
33
33
|
const details = body['response content']['details about your request'] as any;
|
|
34
34
|
assert.ok(details.error);
|
|
35
|
-
assert.ok(
|
|
35
|
+
assert.ok(
|
|
36
|
+
!logged.some(line => line.startsWith('WARNING (Kilotest: MCP feature request received)'))
|
|
37
|
+
);
|
|
36
38
|
});
|
|
37
39
|
|
|
38
40
|
test('requestFeature accepts a non-empty feature request and notifies the manager', async () => {
|
|
@@ -42,7 +44,7 @@ test('requestFeature accepts a non-empty feature request and notifies the manage
|
|
|
42
44
|
assert.ok(details['date and time received']);
|
|
43
45
|
assert.equal(details.disposition, 'received and logged; manager notified');
|
|
44
46
|
assert.ok(logged.some(line =>
|
|
45
|
-
line.startsWith('WARNING (MCP feature request received)')
|
|
47
|
+
line.startsWith('WARNING (Kilotest: MCP feature request received)')
|
|
46
48
|
&& line.includes('Add a dark mode toggle')
|
|
47
49
|
));
|
|
48
50
|
});
|
package/api/requestFeature.ts
CHANGED
|
@@ -30,7 +30,7 @@ export const response = async (args: string[]) => {
|
|
|
30
30
|
// If the requested feature or improvement exists:
|
|
31
31
|
if (feature) {
|
|
32
32
|
// Notify the manager.
|
|
33
|
-
await sendAlert('MCP feature request received', feature);
|
|
33
|
+
await sendAlert('Kilotest: MCP feature request received', feature);
|
|
34
34
|
// Add the disposition to the response content.
|
|
35
35
|
responseContent['details about your request'] = {
|
|
36
36
|
'date and time received': new Date().toISOString(),
|
|
@@ -46,22 +46,30 @@ after(() => {
|
|
|
46
46
|
|
|
47
47
|
test('requestRetest rejects a nonexistent report', async () => {
|
|
48
48
|
const body = await response(['999999T9999', 'xyz', 'A reason that is long enough.']);
|
|
49
|
-
|
|
50
|
-
assert.ok(
|
|
49
|
+
const details = body['response content']['details about your request'] as any;
|
|
50
|
+
assert.ok(details.error.includes('does not exist'));
|
|
51
|
+
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
51
52
|
});
|
|
52
53
|
|
|
53
54
|
test('requestRetest rejects a superseded report', async () => {
|
|
55
|
+
// 260101T0000-mix is an earlier report of "Mixed Outcomes Page" than 260202T0000-new,
|
|
56
|
+
// so it is superseded and processTestRequest returns 'superseded', reported via the
|
|
57
|
+
// disposition rather than details.error.
|
|
54
58
|
const body = await response(['260101T0000', 'mix', 'A reason that is long enough.']);
|
|
55
59
|
const details = body['response content']['details about your request'] as any;
|
|
56
|
-
assert.
|
|
57
|
-
|
|
60
|
+
assert.equal(details.error, undefined);
|
|
61
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
62
|
+
assert.ok(
|
|
63
|
+
disposition['what happens next'].includes('a later report about a page with the same description is available.')
|
|
64
|
+
);
|
|
65
|
+
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
58
66
|
});
|
|
59
67
|
|
|
60
68
|
test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
61
69
|
const body = await response(['260101T0001', 'ct', 'short']);
|
|
62
70
|
const details = body['response content']['details about your request'] as any;
|
|
63
71
|
assert.ok(details.error.includes('reason'));
|
|
64
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
72
|
+
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
65
73
|
});
|
|
66
74
|
|
|
67
75
|
test('requestRetest rejects a reason longer than 100 characters', async () => {
|
|
@@ -69,7 +77,58 @@ test('requestRetest rejects a reason longer than 100 characters', async () => {
|
|
|
69
77
|
const body = await response(['260101T0001', 'ct', longReason]);
|
|
70
78
|
const details = body['response content']['details about your request'] as any;
|
|
71
79
|
assert.ok(details.error.includes('reason'));
|
|
72
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
80
|
+
assert.ok(!logged.some(line => line.includes('new retest request awaits approval')));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('requestRetest rejects a report matching a claimed job by description', async () => {
|
|
84
|
+
// 260101T0001-ct is the "All CantTell Page" report.
|
|
85
|
+
const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', 'clm.json');
|
|
86
|
+
await fs.writeFile(claimedPath, JSON.stringify({
|
|
87
|
+
target: {what: 'All CantTell Page', url: 'https://example.com/unrelated'}
|
|
88
|
+
}));
|
|
89
|
+
try {
|
|
90
|
+
const body = await response(['260101T0001', 'ct', 'A reason that is long enough.']);
|
|
91
|
+
const details = body['response content']['details about your request'] as any;
|
|
92
|
+
assert.equal(details.error, undefined);
|
|
93
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
94
|
+
assert.ok(
|
|
95
|
+
disposition['what happens next']
|
|
96
|
+
.includes('a request to test a page with the same description is already approved.')
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await fs.unlink(claimedPath);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('requestRetest rejects a report matching a queued job by URL', async () => {
|
|
105
|
+
// 260101T0001-ct's URL is https://example.com/canttell.
|
|
106
|
+
const queuedPath = path.join(fixtureDBDir, 'jobs', 'queue', 'que.json');
|
|
107
|
+
await fs.writeFile(queuedPath, JSON.stringify({
|
|
108
|
+
target: {what: 'Some Other Page', url: 'https://example.com/canttell'}
|
|
109
|
+
}));
|
|
110
|
+
try {
|
|
111
|
+
const body = await response(['260101T0001', 'ct', 'A reason that is long enough.']);
|
|
112
|
+
const details = body['response content']['details about your request'] as any;
|
|
113
|
+
assert.equal(details.error, undefined);
|
|
114
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
115
|
+
assert.ok(
|
|
116
|
+
disposition['what happens next']
|
|
117
|
+
.includes('a request to test a page with the same URL is already approved.')
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
await fs.unlink(queuedPath);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('requestRetest rejects a duplicate request', async () => {
|
|
126
|
+
await response(['260101T0001', 'ct', 'A reason that is long enough.']);
|
|
127
|
+
const body = await response(['260101T0001', 'ct', 'A reason that is long enough.']);
|
|
128
|
+
const details = body['response content']['details about your request'] as any;
|
|
129
|
+
assert.equal(details.error, undefined);
|
|
130
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
131
|
+
assert.ok(disposition['what happens next'].includes('an identical request is already awaiting approval.'));
|
|
73
132
|
});
|
|
74
133
|
|
|
75
134
|
test('requestRetest accepts a valid retest request for the latest report of a page', async () => {
|
|
@@ -78,7 +137,7 @@ test('requestRetest accepts a valid retest request for the latest report of a pa
|
|
|
78
137
|
assert.equal(details.error, undefined);
|
|
79
138
|
assert.equal(details['page to be retested'].description, 'Mixed Outcomes Page');
|
|
80
139
|
assert.ok(logged.some(line =>
|
|
81
|
-
line.startsWith('WARNING (Kilotest: new retest request
|
|
140
|
+
line.startsWith('WARNING (Kilotest: new retest request awaits approval)')
|
|
82
141
|
&& line.includes('Mixed Outcomes Page')
|
|
83
142
|
&& line.includes('https://example.com/mixed')
|
|
84
143
|
));
|
package/api/requestRetest.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
|
-
import {getResponseMetadata, getThisHost, getToolsFacts
|
|
10
|
-
import {
|
|
9
|
+
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
+
import {processTestRequest} from '../util.ts';
|
|
11
11
|
import {requestRetestResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -22,51 +22,67 @@ export const response = async (args: string[]) => {
|
|
|
22
22
|
const [timeStamp = '', jobID = '', reason = ''] = args;
|
|
23
23
|
const thisHost = getThisHost();
|
|
24
24
|
const reasonLength = reason.length;
|
|
25
|
-
// Get data on the report.
|
|
26
|
-
const reportExtract = await getReportExtract(timeStamp, jobID);
|
|
27
25
|
// Initialize the response-content properties.
|
|
28
26
|
let requestDetails: ResponseContent['details about your request'];
|
|
29
27
|
let requestDisposition: ResponseContent['disposition of your request'] = null;
|
|
30
|
-
// If
|
|
31
|
-
if (
|
|
32
|
-
requestDetails = {
|
|
33
|
-
error: 'request invalid: the specified existing report is not an available report'
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
// Otherwise, if the report has been superseded:
|
|
37
|
-
else if (
|
|
38
|
-
(await getReportExtracts(true))
|
|
39
|
-
.every(extract => extract.timeStamp !== timeStamp || extract.jobID !== jobID)
|
|
40
|
-
) {
|
|
41
|
-
requestDetails = {
|
|
42
|
-
error: 'request invalid: a later report about the page exists'
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
// Otherwise, if the encoded reason is too short or too long:
|
|
46
|
-
else if (reasonLength < 20 || reasonLength > 100) {
|
|
28
|
+
// If the encoded reason is too short or too long:
|
|
29
|
+
if (reasonLength < 20 || reasonLength > 100) {
|
|
47
30
|
requestDetails = {
|
|
48
31
|
error: 'request invalid: your reason is not between 20 and 100 characters long'
|
|
49
32
|
}
|
|
50
33
|
}
|
|
51
|
-
// Otherwise, i.e. if
|
|
34
|
+
// Otherwise, i.e. if the request is facially valid:
|
|
52
35
|
else {
|
|
53
|
-
// Process the request.
|
|
54
|
-
await processTestRequest(
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
36
|
+
// Process the request. This resolves the cited report's description and URL itself.
|
|
37
|
+
const processResult = await processTestRequest(reason, {timeStamp, jobID});
|
|
38
|
+
// If the cited report does not exist:
|
|
39
|
+
if (processResult.result === 'nonreport') {
|
|
40
|
+
requestDetails = {
|
|
41
|
+
error: 'request invalid: the specified report does not exist'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Otherwise, i.e. if the cited report was found:
|
|
45
|
+
else {
|
|
46
|
+
const {result: requestResult, description, url} = processResult;
|
|
47
|
+
// Add details about the request.
|
|
48
|
+
requestDetails = {
|
|
49
|
+
'date and time received': new Date().toISOString(),
|
|
50
|
+
'page to be retested': {
|
|
51
|
+
'description': description,
|
|
52
|
+
'URL': url
|
|
53
|
+
},
|
|
54
|
+
'reason why the page should be retested': reason
|
|
55
|
+
};
|
|
56
|
+
// Add information about the disposition of the request.
|
|
57
|
+
if (requestResult === 'ok') {
|
|
58
|
+
requestDisposition = {
|
|
59
|
+
'what happens next': 'Your request is likely to be approved and processed within 1 hour to 1 day.',
|
|
60
|
+
'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.',
|
|
61
|
+
'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.`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const failureFact = 'Your request will not be processed, because ';
|
|
66
|
+
let failureReason: string;
|
|
67
|
+
if (requestResult === 'description') {
|
|
68
|
+
failureReason = 'a request to test a page with the same description is already approved.'
|
|
69
|
+
}
|
|
70
|
+
else if (requestResult === 'url') {
|
|
71
|
+
failureReason = 'a request to test a page with the same URL is already approved.'
|
|
72
|
+
}
|
|
73
|
+
else if (requestResult === 'superseded') {
|
|
74
|
+
failureReason = 'a later report about a page with the same description is available.'
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
failureReason = 'an identical request is already awaiting approval.'
|
|
78
|
+
}
|
|
79
|
+
requestDisposition = {
|
|
80
|
+
'what happens next': `${failureFact}${failureReason}`,
|
|
81
|
+
'how you can check for completion': 'Not applicable.',
|
|
82
|
+
'how a web user can check for completion': 'Not applicable.'
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
70
86
|
}
|
|
71
87
|
// Create the response content.
|
|
72
88
|
const responseContent: ResponseContent = {
|
package/api/requestTest.test.ts
CHANGED
|
@@ -47,34 +47,90 @@ after(() => {
|
|
|
47
47
|
test('requestTest rejects an empty description', async () => {
|
|
48
48
|
const body = await response(['', 'https://example.com/test', 'A reason that is long enough.']);
|
|
49
49
|
assert.ok((body['response content']['details about your request'] as any).error);
|
|
50
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
50
|
+
assert.ok(!logged.some(line => line.includes('new test request awaits approval')));
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
test('requestTest rejects a description longer than 100 characters', async () => {
|
|
54
54
|
const longWhat = 'x'.repeat(101);
|
|
55
55
|
const body = await response([longWhat, 'https://example.com/test', 'A reason that is long enough.']);
|
|
56
56
|
assert.ok((body['response content']['details about your request'] as any).error);
|
|
57
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
57
|
+
assert.ok(!logged.some(line => line.includes('new test request awaits approval')));
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test('requestTest rejects a URL shorter than 12 characters', async () => {
|
|
61
61
|
const body = await response(['Test Page', 'short', 'A reason that is long enough.']);
|
|
62
62
|
assert.ok((body['response content']['details about your request'] as any).error);
|
|
63
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
63
|
+
assert.ok(!logged.some(line => line.includes('new test request awaits approval')));
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
test('requestTest rejects a syntactically invalid URL with the correct length', async () => {
|
|
67
67
|
const body = await response(['Test Page', 'not-a-valid-url', 'A reason that is long enough.']);
|
|
68
68
|
const details = body['response content']['details about your request'] as any;
|
|
69
69
|
assert.ok(details.error.includes('invalid URL'));
|
|
70
|
-
assert.ok(!logged.some(line => line.includes('request
|
|
70
|
+
assert.ok(!logged.some(line => line.includes('new test request awaits approval')));
|
|
71
71
|
});
|
|
72
72
|
|
|
73
73
|
test('requestTest rejects an already-tested page', async () => {
|
|
74
74
|
const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']);
|
|
75
75
|
const details = body['response content']['details about your request'] as any;
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
// A report for this description and URL already exists, so processTestRequest
|
|
77
|
+
// returns 'retest' and the failure is reported via the disposition, not details.error.
|
|
78
|
+
assert.equal(details.error, undefined);
|
|
79
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
80
|
+
assert.ok(
|
|
81
|
+
disposition['what happens next']
|
|
82
|
+
.includes('a report about a page with the same description and URL is available.')
|
|
83
|
+
);
|
|
84
|
+
assert.ok(!logged.some(line => line.includes('new test request awaits approval')));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('requestTest rejects a page matching a claimed job by description', async () => {
|
|
88
|
+
const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', 'clm.json');
|
|
89
|
+
await fs.writeFile(claimedPath, JSON.stringify({
|
|
90
|
+
target: {what: 'Claimed Test Page', url: 'https://example.com/claimed-job'}
|
|
91
|
+
}));
|
|
92
|
+
try {
|
|
93
|
+
const body = await response(['Claimed Test Page', 'https://example.com/other-url', 'A reason that is long enough.']);
|
|
94
|
+
const details = body['response content']['details about your request'] as any;
|
|
95
|
+
assert.equal(details.error, undefined);
|
|
96
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
97
|
+
assert.ok(
|
|
98
|
+
disposition['what happens next']
|
|
99
|
+
.includes('a request to test a page with the same description is already approved.')
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await fs.unlink(claimedPath);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('requestTest rejects a page matching a queued job by URL', async () => {
|
|
108
|
+
const queuedPath = path.join(fixtureDBDir, 'jobs', 'queue', 'que.json');
|
|
109
|
+
await fs.writeFile(queuedPath, JSON.stringify({
|
|
110
|
+
target: {what: 'Some Other Page', url: 'https://example.com/queued-url'}
|
|
111
|
+
}));
|
|
112
|
+
try {
|
|
113
|
+
const body = await response(['A Different Page', 'https://example.com/queued-url', 'A reason that is long enough.']);
|
|
114
|
+
const details = body['response content']['details about your request'] as any;
|
|
115
|
+
assert.equal(details.error, undefined);
|
|
116
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
117
|
+
assert.ok(
|
|
118
|
+
disposition['what happens next']
|
|
119
|
+
.includes('a request to test a page with the same URL is already approved.')
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await fs.unlink(queuedPath);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('requestTest rejects a duplicate request', async () => {
|
|
128
|
+
await response(['Duplicate API Page', 'https://example.com/dup-api', 'A reason that is long enough.']);
|
|
129
|
+
const body = await response(['Duplicate API Page', 'https://example.com/dup-api', 'A reason that is long enough.']);
|
|
130
|
+
const details = body['response content']['details about your request'] as any;
|
|
131
|
+
assert.equal(details.error, undefined);
|
|
132
|
+
const disposition = body['response content']['disposition of your request'] as any;
|
|
133
|
+
assert.ok(disposition['what happens next'].includes('an identical request is already awaiting approval.'));
|
|
78
134
|
});
|
|
79
135
|
|
|
80
136
|
test('requestTest accepts a valid new page request', async () => {
|
|
@@ -83,7 +139,7 @@ test('requestTest accepts a valid new page request', async () => {
|
|
|
83
139
|
assert.equal(details.error, undefined);
|
|
84
140
|
assert.equal(details['page to be tested'].description, 'Brand New Page');
|
|
85
141
|
assert.ok(logged.some(line =>
|
|
86
|
-
line.startsWith('WARNING (Kilotest: new test request
|
|
142
|
+
line.startsWith('WARNING (Kilotest: new test request awaits approval)')
|
|
87
143
|
&& line.includes('Brand New Page')
|
|
88
144
|
&& line.includes('https://example.com/brandnew')
|
|
89
145
|
));
|
package/api/requestTest.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
|
-
import {getResponseMetadata, getThisHost, getToolsFacts
|
|
10
|
-
import {
|
|
9
|
+
import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
+
import {isURL, processTestRequest} from '../util.ts';
|
|
11
11
|
import {requestTestResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -45,36 +45,46 @@ export const response = async (args: string[]) => {
|
|
|
45
45
|
}
|
|
46
46
|
// Otherwise, i.e. if the description and URL are valid:
|
|
47
47
|
else {
|
|
48
|
-
//
|
|
49
|
-
const
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
await processTestRequest('test', description, url, reason);
|
|
62
|
-
// Add details about the request.
|
|
63
|
-
requestDetails = {
|
|
64
|
-
'date and time received': new Date().toISOString(),
|
|
65
|
-
'page to be tested': {
|
|
66
|
-
description,
|
|
67
|
-
URL: url
|
|
68
|
-
},
|
|
69
|
-
'reason why the page should be tested': reason
|
|
70
|
-
};
|
|
71
|
-
// Add information about the disposition of the request.
|
|
48
|
+
// Process the request.
|
|
49
|
+
const {result: requestResult} = await processTestRequest(reason, {description, url});
|
|
50
|
+
// Add details about the request.
|
|
51
|
+
requestDetails = {
|
|
52
|
+
'date and time received': new Date().toISOString(),
|
|
53
|
+
'page to be tested': {
|
|
54
|
+
description,
|
|
55
|
+
URL: url
|
|
56
|
+
},
|
|
57
|
+
'reason why the page should be tested': reason
|
|
58
|
+
};
|
|
59
|
+
// Add information about the disposition of the request.
|
|
60
|
+
if (requestResult === 'ok') {
|
|
72
61
|
requestDisposition = {
|
|
73
62
|
'what happens next': 'Your request is likely to be approved and processed within 1 hour to 1 day.',
|
|
74
63
|
'how you can check for completion': 'You can call the listReports tool to learn whether the page has been tested and a report is available.',
|
|
75
64
|
'how a web user can check for completion': `A web user can visit ${thisHost}/listReports.html to learn whether the page has been tested and a report is available.`
|
|
76
65
|
};
|
|
77
66
|
}
|
|
67
|
+
else {
|
|
68
|
+
const failureFact = 'Your request will not be processed, because ';
|
|
69
|
+
let failureReason: string;
|
|
70
|
+
if (requestResult === 'description') {
|
|
71
|
+
failureReason = 'a request to test a page with the same description is already approved.'
|
|
72
|
+
}
|
|
73
|
+
else if (requestResult === 'url') {
|
|
74
|
+
failureReason = 'a request to test a page with the same URL is already approved.'
|
|
75
|
+
}
|
|
76
|
+
else if (requestResult === 'retest') {
|
|
77
|
+
failureReason = 'a report about a page with the same description and URL is available.'
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
failureReason = 'an identical request is already awaiting approval.'
|
|
81
|
+
}
|
|
82
|
+
requestDisposition = {
|
|
83
|
+
'what happens next': `${failureFact}${failureReason}`,
|
|
84
|
+
'how you can check for completion': 'Not applicable.',
|
|
85
|
+
'how a web user can check for completion': 'Not applicable.'
|
|
86
|
+
}
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
89
|
// Create the response content.
|
|
80
90
|
const responseContent: ResponseContent = {
|
package/api/util.test.ts
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import {test} from 'node:test';
|
|
9
9
|
import assert from 'node:assert/strict';
|
|
10
|
-
import {getReportBasics, getResponseMetadata, getRuleEngineFacts, getRuleEnginesFacts, getIssueSpec, getToolsFacts
|
|
10
|
+
import {getReportBasics, getResponseMetadata, getRuleEngineFacts, getRuleEnginesFacts, getIssueSpec, getToolsFacts} from './util.ts';
|
|
11
|
+
import {processTestRequest} from '../util.ts';
|
|
11
12
|
|
|
12
13
|
// SETUP
|
|
13
14
|
|
|
@@ -98,10 +99,10 @@ test('getReportBasics returns an error for a report whose extract cannot be read
|
|
|
98
99
|
test('processTestRequest returns an error for a duplicate request', async () => {
|
|
99
100
|
process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
|
|
100
101
|
// Submit the same request twice; the second should be a duplicate.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
assert.equal(result
|
|
102
|
+
const target = {description: 'Dup Page', url: 'https://example.com/dup'};
|
|
103
|
+
await processTestRequest('A reason that is long enough.', target);
|
|
104
|
+
const {result} = await processTestRequest('A reason that is long enough.', target);
|
|
105
|
+
assert.equal(result, 'duplicate');
|
|
105
106
|
if (savedDBDir !== undefined) {
|
|
106
107
|
process.env.DB_DIR = savedDBDir;
|
|
107
108
|
}
|
package/api/util.ts
CHANGED
|
@@ -5,18 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
|
-
import {sendAlert} from '../alerts.ts';
|
|
9
8
|
import {
|
|
10
9
|
getAgoDays,
|
|
11
10
|
getReportExtracts,
|
|
12
11
|
getNowStamp,
|
|
13
|
-
getPlainText,
|
|
14
12
|
getRandomString,
|
|
15
13
|
getReportExtract,
|
|
16
14
|
getReportStats,
|
|
17
15
|
objectSort,
|
|
18
|
-
ruleEngines
|
|
19
|
-
addTestRequest
|
|
16
|
+
ruleEngines
|
|
20
17
|
} from '../util.ts';
|
|
21
18
|
import type {ReportExtract} from '../util.ts';
|
|
22
19
|
import {issues as issueSpecs} from 'testaro-issues';
|
|
@@ -83,6 +80,7 @@ export const getRuleEnginesFacts = (ruleEngineIDSet: Iterable<string>) => {
|
|
|
83
80
|
// getReportExtracts, it carries a superseded flag; otherwise the flag is computed.
|
|
84
81
|
// With an extract provided, failure is impossible, so the return type narrows
|
|
85
82
|
// to ReportBasics; without one, an error object may be returned.
|
|
83
|
+
// XXX Why are these overloads necessary? Can't we just have one function?
|
|
86
84
|
export function getReportBasics(
|
|
87
85
|
timeStamp: string, jobID: string, extract: ReportExtract
|
|
88
86
|
): Promise<ReportBasics>;
|
|
@@ -151,23 +149,3 @@ export const getIssueSpec = (issueID: string) => {
|
|
|
151
149
|
// Otherwise, i.e. if it does not exist, return this.
|
|
152
150
|
return null;
|
|
153
151
|
};
|
|
154
|
-
// Processes a test or retest request from the API.
|
|
155
|
-
export const processTestRequest = async (testType: string, description: string, url: string, why: string): Promise<{status: string; message: string} | undefined> => {
|
|
156
|
-
// Get an email-safe version of the reason.
|
|
157
|
-
const plainWhy = getPlainText(why);
|
|
158
|
-
// Add the test request as a transaction.
|
|
159
|
-
const updateResult = await addTestRequest(description, url, plainWhy);
|
|
160
|
-
// If the request was a duplicate:
|
|
161
|
-
if (updateResult.error === 'duplicate') {
|
|
162
|
-
// Return this.
|
|
163
|
-
return {
|
|
164
|
-
status: 'error',
|
|
165
|
-
message: 'Duplicate request'
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
// Otherwise, i.e. if it was not a duplicate, alert a manager about it.
|
|
169
|
-
await sendAlert(
|
|
170
|
-
`Kilotest: new ${testType} request in the API`,
|
|
171
|
-
`Target: ${description}\nURL: ${url}\nReason: ${plainWhy}`
|
|
172
|
-
);
|
|
173
|
-
};
|