@jrpool/kilotest 58.2.4 → 59.0.2

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 (66) hide show
  1. package/api/getReport.test.ts +1 -1
  2. package/api/listIssues.test.ts +32 -0
  3. package/api/listIssues.ts +38 -8
  4. package/api/listPages.test.ts +92 -0
  5. package/api/listPages.ts +93 -0
  6. package/api/orderNewTest.test.ts +2 -2
  7. package/api/orderNewTest.ts +4 -4
  8. package/api/orderRetest.test.ts +1 -1
  9. package/api/requestNewTest.test.ts +1 -1
  10. package/api/requestNewTest.ts +6 -6
  11. package/api/requestRetest.test.ts +3 -3
  12. package/api/requestRetest.ts +2 -2
  13. package/api/routes.test.ts +1 -1
  14. package/api/routes.ts +5 -5
  15. package/api/schemas.ts +24 -7
  16. package/api/util.ts +26 -45
  17. package/apiIntegration.test.ts +5 -5
  18. package/docs/AI-TOOLS.md +1 -1
  19. package/docs/SERVICE.md +16 -5
  20. package/index.test.ts +31 -19
  21. package/index.ts +25 -13
  22. package/lhm.plugin.json +1433 -141
  23. package/linkValidation.test.ts +2 -2
  24. package/llms-full.txt +9 -9
  25. package/llms.txt +4 -4
  26. package/mcp.test.ts +9 -9
  27. package/mcp.ts +14 -14
  28. package/openapi.yaml +84 -30
  29. package/package.json +1 -1
  30. package/routing.test.ts +5 -5
  31. package/server.json +1 -1
  32. package/sitemap.xml +1 -1
  33. package/smokeTest.ts +2 -2
  34. package/style.css +8 -8
  35. package/test/fixtures/buildFixtures.ts +3 -3
  36. package/test/fixtures/db/reports/260101T0000-mix.json +2 -2
  37. package/test/fixtures/db/reports/260202T0000-new.json +2 -2
  38. package/util.test.ts +31 -22
  39. package/util.ts +16 -5
  40. package/web/enqueueForm/index.html +1 -1
  41. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  42. package/web/hideReportForm/index.ts +9 -6
  43. package/web/home/home.test.ts +19 -0
  44. package/{index.html → web/home/index.html} +2 -2
  45. package/web/home/index.ts +20 -0
  46. package/web/listDiagnoses/listDiagnoses.test.ts +1 -1
  47. package/web/listIssues/index.html +9 -10
  48. package/web/listIssues/index.ts +45 -3
  49. package/web/listIssues/listIssues.test.ts +82 -6
  50. package/web/{listReports → listPages}/index.html +1 -2
  51. package/web/listPages/index.ts +99 -0
  52. package/web/listPages/listPages.test.ts +196 -0
  53. package/web/listViolators/listViolators.test.ts +1 -1
  54. package/web/metrics/metrics.test.ts +2 -2
  55. package/web/requestNewTest/requestNewTest.test.ts +1 -1
  56. package/web/requestRetest/requestRetest.test.ts +1 -2
  57. package/web/requestRetestForm/index.ts +4 -2
  58. package/web/requestRetestForm/requestRetestForm.test.ts +12 -1
  59. package/web/unhideReportForm/index.ts +9 -6
  60. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  61. package/web/util.test.ts +87 -23
  62. package/web/util.ts +27 -31
  63. package/api/listReports.test.ts +0 -109
  64. package/api/listReports.ts +0 -87
  65. package/web/listReports/index.ts +0 -182
  66. package/web/listReports/listReports.test.ts +0 -300
@@ -35,7 +35,7 @@ test('getReport returns the full report for a valid report', async () => {
35
35
  assert.ok((content['size of the report in bytes'] as number) > 0);
36
36
  const report = content['full report'] as any;
37
37
  assert.equal(report.id, '260101T0000-mix');
38
- assert.equal(report.target.what, 'Mixed Outcomes Page');
38
+ assert.equal(report.target.what, 'Mixed Outcomes');
39
39
  assert.equal(report.target.url, 'https://example.com/mixed');
40
40
  assert.equal(report.acts.length, 2);
41
41
  });
@@ -113,3 +113,35 @@ test('listIssues handles instances with missing issueID and null instances witho
113
113
  const ids = issues.map(i => i.identifier);
114
114
  assert.ok(ids.includes('duplicateID'));
115
115
  });
