@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.
Files changed (64) hide show
  1. package/README.md +2 -5
  2. package/api/awaitTest.test.ts +9 -9
  3. package/api/awaitTest.ts +12 -12
  4. package/api/getReport.test.ts +4 -4
  5. package/api/getReport.ts +9 -9
  6. package/api/listDiagnoses.test.ts +14 -14
  7. package/api/listDiagnoses.ts +8 -8
  8. package/api/listIssues.test.ts +15 -15
  9. package/api/listIssues.ts +18 -20
  10. package/api/listPages.test.ts +2 -2
  11. package/api/listPages.ts +4 -4
  12. package/api/listViolators.test.ts +10 -10
  13. package/api/listViolators.ts +10 -10
  14. package/api/orderNewTest.test.ts +1 -2
  15. package/api/orderNewTest.ts +3 -4
  16. package/api/orderRetest.test.ts +10 -10
  17. package/api/orderRetest.ts +13 -14
  18. package/api/requestRetest.test.ts +11 -11
  19. package/api/requestRetest.ts +11 -11
  20. package/api/routes.ts +9 -11
  21. package/api/schemas.ts +18 -28
  22. package/api/util.test.ts +2 -2
  23. package/api/util.ts +7 -7
  24. package/capability.md +4 -4
  25. package/docs/AI-TOOLS.md +45 -5
  26. package/docs/decisions/009-report-id.md +16 -0
  27. package/index.test.ts +91 -41
  28. package/index.ts +66 -15
  29. package/lhm.plugin.json +33 -119
  30. package/llms-full.txt +18 -24
  31. package/llms.txt +12 -12
  32. package/mcp.test.ts +11 -20
  33. package/mcp.ts +18 -18
  34. package/openapi-3.0.yaml +1847 -0
  35. package/openapi.yaml +38 -97
  36. package/package.json +8 -2
  37. package/routing.test.ts +1 -1
  38. package/scripts/generate-openapi.ts +83 -1
  39. package/scripts/postPR.ts +89 -0
  40. package/scripts/prePR.ts +64 -0
  41. package/server.json +1 -1
  42. package/util.test.ts +32 -33
  43. package/util.ts +47 -43
  44. package/web/hideReportForm/index.ts +5 -6
  45. package/web/listDiagnoses/index.ts +4 -5
  46. package/web/listDiagnoses/listDiagnoses.test.ts +13 -13
  47. package/web/listIssues/index.ts +11 -12
  48. package/web/listIssues/listIssues.test.ts +24 -24
  49. package/web/listPages/index.ts +2 -2
  50. package/web/listPages/listPages.test.ts +2 -2
  51. package/web/listTopIssues/index.ts +2 -2
  52. package/web/listViolators/index.ts +5 -6
  53. package/web/listViolators/listViolators.test.ts +12 -12
  54. package/web/reannotate/index.ts +3 -3
  55. package/web/reannotateForm/index.ts +1 -2
  56. package/web/reportDeletion.ts +9 -12
  57. package/web/requestRetest/index.ts +2 -2
  58. package/web/requestRetest/requestRetest.test.ts +7 -7
  59. package/web/requestRetestForm/index.html +2 -2
  60. package/web/requestRetestForm/index.ts +3 -4
  61. package/web/requestRetestForm/requestRetestForm.test.ts +7 -8
  62. package/web/unhideReportForm/index.ts +4 -6
  63. package/web/util.test.ts +29 -29
  64. 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 [timeStamp = '', jobID = ''] = args;
39
+ const [reportID = ''] = args;
40
40
  const thisHost = getThisHost();
41
41
  // Get the report.
42
- const report = await getReport(timeStamp, jobID);
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(timeStamp, jobID);
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 extractTimeStamp = extract.timeStamp;
72
- const extractJobID = extract.jobID;
71
+ const extractID = extract.id;
73
72
  return {
74
- identifier: `${extractTimeStamp}-${extractJobID}`,
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/${extractTimeStamp}/${extractJobID}`
81
+ URL: `${thisHost}/api/listIssues/${extractID}`
84
82
  },
85
- 'web users can get details about the report at': `${thisHost}/listIssues.html/${extractTimeStamp}/${extractJobID}`
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(timeStamp, jobID);
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/${timeStamp}/${jobID}`
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/${timeStamp}/${jobID}`
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/${timeStamp}/${jobID}`,
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/${timeStamp}/${jobID}`
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}/${timeStamp}/${jobID}`
224
+ URL: `${thisHost}/api/listViolators/${id}/${reportID}`
227
225
  },
