@jrpool/kilotest 59.1.3 → 60.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/README.md +2 -5
- package/api/awaitTest.test.ts +9 -9
- package/api/awaitTest.ts +12 -12
- package/api/getReport.test.ts +4 -4
- package/api/getReport.ts +9 -9
- package/api/listDiagnoses.test.ts +14 -14
- package/api/listDiagnoses.ts +8 -8
- package/api/listIssues.test.ts +15 -15
- package/api/listIssues.ts +18 -20
- package/api/listPages.test.ts +2 -2
- package/api/listPages.ts +4 -4
- package/api/listViolators.test.ts +10 -10
- package/api/listViolators.ts +10 -10
- package/api/orderNewTest.test.ts +1 -2
- package/api/orderNewTest.ts +3 -4
- package/api/orderRetest.test.ts +10 -10
- package/api/orderRetest.ts +13 -14
- package/api/requestRetest.test.ts +11 -11
- package/api/requestRetest.ts +11 -11
- package/api/routes.ts +9 -11
- package/api/schemas.ts +18 -28
- package/api/util.test.ts +2 -2
- package/api/util.ts +7 -7
- package/capability.md +4 -4
- package/docs/AI-TOOLS.md +45 -5
- package/docs/decisions/009-report-id.md +16 -0
- package/index.test.ts +91 -41
- package/index.ts +66 -15
- package/lhm.plugin.json +33 -119
- package/llms-full.txt +18 -24
- package/llms.txt +12 -12
- package/mcp.test.ts +11 -20
- package/mcp.ts +18 -18
- package/openapi-3.0.yaml +1847 -0
- package/openapi.yaml +38 -97
- package/package.json +8 -2
- package/routing.test.ts +1 -1
- package/scripts/generate-openapi.ts +83 -1
- package/scripts/postPR.ts +89 -0
- package/scripts/prePR.ts +64 -0
- package/server.json +1 -1
- package/util.test.ts +32 -33
- package/util.ts +47 -43
- package/web/hideReportForm/index.ts +5 -6
- package/web/listDiagnoses/index.ts +4 -5
- package/web/listDiagnoses/listDiagnoses.test.ts +13 -13
- package/web/listIssues/index.ts +11 -12
- package/web/listIssues/listIssues.test.ts +24 -24
- package/web/listPages/index.ts +2 -2
- package/web/listPages/listPages.test.ts +2 -2
- package/web/listTopIssues/index.ts +2 -2
- package/web/listViolators/index.ts +5 -6
- package/web/listViolators/listViolators.test.ts +12 -12
- package/web/reannotate/index.ts +3 -3
- package/web/reannotateForm/index.ts +1 -2
- package/web/reportDeletion.ts +9 -12
- package/web/requestRetest/index.ts +2 -2
- package/web/requestRetest/requestRetest.test.ts +7 -7
- package/web/requestRetestForm/index.html +2 -2
- package/web/requestRetestForm/index.ts +3 -4
- package/web/requestRetestForm/requestRetestForm.test.ts +7 -8
- package/web/unhideReportForm/index.ts +4 -6
- package/web/util.test.ts +29 -29
- package/web/util.ts +1 -1
package/api/listIssues.ts
CHANGED
|
@@ -36,12 +36,12 @@ type ResponseContent = z.infer<typeof listIssuesResponseSchema>['response conten
|
|
|
36
36
|
|
|
37
37
|
// Returns the response body.
|
|
38
38
|
export const response = async (args: string[]) => {
|
|
39
|
-
const [
|
|
39
|
+
const [reportID = ''] = args;
|
|
40
40
|
const thisHost = getThisHost();
|
|
41
41
|
// Get the report.
|
|
42
|
-
const report = await getReport(
|
|
42
|
+
const report = await getReport(reportID);
|
|
43
43
|
// Get the basics about the report (which may be only an error message).
|
|
44
|
-
const reportBasics = isReportError(report) ? report : await getReportBasics(
|
|
44
|
+
const reportBasics = isReportError(report) ? report : await getReportBasics(reportID);
|
|
45
45
|
// Initialize the response content.
|
|
46
46
|
const responseContent: ResponseContent = {
|
|
47
47
|
'basics about the report': reportBasics,
|
|
@@ -68,21 +68,19 @@ export const response = async (args: string[]) => {
|
|
|
68
68
|
const lastIndex = pageExtracts.length - 1;
|
|
69
69
|
// Add the history of reports about the page to the response content.
|
|
70
70
|
responseContent['history of reports about the page'] = pageExtracts.map((extract, index) => {
|
|
71
|
-
const
|
|
72
|
-
const extractJobID = extract.jobID;
|
|
71
|
+
const extractID = extract.id;
|
|
73
72
|
return {
|
|
74
|
-
identifier:
|
|
73
|
+
identifier: extractID,
|
|
75
74
|
'completion date and time': extract.reportTime,
|
|
76
75
|
'days since the report was completed': getAgoDays(new Date(extract.reportTime)),
|
|
77
76
|
'whether it is the oldest report about the page': index === 0,
|
|
78
77
|
'whether it is the latest report about the page': index === lastIndex,
|
|
79
|
-
'whether it is the report described in this response':
|
|
80
|
-
extractTimeStamp === timeStamp && extractJobID === jobID,
|
|
78
|
+
'whether it is the report described in this response': extractID === reportID,
|
|
81
79
|
'how to get details about the report': {
|
|
82
80
|
method: 'GET' as const,
|
|
83
|
-
URL: `${thisHost}/api/listIssues/${
|
|
81
|
+
URL: `${thisHost}/api/listIssues/${extractID}`
|
|
84
82
|
},
|
|
85
|
-
'web users can get details about the report at': `${thisHost}/listIssues.html/${
|
|
83
|
+
'web users can get details about the report at': `${thisHost}/listIssues.html/${extractID}`
|
|
86
84
|
};
|
|
87
85
|
});
|
|
88
86
|
// Get details about the job definition.
|
|
@@ -165,7 +163,7 @@ export const response = async (args: string[]) => {
|
|
|
165
163
|
'number of elements reported as violators': violatorIndexes.size
|
|
166
164
|
};
|
|
167
165
|
// Get the size of the report.
|
|
168
|
-
const reportStats = await getReportStats(
|
|
166
|
+
const reportStats = await getReportStats(reportID);
|
|
169
167
|
// If this succeeded:
|
|
170
168
|
if (reportStats) {
|
|
171
169
|
const {reportSize} = reportStats;
|
|
@@ -173,11 +171,11 @@ export const response = async (args: string[]) => {
|
|
|
173
171
|
responseContent['how to get the full report in JSON'] = {
|
|
174
172
|
'size of the report in bytes': reportSize,
|
|
175
173
|
method: 'GET',
|
|
176
|
-
URL: `${thisHost}/api/getReport/${
|
|
174
|
+
URL: `${thisHost}/api/getReport/${reportID}`
|
|
177
175
|
};
|
|
178
176
|
// Add instructions for web users to get the full report to the response content.
|
|
179
177
|
responseContent['how a web user can get the full report in JSON'] = {
|
|
180
|
-
URL: `${thisHost}/fullReport.json/${
|
|
178
|
+
URL: `${thisHost}/fullReport.json/${reportID}`
|
|
181
179
|
};
|
|
182
180
|
}
|
|
183
181
|
// Add the details about the job and test results to the response content.
|
|
@@ -197,7 +195,7 @@ export const response = async (args: string[]) => {
|
|
|
197
195
|
// Add instructions for a retest request to the response content.
|
|
198
196
|
responseContent['how to request that the page be retested'] = {
|
|
199
197
|
method: 'POST',
|
|
200
|
-
URL: `${thisHost}/api/requestRetest/${
|
|
198
|
+
URL: `${thisHost}/api/requestRetest/${reportID}`,
|
|
201
199
|
'request body': {
|
|
202
200
|
reason: '20- to 100-character reason why the page should be retested'
|
|
203
201
|
},
|
|
@@ -206,7 +204,7 @@ export const response = async (args: string[]) => {
|
|
|
206
204
|
};
|
|
207
205
|
// Add instructions for a web user to request a retest.
|
|
208
206
|
responseContent['how a web user can request that the page be retested'] = {
|
|
209
|
-
URL: `${thisHost}/requestRetestForm.html/${
|
|
207
|
+
URL: `${thisHost}/requestRetestForm.html/${reportID}`
|
|
210
208
|
};
|
|
211
209
|
}
|
|
212
210
|
// Sort the data about issues by summary.
|
|
@@ -223,9 +221,9 @@ export const response = async (args: string[]) => {
|
|
|
223
221
|
.map(ruleEnginesFact => ruleEnginesFact.name),
|
|
224
222
|
'how to get details about the issue': {
|
|
225
223
|
method: 'GET' as const,
|
|
226
|
-
URL: `${thisHost}/api/listViolators/${id}/${
|
|
224
|
+
URL: `${thisHost}/api/listViolators/${id}/${reportID}`
|
|
227
225
|
},
|
|
228
|
-
'web users can get details about the issue at': `${thisHost}/listViolators.html/${id}/${
|
|
226
|
+
'web users can get details about the issue at': `${thisHost}/listViolators.html/${id}/${reportID}`
|
|
229
227
|
};
|
|
230
228
|
});
|
|
231
229
|
// Add the basics about the issues to the response content.
|
|
@@ -237,9 +235,9 @@ export const response = async (args: string[]) => {
|
|
|
237
235
|
'tool collection': getToolsFacts(),
|
|
238
236
|
'tool name': 'listIssues',
|
|
239
237
|
'this request': {
|
|
240
|
-
description: 'Provide details about one report, including basics about the issues reported in it. The
|
|
238
|
+
description: 'Provide details about one report, including basics about the issues reported in it. The reportID parameter identifies the report that I want details about. It was in the response to my earlier listPages request, or in the history of reports about the page in an earlier listIssues response.',
|
|
241
239
|
method: 'GET',
|
|
242
|
-
URL: `${thisHost}/api/listIssues/${
|
|
240
|
+
URL: `${thisHost}/api/listIssues/${reportID}`,
|
|
243
241
|
'closest ancestor request': {
|
|
244
242
|
'tool name': 'listPages',
|
|
245
243
|
description: 'Provide basics about all tested pages.',
|
|
@@ -248,7 +246,7 @@ export const response = async (args: string[]) => {
|
|
|
248
246
|
}
|
|
249
247
|
},
|
|
250
248
|
'URLs of similar requests for web users': {
|
|
251
|
-
'this request': `${thisHost}/listIssues.html/${
|
|
249
|
+
'this request': `${thisHost}/listIssues.html/${reportID}`,
|
|
252
250
|
'closest ancestor request': `${thisHost}/listPages.html`
|
|
253
251
|
},
|
|
254
252
|
'response metadata': getResponseMetadata(),
|
package/api/listPages.test.ts
CHANGED
|
@@ -63,8 +63,8 @@ test('listPages describes a multi-report page by its latest report and its repor
|
|
|
63
63
|
assert.equal(latest['completion date and time'], '2026-02-02T00:10:00.000Z');
|
|
64
64
|
assert.equal(typeof latest['days since the report was completed'], 'number');
|
|
65
65
|
assert.equal(mixed['how to get details about the latest report'].method, 'GET');
|
|
66
|
-
assert.ok(mixed['how to get details about the latest report'].URL.endsWith('/api/listIssues/260202T0000
|
|
67
|
-
assert.ok(mixed['web users can get details about the latest report at'].endsWith('/listIssues.html/260202T0000
|
|
66
|
+
assert.ok(mixed['how to get details about the latest report'].URL.endsWith('/api/listIssues/260202T0000-new'));
|
|
67
|
+
assert.ok(mixed['web users can get details about the latest report at'].endsWith('/listIssues.html/260202T0000-new'));
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test('listPages describes a single-report page', async () => {
|
package/api/listPages.ts
CHANGED
|
@@ -31,7 +31,7 @@ export const response = async () => {
|
|
|
31
31
|
const pagesBasics: ResponseContent['basics about all tested pages'] = [];
|
|
32
32
|
// For each report:
|
|
33
33
|
reportExtracts.forEach(extract => {
|
|
34
|
-
const {description,
|
|
34
|
+
const {description, id, reportTime, superseded, url} = extract;
|
|
35
35
|
// If it is the latest report about its page:
|
|
36
36
|
if (!superseded) {
|
|
37
37
|
// Add basics about the page, with instructions for getting details, to the array.
|
|
@@ -42,15 +42,15 @@ export const response = async () => {
|
|
|
42
42
|
},
|
|
43
43
|
'number of reports about the page': reportCounts.get(description)!,
|
|
44
44
|
'basics about the latest report': {
|
|
45
|
-
identifier:
|
|
45
|
+
identifier: id,
|
|
46
46
|
'completion date and time': reportTime,
|
|
47
47
|
'days since the report was completed': getAgoDays(new Date(reportTime))
|
|
48
48
|
},
|
|
49
49
|
'how to get details about the latest report': {
|
|
50
50
|
method: 'GET',
|
|
51
|
-
URL: `${thisHost}/api/listIssues/${
|
|
51
|
+
URL: `${thisHost}/api/listIssues/${id}`
|
|
52
52
|
},
|
|
53
|
-
'web users can get details about the latest report at': `${thisHost}/listIssues.html/${
|
|
53
|
+
'web users can get details about the latest report at': `${thisHost}/listIssues.html/${id}`
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
});
|
|
@@ -29,7 +29,7 @@ after(() => {
|
|
|
29
29
|
// TESTS
|
|
30
30
|
|
|
31
31
|
test('listViolators returns 1 violator for linkNoText in the mixed report, excluding cantTell', async () => {
|
|
32
|
-
const body = await response(['linkNoText', '260101T0000
|
|
32
|
+
const body = await response(['linkNoText', '260101T0000-mix']);
|
|
33
33
|
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
34
34
|
assert.equal(violators.length, 1);
|
|
35
35
|
assert.equal(violators[0].identifier, '0');
|
|
@@ -39,32 +39,32 @@ test('listViolators returns 1 violator for linkNoText in the mixed report, exclu
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
test('listViolators returns 0 violators for an issue when all instances are cantTell', async () => {
|
|
42
|
-
const body = await response(['focusIndicationBad', '260101T0001
|
|
42
|
+
const body = await response(['focusIndicationBad', '260101T0001-ct']);
|
|
43
43
|
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
44
44
|
assert.equal(violators.length, 0);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
test('listViolators treats missing outcome as a violation', async () => {
|
|
48
|
-
const body = await response(['linkNoText', '260101T0002
|
|
48
|
+
const body = await response(['linkNoText', '260101T0002-no']);
|
|
49
49
|
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
50
50
|
assert.equal(violators.length, 1);
|
|
51
51
|
assert.equal(violators[0].identifier, '0');
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
test('listViolators returns an error for an unknown issue ID', async () => {
|
|
55
|
-
const body = await response(['nonexistentIssue', '260101T0000
|
|
55
|
+
const body = await response(['nonexistentIssue', '260101T0000-mix']);
|
|
56
56
|
const basics = body['response content']['basics about the issue'] as any;
|
|
57
57
|
assert.ok(basics.error);
|
|
58
58
|
});
|
|
59
59
|
|
|
60
60
|
test('listViolators returns an error for a nonexistent report', async () => {
|
|
61
|
-
const body = await response(['linkNoText', '999999T9999
|
|
61
|
+
const body = await response(['linkNoText', '999999T9999-xyz']);
|
|
62
62
|
const basics = body['response content']['basics about the report'] as any;
|
|
63
63
|
assert.ok(basics.error);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
test('listViolators includes reporter facts for the issue', async () => {
|
|
67
|
-
const body = await response(['linkNoText', '260101T0000
|
|
67
|
+
const body = await response(['linkNoText', '260101T0000-mix']);
|
|
68
68
|
const details = body['response content']['details about the issue'] as any;
|
|
69
69
|
const reporters = details['rule engines reporting violations belonging to the issue'];
|
|
70
70
|
const names = reporters.map((r: any) => r.name).sort();
|
|
@@ -72,7 +72,7 @@ test('listViolators includes reporter facts for the issue', async () => {
|
|
|
72
72
|
});
|
|
73
73
|
|
|
74
74
|
test('listViolators sorts by catalogIndex when two violators have the same reporter count', async () => {
|
|
75
|
-
const body = await response(['linkNoText', '260101T0008
|
|
75
|
+
const body = await response(['linkNoText', '260101T0008-mul']);
|
|
76
76
|
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
77
77
|
assert.equal(violators.length, 3);
|
|
78
78
|
// The violator with 2 reporters (catalogIndex 2) sorts first.
|
|
@@ -86,13 +86,13 @@ test('listViolators sorts by catalogIndex when two violators have the same repor
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
test('listViolators returns guideline layer for an issue with a short WCAG code', async () => {
|
|
89
|
-
const body = await response(['duplicateID', '260101T0009
|
|
89
|
+
const body = await response(['duplicateID', '260101T0009-brd']);
|
|
90
90
|
const issueBasics = body['response content']['basics about the issue'] as any;
|
|
91
91
|
assert.equal(issueBasics['related WCAG standard'].layer, 'guideline');
|
|
92
92
|
});
|
|
93
93
|
|
|
94
94
|
test('listViolators returns null tag name and text for a violator not in the catalog', async () => {
|
|
95
|
-
const body = await response(['duplicateID', '260101T0009
|
|
95
|
+
const body = await response(['duplicateID', '260101T0009-brd']);
|
|
96
96
|
const violators = body['response content']['basics about all elements exhibiting the issue'] as any;
|
|
97
97
|
const orphan = violators.find((v: any) => v.identifier === '3');
|
|
98
98
|
assert.ok(orphan);
|
|
@@ -101,7 +101,7 @@ test('listViolators returns null tag name and text for a violator not in the cat
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
test('listViolators returns an error for a falsy issue ID', async () => {
|
|
104
|
-
const body = await response(['', '260101T0009
|
|
104
|
+
const body = await response(['', '260101T0009-brd']);
|
|
105
105
|
const issueBasics = body['response content']['basics about the issue'] as any;
|
|
106
106
|
assert.ok(issueBasics.error);
|
|
107
107
|
});
|
package/api/listViolators.ts
CHANGED
|
@@ -26,13 +26,13 @@ type ResponseContent = z.infer<typeof listViolatorsResponseSchema>['response con
|
|
|
26
26
|
|
|
27
27
|
// Returns the response body.
|
|
28
28
|
export const response = async (args: string[]) => {
|
|
29
|
-
const [issueID = '',
|
|
29
|
+
const [issueID = '', reportID = ''] = args;
|
|
30
30
|
const thisHost = getThisHost();
|
|
31
31
|
// Get the report.
|
|
32
|
-
const report = await getReport(
|
|
32
|
+
const report = await getReport(reportID);
|
|
33
33
|
// Initialize the response content.
|
|
34
34
|
const responseContent: ResponseContent = {
|
|
35
|
-
'basics about the report': isReportError(report) ? report : await getReportBasics(
|
|
35
|
+
'basics about the report': isReportError(report) ? report : await getReportBasics(reportID),
|
|
36
36
|
'basics about the issue': {
|
|
37
37
|
'error': 'No information about the specified issue is available'
|
|
38
38
|
},
|
|
@@ -105,10 +105,10 @@ export const response = async (args: string[]) => {
|
|
|
105
105
|
'inner text': catalogItem?.text ?? null,
|
|
106
106
|
'count of rule engines reporting that the element exhibited the issue': reporters.size,
|
|
107
107
|
'how to get details about the element': {
|
|
108
|
-
URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${
|
|
108
|
+
URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${reportID}`,
|
|
109
109
|
'request method': 'GET' as const
|
|
110
110
|
},
|
|
111
|
-
'web users can get details about the element at': `${thisHost}/listDiagnoses.html/${issueID}/${
|
|
111
|
+
'web users can get details about the element at': `${thisHost}/listDiagnoses.html/${issueID}/${reportID}/${catalogIndex}`
|
|
112
112
|
};
|
|
113
113
|
});
|
|
114
114
|
// Add basics about the violators to the response content.
|
|
@@ -120,19 +120,19 @@ export const response = async (args: string[]) => {
|
|
|
120
120
|
'tool collection': getToolsFacts(),
|
|
121
121
|
'tool name': 'listViolators',
|
|
122
122
|
'this request': {
|
|
123
|
-
description: 'Provide details about one issue in one report, including basics about the elements of the tested page that exhibited the issue. The issueID
|
|
123
|
+
description: 'Provide details about one issue in one report, including basics about the elements of the tested page that exhibited the issue. The issueID and reportID parameters identify the issue and report that I want details about. Those parameters were in the response to my earlier listIssues request.',
|
|
124
124
|
method: 'GET',
|
|
125
|
-
URL: `${thisHost}/api/listViolators/${issueID}/${
|
|
125
|
+
URL: `${thisHost}/api/listViolators/${issueID}/${reportID}`,
|
|
126
126
|
'closest ancestor request': {
|
|
127
127
|
'tool name': 'listIssues',
|
|
128
128
|
description: 'Provide details about one report, including basics about the issues reported in it.',
|
|
129
129
|
method: 'GET',
|
|
130
|
-
URL: `${thisHost}/api/listIssues/${
|
|
130
|
+
URL: `${thisHost}/api/listIssues/${reportID}`
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
'URLs of similar requests for web users': {
|
|
134
|
-
'this request': `${thisHost}/listViolators.html/${issueID}/${
|
|
135
|
-
'closest ancestor request': `${thisHost}/listIssues.html/${
|
|
134
|
+
'this request': `${thisHost}/listViolators.html/${issueID}/${reportID}`,
|
|
135
|
+
'closest ancestor request': `${thisHost}/listIssues.html/${reportID}`
|
|
136
136
|
},
|
|
137
137
|
'response metadata': getResponseMetadata(),
|
|
138
138
|
'response content': responseContent
|
package/api/orderNewTest.test.ts
CHANGED
|
@@ -194,8 +194,7 @@ test('orderNewTest accepts a valid new page order and enqueues a job directly',
|
|
|
194
194
|
assert.equal(details['page to be tested'].description, 'Brand New Ordered Page');
|
|
195
195
|
const disposition = body['response content']['disposition of your order'] as any;
|
|
196
196
|
assert.ok(disposition['report identifier']);
|
|
197
|
-
assert.match(disposition['report identifier']
|
|
198
|
-
assert.match(disposition['report identifier'].jobID, /^[a-z0-9]{3}$/);
|
|
197
|
+
assert.match(disposition['report identifier'], /^\d{6}T\d{4}-[a-z0-9]{3}$/);
|
|
199
198
|
assert.ok(disposition['what happens next'].includes('Testing has begun.'));
|
|
200
199
|
assert.ok(disposition['do you want to wait for completion'].includes('awaitTest'));
|
|
201
200
|
const queuedFiles = await fs.readdir(queuePath);
|
package/api/orderNewTest.ts
CHANGED
|
@@ -88,12 +88,11 @@ export const response = async (args: string[]) => {
|
|
|
88
88
|
};
|
|
89
89
|
// Add information about the disposition of the order.
|
|
90
90
|
if (orderResult.result === 'ok') {
|
|
91
|
-
const [timeStamp, reportJobID] = orderResult.jobID.split('-') as [string, string];
|
|
92
91
|
requestDisposition = {
|
|
93
92
|
'what happens next': 'Testing has begun. Once it finishes, the report will be available via the listIssues tool.',
|
|
94
|
-
'report identifier':
|
|
95
|
-
'how you can check for completion': `Call the listIssues tool with
|
|
96
|
-
'do you want to wait for completion': 'Testing typically takes 2 to 4 minutes. Do you want to wait (and be notified of completion) instead of checking back later? If so, call the awaitTest tool with the same
|
|
93
|
+
'report identifier': orderResult.reportID,
|
|
94
|
+
'how you can check for completion': `Call the listIssues tool with reportID "${orderResult.reportID}" to learn whether the report is ready.`,
|
|
95
|
+
'do you want to wait for completion': 'Testing typically takes 2 to 4 minutes. Do you want to wait (and be notified of completion) instead of checking back later? If so, call the awaitTest tool with the same reportID.'
|
|
97
96
|
};
|
|
98
97
|
}
|
|
99
98
|
else {
|
package/api/orderRetest.test.ts
CHANGED
|
@@ -62,26 +62,26 @@ after(async () => {
|
|
|
62
62
|
// TESTS
|
|
63
63
|
|
|
64
64
|
test('orderRetest rejects a nonexistent report', async () => {
|
|
65
|
-
const body = await response(['251231T0000
|
|
65
|
+
const body = await response(['251231T0000-zzz', 'A reason that is long enough.']);
|
|
66
66
|
const details = body['response content']['details about your order'] as any;
|
|
67
67
|
assert.ok(details.error.includes('does not exist'));
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
test('orderRetest rejects a malformed timestamp or job identifier', async () => {
|
|
71
|
-
const body = await response(['999999T9999
|
|
71
|
+
const body = await response(['999999T9999-xyz', 'A reason that is long enough.']);
|
|
72
72
|
const details = body['response content']['details about your order'] as any;
|
|
73
73
|
assert.ok(details.error.includes('malformed'));
|
|
74
74
|
});
|
|
75
75
|
|
|
76
76
|
test('orderRetest rejects a reason shorter than 20 characters', async () => {
|
|
77
|
-
const body = await response(['260101T0004
|
|
77
|
+
const body = await response(['260101T0004-ord', 'short']);
|
|
78
78
|
const details = body['response content']['details about your order'] as any;
|
|
79
79
|
assert.ok(details.error.includes('reason'));
|
|
80
80
|
});
|
|
81
81
|
|
|
82
82
|
test('orderRetest rejects a reason longer than 100 characters', async () => {
|
|
83
83
|
const longReason = 'x'.repeat(101);
|
|
84
|
-
const body = await response(['260101T0004
|
|
84
|
+
const body = await response(['260101T0004-ord', longReason]);
|
|
85
85
|
const details = body['response content']['details about your order'] as any;
|
|
86
86
|
assert.ok(details.error.includes('reason'));
|
|
87
87
|
});
|
|
@@ -89,7 +89,7 @@ test('orderRetest rejects a reason longer than 100 characters', async () => {
|
|
|
89
89
|
test('orderRetest rejects a superseded report', async () => {
|
|
90
90
|
// 260101T0000-mix is an earlier report of "Mixed Outcomes" than 260202T0000-new,
|
|
91
91
|
// so it is superseded.
|
|
92
|
-
const body = await response(['260101T0000
|
|
92
|
+
const body = await response(['260101T0000-mix', 'A reason that is long enough.']);
|
|
93
93
|
const details = body['response content']['details about your order'] as any;
|
|
94
94
|
assert.equal(details.error, undefined);
|
|
95
95
|
const disposition = body['response content']['disposition of your order'] as any;
|
|
@@ -105,7 +105,7 @@ test('orderRetest rejects a report matching a claimed job by description', async
|
|
|
105
105
|
target: {what: 'Order Retest Fixture Page', url: 'https://example.com/unrelated'}
|
|
106
106
|
}));
|
|
107
107
|
try {
|
|
108
|
-
const body = await response(['260101T0004
|
|
108
|
+
const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
|
|
109
109
|
const details = body['response content']['details about your order'] as any;
|
|
110
110
|
assert.equal(details.error, undefined);
|
|
111
111
|
const disposition = body['response content']['disposition of your order'] as any;
|
|
@@ -125,7 +125,7 @@ test('orderRetest rejects a report matching a queued job by URL', async () => {
|
|
|
125
125
|
target: {what: 'Some Other Page', url: 'https://example.com/orderretestfixture'}
|
|
126
126
|
}));
|
|
127
127
|
try {
|
|
128
|
-
const body = await response(['260101T0004
|
|
128
|
+
const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
|
|
129
129
|
const details = body['response content']['details about your order'] as any;
|
|
130
130
|
assert.equal(details.error, undefined);
|
|
131
131
|
const disposition = body['response content']['disposition of your order'] as any;
|
|
@@ -144,7 +144,7 @@ test('orderRetest rejects an order once the job queue is full', async () => {
|
|
|
144
144
|
await fs.writeFile(fillerPath, JSON.stringify({target: {what: 'Filler', url: 'https://example.com/filler'}}));
|
|
145
145
|
process.env.JOB_QUEUE_MAX = '1';
|
|
146
146
|
try {
|
|
147
|
-
const body = await response(['260101T0004
|
|
147
|
+
const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
|
|
148
148
|
const details = body['response content']['details about your order'] as any;
|
|
149
149
|
assert.equal(details.error, undefined);
|
|
150
150
|
const disposition = body['response content']['disposition of your order'] as any;
|
|
@@ -156,7 +156,7 @@ test('orderRetest rejects an order once the job queue is full', async () => {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test('orderRetest accepts a valid retest order and enqueues a job directly', async () => {
|
|
159
|
-
const body = await response(['260101T0004
|
|
159
|
+
const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
|
|
160
160
|
const details = body['response content']['details about your order'] as any;
|
|
161
161
|
assert.equal(details.error, undefined);
|
|
162
162
|
assert.equal(details['page to be retested'].description, 'Order Retest Fixture Page');
|
|
@@ -172,7 +172,7 @@ test('orderRetest accepts a valid retest order and enqueues a job directly', asy
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
test('orderRetest response omits a web UI URL for this request but includes the ancestor', async () => {
|
|
175
|
-
const body = await response(['260101T0004
|
|
175
|
+
const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
|
|
176
176
|
const similarWeb = body['URLs of similar requests for web users'] as any;
|
|
177
177
|
assert.equal(similarWeb['this request'], null);
|
|
178
178
|
assert.ok(similarWeb['closest ancestor request'].includes('listIssues.html'));
|
package/api/orderRetest.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import {z} from 'zod';
|
|
9
9
|
import {getRequestFailureReason, getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
|
|
10
|
-
import {checkLength,
|
|
10
|
+
import {checkLength, isReportID, orderJob} from '../util.ts';
|
|
11
11
|
import {orderRetestResponseSchema} from './schemas.ts';
|
|
12
12
|
|
|
13
13
|
// TYPES
|
|
@@ -19,16 +19,16 @@ type ResponseContent = z.infer<typeof orderRetestResponseSchema>['response conte
|
|
|
19
19
|
|
|
20
20
|
// Returns the response body.
|
|
21
21
|
export const response = async (args: string[]) => {
|
|
22
|
-
const [
|
|
22
|
+
const [reportID = '', reason = ''] = args;
|
|
23
23
|
const thisHost = getThisHost();
|
|
24
24
|
const reasonCheck = checkLength(reason, 20, 100, 'reason');
|
|
25
25
|
// Initialize the response-content properties.
|
|
26
26
|
let requestDetails: ResponseContent['details about your order'];
|
|
27
27
|
let requestDisposition: ResponseContent['disposition of your order'] = null;
|
|
28
|
-
// If the
|
|
29
|
-
if (!
|
|
28
|
+
// If the report identifier is not syntactically valid:
|
|
29
|
+
if (!isReportID(reportID)) {
|
|
30
30
|
requestDetails = {
|
|
31
|
-
error: 'order invalid: the
|
|
31
|
+
error: 'order invalid: the reportID is malformed'
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
34
|
// Otherwise, if the encoded reason is too short or too long:
|
|
@@ -43,7 +43,7 @@ export const response = async (args: string[]) => {
|
|
|
43
43
|
// no manual-approval step, gated only by the lightweight checks orderJob itself
|
|
44
44
|
// performs (the target is not already claimed or queued, and the job queue is not
|
|
45
45
|
// already full). This resolves the cited report's description and URL itself.
|
|
46
|
-
const orderResult = await orderJob({
|
|
46
|
+
const orderResult = await orderJob({reportID}, reason);
|
|
47
47
|
// If the cited report does not exist:
|
|
48
48
|
if (orderResult.result === 'nonreport') {
|
|
49
49
|
requestDetails = {
|
|
@@ -63,12 +63,11 @@ export const response = async (args: string[]) => {
|
|
|
63
63
|
};
|
|
64
64
|
// Add information about the disposition of the order.
|
|
65
65
|
if (orderResult.result === 'ok') {
|
|
66
|
-
const [reportTimeStamp, reportJobID] = orderResult.jobID.split('-') as [string, string];
|
|
67
66
|
requestDisposition = {
|
|
68
67
|
'what happens next': 'Testing has begun. Once it finishes, the report will be available via the listIssues tool.',
|
|
69
|
-
'report identifier':
|
|
70
|
-
'how you can check for completion': `Call the listIssues tool with
|
|
71
|
-
'do you want to wait for completion': 'Testing typically takes 2 to 4 minutes. Do you want to wait (and be notified of completion) instead of checking back later? If so, call the awaitTest tool with the same
|
|
68
|
+
'report identifier': orderResult.reportID,
|
|
69
|
+
'how you can check for completion': `Call the listIssues tool with reportID "${orderResult.reportID}" to learn whether the report is ready.`,
|
|
70
|
+
'do you want to wait for completion': 'Testing typically takes 2 to 4 minutes. Do you want to wait (and be notified of completion) instead of checking back later? If so, call the awaitTest tool with the same reportID.'
|
|
72
71
|
};
|
|
73
72
|
}
|
|
74
73
|
else {
|
|
@@ -94,9 +93,9 @@ export const response = async (args: string[]) => {
|
|
|
94
93
|
'tool collection': getToolsFacts(),
|
|
95
94
|
'tool name': 'orderRetest',
|
|
96
95
|
'this request': {
|
|
97
|
-
description: 'Process my order to retest a page, immediately and without manual approval. The
|
|
96
|
+
description: 'Process my order to retest a page, immediately and without manual approval. The reportID parameter identifies the latest available report about the page. It was in the response to my earlier listIssues request. The reason property of the request body is the reason why the page should be retested.',
|
|
98
97
|
method: 'POST',
|
|
99
|
-
URL: `${thisHost}/api/orderRetest/${
|
|
98
|
+
URL: `${thisHost}/api/orderRetest/${reportID}`,
|
|
100
99
|
body: {
|
|
101
100
|
reason
|
|
102
101
|
},
|
|
@@ -104,12 +103,12 @@ export const response = async (args: string[]) => {
|
|
|
104
103
|
'tool name': 'listIssues',
|
|
105
104
|
description: 'Provide details about one report, including basics about the issues reported in it.',
|
|
106
105
|
method: 'GET',
|
|
107
|
-
URL: `${thisHost}/api/listIssues/${
|
|
106
|
+
URL: `${thisHost}/api/listIssues/${reportID}`
|
|
108
107
|
}
|
|
109
108
|
},
|
|
110
109
|
'URLs of similar requests for web users': {
|
|
111
110
|
'this request': null,
|
|
112
|
-
'closest ancestor request': `${thisHost}/listIssues.html/${
|
|
111
|
+
'closest ancestor request': `${thisHost}/listIssues.html/${reportID}`
|
|
113
112
|
},
|
|
114
113
|
'response metadata': getResponseMetadata(),
|
|
115
114
|
'response content': responseContent
|
|
@@ -71,14 +71,14 @@ after(async () => {
|
|
|
71
71
|
// TESTS
|
|
72
72
|
|
|
73
73
|
test('requestRetest rejects a nonexistent report', async () => {
|
|
74
|
-
const body = await response(['251231T0000
|
|
74
|
+
const body = await response(['251231T0000-zzz', 'A reason that is long enough.']);
|
|
75
75
|
const details = body['response content']['details about your request'] as any;
|
|
76
76
|
assert.ok(details.error.includes('does not exist'));
|
|
77
77
|
assert.ok(!logged.some(line => line.includes('request for a retest awaits approval')));
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
test('requestRetest rejects a malformed timestamp or job identifier', async () => {
|
|
81
|
-
const body = await response(['999999T9999
|
|
81
|
+
const body = await response(['999999T9999-xyz', 'A reason that is long enough.']);
|
|
82
82
|
const details = body['response content']['details about your request'] as any;
|
|
83
83
|
assert.ok(details.error.includes('malformed'));
|
|
84
84
|
assert.ok(!logged.some(line => line.includes('request for a retest awaits approval')));
|
|
@@ -88,7 +88,7 @@ test('requestRetest rejects a superseded report', async () => {
|
|
|
88
88
|
// 260101T0000-mix is an earlier report of "Mixed Outcomes" than 260202T0000-new,
|
|
89
89
|
// so it is superseded and processTestRequest returns 'superseded', reported via the
|
|
90
90
|
// disposition rather than details.error.
|
|
91
|
-
const body = await response(['260101T0000
|
|
91
|
+
const body = await response(['260101T0000-mix', 'A reason that is long enough.']);
|
|
92
92
|
const details = body['response content']['details about your request'] as any;
|
|
93
93
|
assert.equal(details.error, undefined);
|
|
94
94
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -99,7 +99,7 @@ test('requestRetest rejects a superseded report', async () => {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
102
|
-
const body = await response(['260101T0003
|
|
102
|
+
const body = await response(['260101T0003-ret', 'short']);
|
|
103
103
|
const details = body['response content']['details about your request'] as any;
|
|
104
104
|
assert.ok(details.error.includes('reason'));
|
|
105
105
|
assert.ok(!logged.some(line => line.includes('request for a retest awaits approval')));
|
|
@@ -107,7 +107,7 @@ test('requestRetest rejects a reason shorter than 20 characters', async () => {
|
|
|
107
107
|
|
|
108
108
|
test('requestRetest rejects a reason longer than 100 characters', async () => {
|
|
109
109
|
const longReason = 'x'.repeat(101);
|
|
110
|
-
const body = await response(['260101T0003
|
|
110
|
+
const body = await response(['260101T0003-ret', longReason]);
|
|
111
111
|
const details = body['response content']['details about your request'] as any;
|
|
112
112
|
assert.ok(details.error.includes('reason'));
|
|
113
113
|
assert.ok(!logged.some(line => line.includes('request for a retest awaits approval')));
|
|
@@ -120,7 +120,7 @@ test('requestRetest rejects a report matching a claimed job by description', asy
|
|
|
120
120
|
target: {what: 'Retest Fixture Page', url: 'https://example.com/unrelated'}
|
|
121
121
|
}));
|
|
122
122
|
try {
|
|
123
|
-
const body = await response(['260101T0003
|
|
123
|
+
const body = await response(['260101T0003-ret', 'A reason that is long enough.']);
|
|
124
124
|
const details = body['response content']['details about your request'] as any;
|
|
125
125
|
assert.equal(details.error, undefined);
|
|
126
126
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -141,7 +141,7 @@ test('requestRetest rejects a report matching a queued job by URL', async () =>
|
|
|
141
141
|
target: {what: 'Some Other Page', url: 'https://example.com/retestfixture'}
|
|
142
142
|
}));
|
|
143
143
|
try {
|
|
144
|
-
const body = await response(['260101T0003
|
|
144
|
+
const body = await response(['260101T0003-ret', 'A reason that is long enough.']);
|
|
145
145
|
const details = body['response content']['details about your request'] as any;
|
|
146
146
|
assert.equal(details.error, undefined);
|
|
147
147
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -156,8 +156,8 @@ test('requestRetest rejects a report matching a queued job by URL', async () =>
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
test('requestRetest rejects a duplicate request', async () => {
|
|
159
|
-
await response(['260101T0003
|
|
160
|
-
const body = await response(['260101T0003
|
|
159
|
+
await response(['260101T0003-ret', 'A reason that is long enough.']);
|
|
160
|
+
const body = await response(['260101T0003-ret', 'A reason that is long enough.']);
|
|
161
161
|
const details = body['response content']['details about your request'] as any;
|
|
162
162
|
assert.equal(details.error, undefined);
|
|
163
163
|
const disposition = body['response content']['disposition of your request'] as any;
|
|
@@ -165,7 +165,7 @@ test('requestRetest rejects a duplicate request', async () => {
|
|
|
165
165
|
});
|
|
166
166
|
|
|
167
167
|
test('requestRetest accepts a valid retest request for the latest report of a page', async () => {
|
|
168
|
-
const body = await response(['260202T0000
|
|
168
|
+
const body = await response(['260202T0000-new', 'A reason that is long enough.']);
|
|
169
169
|
const details = body['response content']['details about your request'] as any;
|
|
170
170
|
assert.equal(details.error, undefined);
|
|
171
171
|
assert.equal(details['page to be retested'].description, 'Mixed Outcomes');
|
|
@@ -185,7 +185,7 @@ test('requestRetest rejects a request with a fallback channel when the queue is
|
|
|
185
185
|
timeStamp: '260101T0000', description: `Filler Page ${i}`, reason: 'Filler reason.'
|
|
186
186
|
}));
|
|
187
187
|
await fs.writeFile(testRequestsPath, JSON.stringify({'https://example.com/filler': filler}));
|
|
188
|
-
const body = await response(['260101T0003
|
|
188
|
+
const body = await response(['260101T0003-ret', 'A reason that is long enough.']);
|
|
189
189
|
const details = body['response content']['details about your request'] as any;
|
|
190
190
|
assert.equal(details.error, undefined);
|
|
191
191
|
const disposition = body['response content']['disposition of your request'] as any;
|