116
+
117
+ test('listIssues lists the history of reports about the page, from oldest to latest', async () => {
118
+ const body = await response(['260101T0000', 'mix']);
119
+ const history = body['response content']['history of reports about the page']!;
120
+ assert.deepEqual(history.map(entry => entry.identifier), ['260101T0000-mix', '260202T0000-new']);
121
+ assert.deepEqual(
122
+ history.map(entry => [
123
+ entry['whether it is the oldest report about the page'],
124
+ entry['whether it is the latest report about the page'],
125
+ entry['whether it is the report described in this response']
126
+ ]),
127
+ [[true, false, true], [false, true, false]]
128
+ );
129
+ assert.equal(history[1]!['completion date and time'], '2026-02-02T00:10:00.000Z');
130
+ assert.equal(typeof history[1]!['days since the report was completed'], 'number');
131
+ assert.ok(history[1]!['how to get details about the report'].URL.endsWith('/api/listIssues/260202T0000/new'));
132
+ assert.ok(history[1]!['web users can get details about the report at'].endsWith('/listIssues.html/260202T0000/new'));
133
+ });
134
+
135
+ test('listIssues gives a one-report history for the only report about a page', async () => {
136
+ const body = await response(['260101T0001', 'ct']);
137
+ const history = body['response content']['history of reports about the page']!;
138
+ assert.equal(history.length, 1);
139
+ assert.equal(history[0]!['whether it is the oldest report about the page'], true);
140
+ assert.equal(history[0]!['whether it is the latest report about the page'], true);
141
+ assert.equal(history[0]!['whether it is the report described in this response'], true);
142
+ });
143
+
144
+ test('listIssues gives no history for a nonexistent report', async () => {
145
+ const body = await response(['999999T9999', 'xyz']);
146
+ assert.equal(body['response content']['history of reports about the page'], null);
147
+ });
package/api/listIssues.ts CHANGED
@@ -16,7 +16,14 @@ import {
16
16
  getThisHost
17
17
  } from './util.ts';
18
18
  import {
19
- getReport, getReportStats, getTestActInstances, getTestActs, isReportError, objectSort
19
+ getAgoDays,
20
+ getPageReportExtracts,
21
+ getReport,
22
+ getReportStats,
23
+ getTestActInstances,
24
+ getTestActs,
25
+ isReportError,
26
+ objectSort
20
27
  } from '../util.ts';
21
28
  import {listIssuesResponseSchema} from './schemas.ts';
22
29
 