228
- 'web users can get details about the issue at': `${thisHost}/listViolators.html/${id}/${timeStamp}/${jobID}`
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 timeStamp and jobID parameters identify the report that I want details about. Those parameters were in the response to my earlier listPages request, or in the history of reports about the page in an earlier listIssues response.',
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/${timeStamp}/${jobID}`,
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/${timeStamp}/${jobID}`,
249
+ 'this request': `${thisHost}/listIssues.html/${reportID}`,
252
250
  'closest ancestor request': `${thisHost}/listPages.html`
253
251
  },
254
252
  'response metadata': getResponseMetadata(),
@@ -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/new'));
67
- assert.ok(mixed['web users can get details about the latest report at'].endsWith('/listIssues.html/260202T0000/new'));
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, jobID, reportTime, superseded, timeStamp, url} = extract;
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: `${timeStamp}-${jobID}`,
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/${timeStamp}/${jobID}`
51
+ URL: `${thisHost}/api/listIssues/${id}`
52
52
  },
53
- 'web users can get details about the latest report at': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
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', 'mix']);
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', 'ct']);
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', 'no']);
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', 'mix']);
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', 'xyz']);
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', 'mix']);
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', 'mul']);
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', 'brd']);
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', 'brd']);
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', 'brd']);
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
  });
@@ -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 = '', timeStamp = '', jobID = ''] = args;
29
+ const [issueID = '', reportID = ''] = args;
30
30
  const thisHost = getThisHost();
31
31
  // Get the report.
32
- const report = await getReport(timeStamp, jobID);
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(timeStamp, jobID),
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}/${timeStamp}/${jobID}`,
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}/${timeStamp}/${jobID}/${catalogIndex}`
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, timeStamp, and jobID parameters identify the issue and report that I want details about. Those parameters were in the response to my earlier listIssues 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 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}/${timeStamp}/${jobID}`,
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/${timeStamp}/${jobID}`
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}/${timeStamp}/${jobID}`,
135
- 'closest ancestor request': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
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
@@ -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'].timeStamp, /^\d{6}T\d{4}$/);
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);
@@ -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': {timeStamp, jobID: reportJobID},
95
- 'how you can check for completion': `Call the listIssues tool with timeStamp "${timeStamp}" and jobID "${reportJobID}" to learn whether the report is ready.`,
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 timeStamp and jobID.'
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 {
@@ -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', 'zzz', 'A reason that is long enough.']);
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', 'xyz', 'A reason that is long enough.']);
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', 'ord', 'short']);
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', 'ord', longReason]);
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', 'mix', 'A reason that is long enough.']);
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', 'ord', 'A reason that is long enough.']);
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', 'ord', 'A reason that is long enough.']);
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', 'ord', 'A reason that is long enough.']);
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', 'ord', 'A reason that is long enough.']);
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', 'ord', 'A reason that is long enough.']);
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'));
@@ -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, isJobID, isTimeStamp, orderJob} from '../util.ts';
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 [timeStamp = '', jobID = '', reason = ''] = args;
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 timestamp or job ID is not syntactically valid:
29
- if (!isTimeStamp(timeStamp) || !isJobID(jobID)) {
28
+ // If the report identifier is not syntactically valid:
29
+ if (!isReportID(reportID)) {
30
30
  requestDetails = {
31
- error: 'order invalid: the report timestamp or job identifier is malformed'
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({timeStamp, jobID}, reason);
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': {timeStamp: reportTimeStamp, jobID: reportJobID},
70
- 'how you can check for completion': `Call the listIssues tool with timeStamp "${reportTimeStamp}" and jobID "${reportJobID}" to learn whether the report is ready.`,
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 timeStamp and jobID.'
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 timeStamp and jobID parameters identify the latest available report about the page. Those parameters were in the response to my earlier listIssues request. The reason property of the request body is the reason why the page should be retested.',
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/${timeStamp}/${jobID}`,
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/${timeStamp}/${jobID}`
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/${timeStamp}/${jobID}`
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', 'zzz', 'A reason that is long enough.']);
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', 'xyz', 'A reason that is long enough.']);
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', 'mix', 'A reason that is long enough.']);
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', 'ret', 'short']);
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', 'ret', longReason]);
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', 'ret', 'A reason that is long enough.']);
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', 'ret', 'A reason that is long enough.']);
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', 'ret', 'A reason that is long enough.']);
160
- const body = await response(['260101T0003', 'ret', 'A reason that is long enough.']);
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', 'new', 'A reason that is long enough.']);
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', 'ret', 'A reason that is long enough.']);
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;