@@ -38,6 +45,7 @@ export const response = async (args: string[]) => {
38
45
  // Initialize the response content.
39
46
  const responseContent: ResponseContent = {
40
47
  'basics about the report': reportBasics,
48
+ 'history of reports about the page': null,
41
49
  'details about the report': null,
42
50
  'how to request that the page be retested': null,
43
51
  'how a web user can request that the page be retested': null,
@@ -55,6 +63,28 @@ export const response = async (args: string[]) => {
55
63
  device = {id: 'default'},
56
64
  browserID = null
57
65
  } = report;
66
+ // Get extracts of all reports about the page, from oldest to latest.
67
+ const pageExtracts = await getPageReportExtracts(report.target.what);
68
+ const lastIndex = pageExtracts.length - 1;
69
+ // Add the history of reports about the page to the response content.
70
+ responseContent['history of reports about the page'] = pageExtracts.map((extract, index) => {
71
+ const extractTimeStamp = extract.timeStamp;
72
+ const extractJobID = extract.jobID;
73
+ return {
74
+ identifier: `${extractTimeStamp}-${extractJobID}`,
75
+ 'completion date and time': extract.reportTime,
76
+ 'days since the report was completed': getAgoDays(new Date(extract.reportTime)),
77
+ 'whether it is the oldest report about the page': index === 0,
78
+ '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,
81
+ 'how to get details about the report': {
82
+ method: 'GET' as const,
83
+ URL: `${thisHost}/api/listIssues/${extractTimeStamp}/${extractJobID}`
84
+ },
85
+ 'web users can get details about the report at': `${thisHost}/listIssues.html/${extractTimeStamp}/${extractJobID}`
86
+ };
87
+ });
58
88
  // Get details about the job definition.
59
89
  const jobDefinitionDetails = {
60
90
  'whether the job prohibited redirection': strict,
@@ -159,7 +189,7 @@ export const response = async (args: string[]) => {
159
189
  if (reportBasics['whether a later report about the same page exists']) {
160
190
  // Add information about requesting a retest to the response content.
161
191
  responseContent['how to request that the page be retested'] = {
162
- notice: 'A later report about the same page already exists. The listIssues output about that report includes instructions for requesting a retest.'
192
+ notice: 'A later report about the same page already exists (see the history of reports about the page). The listIssues output about the latest report includes instructions for requesting a retest.'
163
193
  };
164
194
  }
165
195
  // Otherwise, i.e. if the report has not been superseded:
@@ -172,7 +202,7 @@ export const response = async (args: string[]) => {
172
202
  reason: '20- to 100-character reason why the page should be retested'
173
203
  },
174
204
  'how to check whether the request has been fulfilled':
175
- 'use the listReports tool to determine whether a report about the page has become available (typical wait time: 1 hour to 1 day)'
205
+ 'use the listPages tool to determine whether a later report about the page has become available (typical wait time: 1 hour to 1 day)'
176
206
  };
177
207
  // Add instructions for a web user to request a retest.
178
208
  responseContent['how a web user can request that the page be retested'] = {
@@ -207,19 +237,19 @@ export const response = async (args: string[]) => {
207
237
  'tool collection': getToolsFacts(),
208
238
  'tool name': 'listIssues',
209
239
  'this request': {
210
- 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 listReports 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.',
211
241
  method: 'GET',
212
242
  URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`,
213
243
  'closest ancestor request': {
214
- 'tool name': 'listReports',
215
- description: 'Provide basics about all available reports.',
244
+ 'tool name': 'listPages',
245
+ description: 'Provide basics about all tested pages.',
216
246
  method: 'GET',
217
- URL: `${thisHost}/api/listReports`,
247
+ URL: `${thisHost}/api/listPages`,
218
248
  }
219
249
  },
220
250
  'URLs of similar requests for web users': {
221
251
  'this request': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`,
222
- 'closest ancestor request': `${thisHost}/listReports.html`
252
+ 'closest ancestor request': `${thisHost}/listPages.html`
223
253
  },
224
254
  'response metadata': getResponseMetadata(),
225
255
  'response content': responseContent
@@ -0,0 +1,92 @@
1
+ /*
2
+ listPages.test.ts
3
+ Tests for api/listPages.ts using the fixture corpus.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {test, before, after} from 'node:test';
9
+ import assert from 'node:assert/strict';
10
+ import {response} from './listPages.ts';
11
+
12
+ // SETUP AND TEARDOWN
13
+
14
+ const savedDBDir = process.env.DB_DIR;
15
+
16
+ before(async () => {
17
+ process.env.DB_DIR = (await import('../test/dbFixture.ts')).fixtureDBDir;
18
+ });
19
+
20
+ after(() => {
21
+ if (savedDBDir !== undefined) {
22
+ process.env.DB_DIR = savedDBDir;
23
+ }
24
+ else {
25
+ delete process.env.DB_DIR;
26
+ }
27
+ });
28
+
29
+ // TESTS
30
+
31
+ test('listPages returns one entry per tested page, sorted by page description', async () => {
32
+ const body = await response();
33
+ const pagesBasics = body['response content']['basics about all tested pages'];
34
+ const descriptions = pagesBasics.map(b => b['tested web page'].description);
35
+ assert.deepEqual(
36
+ descriptions,
37
+ [
38
+ 'All CantTell Page',
39
+ 'Branch Coverage Page',
40
+ 'Empty Results Page',
41
+ 'Mixed Outcomes',
42
+ 'Multi Violator Page',
43
+ 'No Outcomes Page',
44
+ 'Prevented Page'
45
+ ]
46
+ );
47
+ });
48
+
49
+ test('listPages does not include the page of the hidden report', async () => {
50
+ const body = await response();
51
+ const pagesBasics = body['response content']['basics about all tested pages'];
52
+ const ids = pagesBasics.map(b => b['basics about the latest report'].identifier);
53
+ assert.ok(!ids.includes('260101T0007-hid'), 'hidden report must not appear in listPages');
54
+ });
55
+
56
+ test('listPages describes a multi-report page by its latest report and its report count', async () => {
57
+ const body = await response();
58
+ const pagesBasics = body['response content']['basics about all tested pages'];
59
+ const mixed = pagesBasics.find(b => b['tested web page'].description === 'Mixed Outcomes')!;
60
+ assert.equal(mixed['number of reports about the page'], 2);
61
+ const latest = mixed['basics about the latest report'];
62
+ assert.equal(latest.identifier, '260202T0000-new');
63
+ assert.equal(latest['completion date and time'], '2026-02-02T00:10:00.000Z');
64
+ assert.equal(typeof latest['days since the report was completed'], 'number');
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'));
68
+ });
69
+
70
+ test('listPages describes a single-report page', async () => {
71
+ const body = await response();
72
+ const pagesBasics = body['response content']['basics about all tested pages'];
73
+ const canttell = pagesBasics.find(b => b['tested web page'].description === 'All CantTell Page')!;
74
+ assert.equal(canttell['number of reports about the page'], 1);
75
+ assert.equal(canttell['tested web page'].URL, 'https://example.com/canttell');
76
+ assert.equal(canttell['basics about the latest report'].identifier, '260101T0001-ct');
77
+ });
78
+
79
+ test('listPages includes request-test instructions', async () => {
80
+ const body = await response();
81
+ const content = body['response content'];
82
+ assert.ok(content['how to request that a page with no report be tested']);
83
+ assert.ok(content['how a web user can request that the page be tested']);
84
+ assert.equal(content['how to request that a page with no report be tested'].method, 'POST');
85
+ });
86
+
87
+ test('listPages includes tool name and metadata', async () => {
88
+ const body = await response();
89
+ assert.equal(body['tool name'], 'listPages');
90
+ assert.ok(body['response metadata'].identifier);
91
+ assert.ok(body['response metadata']['date and time']);
92
+ });
@@ -0,0 +1,93 @@
1
+ /*
2
+ listPages.ts
3
+ Returns basics about all tested pages.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {z} from 'zod';
9
+ import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
10
+ import {getAgoDays, getReportExtracts} from '../util.ts';
11
+ import {listPagesResponseSchema} from './schemas.ts';
12
+
13
+ // TYPES
14
+
15
+ // The response content defined by the response schema.
16
+ type ResponseContent = z.infer<typeof listPagesResponseSchema>['response content'];
17
+
18
+ // FUNCTIONS
19
+
20
+ // Returns the response body.
21
+ export const response = async () => {
22
+ const thisHost = getThisHost();
23
+ // Get extracts of all available reports, sorted by page description and completion time.
24
+ const reportExtracts = await getReportExtracts();
25
+ // Get the number of reports about each page.
26
+ const reportCounts = new Map<string, number>();
27
+ reportExtracts.forEach(({description}) => {
28
+ reportCounts.set(description, (reportCounts.get(description) ?? 0) + 1);
29
+ });
30
+ // Initialize an array of basics about the pages.
31
+ const pagesBasics: ResponseContent['basics about all tested pages'] = [];
32
+ // For each report:
33
+ reportExtracts.forEach(extract => {
34
+ const {description, jobID, reportTime, superseded, timeStamp, url} = extract;
35
+ // If it is the latest report about its page:
36
+ if (!superseded) {
37
+ // Add basics about the page, with instructions for getting details, to the array.
38
+ pagesBasics.push({
39
+ 'tested web page': {
40
+ description,
41
+ URL: url
42
+ },
43
+ 'number of reports about the page': reportCounts.get(description)!,
44
+ 'basics about the latest report': {
45
+ identifier: `${timeStamp}-${jobID}`,
46
+ 'completion date and time': reportTime,
47
+ 'days since the report was completed': getAgoDays(new Date(reportTime))
48
+ },
49
+ 'how to get details about the latest report': {
50
+ method: 'GET',
51
+ URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
52
+ },
53
+ 'web users can get details about the latest report at': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
54
+ });
55
+ }
56
+ });
57
+ // Create the response content.
58
+ const responseContent: ResponseContent = {
59
+ 'basics about all tested pages': pagesBasics,
60
+ 'how to request that a page with no report be tested': {
61
+ method: 'POST',
62
+ URL: `${thisHost}/api/requestNewTest`,
63
+ 'request body': {
64
+ description: '10- to 100-character description of the page conforming to the naming convention used in this list of pages',
65
+ URL: '12- to 300-character URL of the page, including the https:// scheme and any query',
66
+ reason: '20- to 100-character reason why the page should be tested'
67
+ },
68
+ 'how to check whether the request has been fulfilled': 'use this listPages tool to determine whether the page has been tested (typical wait time: 1 hour to 1 day)'
69
+ },
70
+ 'how a web user can request that the page be tested': {
71
+ URL: `${thisHost}/requestNewTestForm.html`
72
+ }
73
+ };
74
+ // Create a response body.
75
+ const body = {
76
+ 'tool collection': getToolsFacts(),
77
+ 'tool name': 'listPages',
78
+ 'this request': {
79
+ description: 'Provide basics about all tested pages.',
80
+ method: 'GET',
81
+ URL: `${thisHost}/api/listPages`,
82
+ 'closest ancestor request': null
83
+ },
84
+ 'URLs of similar requests for web users': {
85
+ 'this request': `${thisHost}/listPages.html`,
86
+ 'closest ancestor request': null
87
+ },
88
+ 'response metadata': getResponseMetadata(),
89
+ 'response content': responseContent
90
+ };
91
+ // Return it.
92
+ return body;
93
+ };
@@ -115,7 +115,7 @@ test('orderNewTest rejects a URL that redirects to a disallowed target, and aler
115
115
  });
116
116
 
117
117
  test('orderNewTest rejects an already-tested page', async () => {
118
- const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']);
118
+ const body = await response(['Mixed Outcomes', 'https://example.com/mixed', 'A reason that is long enough.']);
119
119
  const details = body['response content']['details about your order'] as any;
120
120
  assert.equal(details.error, undefined);
121
121
  const disposition = body['response content']['disposition of your order'] as any;
@@ -209,5 +209,5 @@ test('orderNewTest response omits a web UI URL for this request but includes the
209
209
  const body = await response(['Brand New Ordered Page 2', 'https://example.com/brandnewordered2', 'A reason that is long enough.']);
210
210
  const similarWeb = body['URLs of similar requests for web users'] as any;
211
211
  assert.equal(similarWeb['this request'], null);
212
- assert.ok(similarWeb['closest ancestor request'].includes('listReports.html'));
212
+ assert.ok(similarWeb['closest ancestor request'].includes('listPages.html'));
213
213
  });
@@ -127,15 +127,15 @@ export const response = async (args: string[]) => {
127
127
  reason
128
128
  },
129
129
  'closest ancestor request': {
130
- 'tool name': 'listReports',
131
- description: 'Provide basics about all available reports.',
130
+ 'tool name': 'listPages',
131
+ description: 'Provide basics about all tested pages.',
132
132
  method: 'GET',
133
- URL: `${thisHost}/api/listReports`
133
+ URL: `${thisHost}/api/listPages`
134
134
  }
135
135
  },
136
136
  'URLs of similar requests for web users': {
137
137
  'this request': null,
138
- 'closest ancestor request': `${thisHost}/listReports.html`
138
+ 'closest ancestor request': `${thisHost}/listPages.html`
139
139
  },
140
140
  'response metadata': getResponseMetadata(),
141
141
  'response content': responseContent
@@ -87,7 +87,7 @@ test('orderRetest rejects a reason longer than 100 characters', async () => {
87
87
  });
88
88
 
89
89
  test('orderRetest rejects a superseded report', async () => {
90
- // 260101T0000-mix is an earlier report of "Mixed Outcomes Page" than 260202T0000-new,
90
+ // 260101T0000-mix is an earlier report of "Mixed Outcomes" than 260202T0000-new,
91
91
  // so it is superseded.
92
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;
@@ -110,7 +110,7 @@ test('requestNewTest rejects a URL that redirects to a disallowed target, and al
110
110
  });
111
111
 
112
112
  test('requestNewTest rejects an already-tested page', async () => {
113
- const body = await response(['Mixed Outcomes Page', 'https://example.com/mixed', 'A reason that is long enough.']);
113
+ const body = await response(['Mixed Outcomes', 'https://example.com/mixed', 'A reason that is long enough.']);
114
114
  const details = body['response content']['details about your request'] as any;
115
115
  // A report for this description and URL already exists, so processTestRequest
116
116
  // returns 'reportExists' and the failure is reported via the disposition, not details.error.
@@ -81,8 +81,8 @@ export const response = async (args: string[]) => {
81
81
  requestResult,
82
82
  {
83
83
  whatHappensNext: 'Your request is likely to be approved and processed within 1 hour to 1 day.',
84
- howToCheck: 'You can call the listReports tool to learn whether the page has been tested and a report is available.',
85
- howWebUserChecks: `A web user can visit ${thisHost}/listReports.html to learn whether the page has been tested and a report is available.`
84
+ howToCheck: 'You can call the listPages tool to learn whether the page has been tested.',
85
+ howWebUserChecks: `A web user can visit ${thisHost}/listPages.html to learn whether the page has been tested.`
86
86
  },
87
87
  {result: 'reportExists', reason: 'a report about a page with the same description or URL is available.'}
88
88
  );
@@ -106,15 +106,15 @@ export const response = async (args: string[]) => {
106
106
  reason
107
107
  },
108
108
  'closest ancestor request': {
109
- 'tool name': 'listReports',
110
- description: 'Provide basics about all available reports.',
109
+ 'tool name': 'listPages',
110
+ description: 'Provide basics about all tested pages.',
111
111
  method: 'GET',
112
- URL: `${thisHost}/api/listReports`
112
+ URL: `${thisHost}/api/listPages`
113
113
  }
114
114
  },
115
115
  'URLs of similar requests for web users': {
116
116
  'this request': `${thisHost}/requestNewTestForm.html`,
117
- 'closest ancestor request': `${thisHost}/listReports.html`
117
+ 'closest ancestor request': `${thisHost}/listPages.html`
118
118
  },
119
119
  'response metadata': getResponseMetadata(),
120
120
  'response content': responseContent
@@ -85,7 +85,7 @@ test('requestRetest rejects a malformed timestamp or job identifier', async () =
85
85
  });
86
86
 
87
87
  test('requestRetest rejects a superseded report', async () => {
88
- // 260101T0000-mix is an earlier report of "Mixed Outcomes Page" than 260202T0000-new,
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
91
  const body = await response(['260101T0000', 'mix', 'A reason that is long enough.']);
@@ -168,10 +168,10 @@ test('requestRetest accepts a valid retest request for the latest report of a pa
168
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
- assert.equal(details['page to be retested'].description, 'Mixed Outcomes Page');
171
+ assert.equal(details['page to be retested'].description, 'Mixed Outcomes');
172
172
  assert.ok(logged.some(line =>
173
173
  line.startsWith('WARNING (Kilotest: request for a retest awaits approval)')
174
- && line.includes('Mixed Outcomes Page')
174
+ && line.includes('Mixed Outcomes')
175
175
  && line.includes('https://example.com/mixed')
176
176
  // The alert reports the resulting queue size, so a maintainer who has been away can
177
177
  // see at a glance how close the queue is to full.
@@ -64,8 +64,8 @@ export const response = async (args: string[]) => {
64
64
  requestResult,
65
65
  {
66
66
  whatHappensNext: 'Your request is likely to be approved and processed within 1 hour to 1 day.',
67
- howToCheck: 'You can call the listReports tool to learn whether the page has been retested and a new report is available.',
68
- howWebUserChecks: `A web user can visit ${thisHost}/listReports.html to learn whether the page has been retested and a new report is available.`
67
+ howToCheck: 'You can call the listPages tool, or the listIssues tool on the current report, to learn whether the page has been retested and a new report is available.',
68
+ howWebUserChecks: `A web user can visit ${thisHost}/listPages.html to learn whether the page has been retested and a new report is available.`
69
69
  },
70
70
  {result: 'superseded', reason: 'a later report about a page with the same description is available.'}
71
71
  );
@@ -23,7 +23,7 @@ test('routes includes all expected operation IDs', () => {
23
23
  'getReport',
24
24
  'listDiagnoses',
25
25
  'listIssues',
26
- 'listReports',
26
+ 'listPages',
27
27
  'listViolators',
28
28
  'orderNewTest',
29
29
  'orderRetest',
package/api/routes.ts CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  orderNewTestSchema,
18
18
  orderRetestSchema,
19
19
  awaitTestSchema,
20
- listReportsResponseSchema,
20
+ listPagesResponseSchema,
21
21
  listIssuesResponseSchema,
22
22
  listViolatorsResponseSchema,
23
23
  listDiagnosesResponseSchema,
@@ -34,11 +34,11 @@ import {
34
34
 
35
35
  export const routes = [
36
36
  {
37
- operationId: 'listReports',
37
+ operationId: 'listPages',
38
38
  method: 'get',
39
- path: '/api/listReports',
40
- summary: 'List all available reports',
41
- responseSchema: listReportsResponseSchema
39
+ path: '/api/listPages',
40
+ summary: 'List all tested pages',
41
+ responseSchema: listPagesResponseSchema
42
42
  },
43
43
  {
44
44
  operationId: 'listIssues',
package/api/schemas.ts CHANGED
@@ -43,7 +43,7 @@ export const getReportSchema = {
43
43
 
44
44
  // requestNewTest: POST /api/requestNewTest
45
45
  export const requestNewTestSchema = {
46
- description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'),
46
+ description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listPages output'),
47
47
  URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
48
48
  reason: z.string().describe('20- to 100-character reason why the page should be tested')
49
49
  };
@@ -62,7 +62,7 @@ export const requestFeatureSchema = {
62
62
 
63
63
  // orderNewTest: POST /api/orderNewTest
64
64
  export const orderNewTestSchema = {
65
- description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listReports output'),
65
+ description: z.string().describe('1- to 100-character description of the page conforming to the naming convention used in the listPages output'),
66
66
  URL: z.string().describe('12- to 300-character URL of the page, including the https:// scheme and any query'),
67
67
  reason: z.string().describe('20- to 100-character reason why the page should be tested')
68
68
  };
@@ -80,7 +80,7 @@ export const awaitTestSchema = {
80
80
  jobID: z.string().describe('Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)')
81
81
  };
82
82
 
83
- // listReports takes no input; omitted (mcp.ts already uses inputSchema: {}).
83
+ // listPages takes no input; omitted (mcp.ts already uses inputSchema: {}).
84
84
 
85
85
  // RESPONSE SCHEMAS
86
86
 
@@ -194,10 +194,20 @@ const issueBasicsOrErrorSchema = z.union([
194
194
  z.object({error: z.string()})
195
195
  ]).meta({id: 'IssueBasicsOrError'});
196
196
 
197
- export const listReportsResponseSchema = envelope('GET', z.object({
198
- 'basics about all available reports': z.array(reportBasicsSchema.extend({
199
- 'how to get details about the report': z.object({method: z.literal('GET'), URL: z.string()}),
200
- 'web users can get details about the report at': z.string()
197
+ // Facts about a report among those about one page.
198
+ const reportTimeSchema = z.object({
199
+ identifier: z.string().describe('timeStamp-jobID identifier of the report.'),
200
+ 'completion date and time': z.string(),
201
+ 'days since the report was completed': z.number().nullable()
202
+ });
203
+
204
+ export const listPagesResponseSchema = envelope('GET', z.object({
205
+ 'basics about all tested pages': z.array(z.object({
206
+ 'tested web page': z.object({description: z.string(), URL: z.string()}),
207
+ 'number of reports about the page': z.number(),
208
+ 'basics about the latest report': reportTimeSchema,
209
+ 'how to get details about the latest report': z.object({method: z.literal('GET'), URL: z.string()}),
210
+ 'web users can get details about the latest report at': z.string()
201
211
  })),
202
212
  'how to request that a page with no report be tested': z.object({
203
213
  method: z.literal('POST'),
@@ -210,6 +220,13 @@ export const listReportsResponseSchema = envelope('GET', z.object({
210
220
 
211
221
  export const listIssuesResponseSchema = envelope('GET', z.object({
212
222
  'basics about the report': reportBasicsOrErrorSchema,
223
+ 'history of reports about the page': z.array(reportTimeSchema.extend({
224
+ 'whether it is the oldest report about the page': z.boolean(),
225
+ 'whether it is the latest report about the page': z.boolean(),
226
+ 'whether it is the report described in this response': z.boolean(),
227
+ 'how to get details about the report': z.object({method: z.literal('GET'), URL: z.string()}),
228
+ 'web users can get details about the report at': z.string()
229
+ })).nullable().describe('All reports about the same page, from oldest to latest, or null if the report is unavailable.'),
213
230
  'details about the report': z.object({
214
231
  'job definition': z.object({
215
232
  'whether the job prohibited redirection': z.unknown().describe('Copied from the report (strict); type not guaranteed.'),