@jrpool/kilotest 58.2.0 → 59.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 (53) hide show
  1. package/api/listIssues.test.ts +32 -0
  2. package/api/listIssues.ts +38 -8
  3. package/api/listPages.test.ts +92 -0
  4. package/api/listPages.ts +93 -0
  5. package/api/orderNewTest.test.ts +1 -1
  6. package/api/orderNewTest.ts +4 -4
  7. package/api/requestNewTest.ts +6 -6
  8. package/api/requestRetest.ts +2 -2
  9. package/api/routes.test.ts +1 -1
  10. package/api/routes.ts +5 -5
  11. package/api/schemas.ts +24 -7
  12. package/api/util.ts +26 -45
  13. package/apiIntegration.test.ts +5 -5
  14. package/docs/SERVICE.md +16 -5
  15. package/index.html +1 -1
  16. package/index.test.ts +30 -18
  17. package/index.ts +22 -11
  18. package/lhm.plugin.json +1433 -141
  19. package/linkValidation.test.ts +2 -2
  20. package/llms-full.txt +9 -9
  21. package/llms.txt +4 -4
  22. package/mcp.test.ts +8 -8
  23. package/mcp.ts +14 -14
  24. package/openapi.yaml +84 -30
  25. package/package.json +1 -1
  26. package/routing.test.ts +5 -5
  27. package/server.json +1 -1
  28. package/sitemap.xml +1 -1
  29. package/smokeTest.ts +2 -2
  30. package/style.css +8 -8
  31. package/test/fixtures/buildFixtures.ts +1 -1
  32. package/util.test.ts +71 -12
  33. package/util.ts +61 -12
  34. package/web/enqueueForm/index.html +1 -1
  35. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  36. package/web/hideReportForm/index.ts +9 -6
  37. package/web/listIssues/index.html +2 -1
  38. package/web/listIssues/index.ts +45 -3
  39. package/web/listIssues/listIssues.test.ts +76 -0
  40. package/web/{listReports → listPages}/index.html +1 -2
  41. package/web/listPages/index.ts +99 -0
  42. package/web/listPages/listPages.test.ts +196 -0
  43. package/web/metrics/metrics.test.ts +2 -2
  44. package/web/requestRetestForm/index.ts +4 -2
  45. package/web/requestRetestForm/requestRetestForm.test.ts +11 -0
  46. package/web/unhideReportForm/index.ts +9 -6
  47. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  48. package/web/util.test.ts +85 -11
  49. package/web/util.ts +26 -28
  50. package/api/listReports.test.ts +0 -109
  51. package/api/listReports.ts +0 -87
  52. package/web/listReports/index.ts +0 -182
  53. package/web/listReports/listReports.test.ts +0 -300
@@ -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 Page',
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 Page')!;
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
+ };
@@ -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
@@ -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
@@ -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.'),
package/api/util.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  objectSort,
16
16
  ruleEngines
17
17
  } from '../util.ts';
18
- import type {ReportExtract, TestRequestResult} from '../util.ts';
18
+ import type {TestRequestResult} from '../util.ts';
19
19
  import {issues as issueSpecs} from 'testaro-issues';
20
20
 
21
21
  // TYPES
@@ -51,9 +51,9 @@ export const getResponseMetadata = () => ({
51
51
  export const getToolsFacts = () => ({
52
52
  'name': 'Kilotest',
53
53
  'description': {
54
- 'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the issues and the violations retrievable at four levels of granularity.',
54
+ 'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the tested pages, the issues, and the violations retrievable at four levels of granularity.',
55
55
  'how to retrieve findings': {
56
- 'level 1': 'Use the listReports tool to get a list of available reports.',
56
+ 'level 1': 'Use the listPages tool to get a list of tested pages, each with its latest report. The listIssues output about any report lists all reports about the same page.',
57
57
  'level 2': 'Use the listIssues tool to get a list of issues in one report.',
58
58
  'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue.',
59
59
  'level 4': 'Use the listDiagnoses tool to get a list of diagnoses of how one element on one page exhibited one issue in one report.',
@@ -62,7 +62,7 @@ export const getToolsFacts = () => ({
62
62
  'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.',
63
63
  'retesting': 'If the listIssues tool shows that the latest report about a page is obsolete, because the page has been revised or for another reason, use the requestRetest tool to request that the page be retested.',
64
64
  'latency': 'Requests for testing and retesting are usually approved and fulfilled within one day.',
65
- 'confirmation': 'Use the listReports tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
65
+ 'confirmation': 'Use the listPages tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
66
66
  }
67
67
  },
68
68
  'URL': `${getThisHost()}/mcp`,
@@ -131,49 +131,30 @@ export const getRuleEnginesFacts = (ruleEngineIDSet: Iterable<string>) => {
131
131
  objectSort(ruleEnginesFacts, 'name', 'alpha');
132
132
  return ruleEnginesFacts;
133
133
  };
134
- // Returns the basics about a report.
135
- // Accepts an optional precomputed extract to avoid redundant reads when called
136
- // in a loop over all reports (e.g. by listReports). When the extract comes from
137
- // getReportExtracts, it carries a superseded flag; otherwise the flag is computed.
138
- // With an extract provided, failure is impossible, so the return type narrows
139
- // to ReportBasics; without one, an error object may be returned.
140
- // XXX Why are these overloads necessary? Can't we just have one function?
141
- export function getReportBasics(
142
- timeStamp: string, jobID: string, extract: ReportExtract
143
- ): Promise<ReportBasics>;
144
- export function getReportBasics(
145
- timeStamp: string, jobID: string, extract?: ReportExtract | null
146
- ): Promise<ReportBasics | {error: string}>;
147
- export async function getReportBasics(
148
- timeStamp: string, jobID: string, extract: ReportExtract | null = null
149
- ): Promise<ReportBasics | {error: string}> {
150
- const extractProvided = !!extract;
151
- // If an extract was not provided, verify the report exists and read it.
152
- if (!extract) {
153
- // Get the creation time of the report.
154
- const reportStats = await getReportStats(timeStamp, jobID);
155
- // If the report does not exist:
156
- if (!reportStats) {
157
- // Log and return this.
158
- console.error(`Report ${timeStamp}-${jobID} does not exist.`);
159
- return {
160
- error: `Report ${timeStamp}-${jobID} could not be retrieved.`
161
- };
162
- }
163
- // Otherwise, i.e. if it exists, get an extract of the report.
164
- const fetchedExtract = await getReportExtract(timeStamp, jobID);
165
- // If this failed, return why.
166
- if ('error' in fetchedExtract) {
167
- return fetchedExtract;
168
- }
169
- extract = fetchedExtract;
134
+ // Returns the basics about a report, or an error object if the report cannot be read.
135
+ export const getReportBasics = async (
136
+ timeStamp: string, jobID: string
137
+ ): Promise<ReportBasics | {error: string}> => {
138
+ // Verify that the report exists.
139
+ const reportStats = await getReportStats(timeStamp, jobID);
140
+ // If it does not exist:
141
+ if (!reportStats) {
142
+ // Log and return this.
143
+ console.error(`Report ${timeStamp}-${jobID} does not exist.`);
144
+ return {
145
+ error: `Report ${timeStamp}-${jobID} could not be retrieved.`
146
+ };
147
+ }
148
+ // Otherwise, i.e. if it exists, get an extract of the report.
149
+ const extract = await getReportExtract(timeStamp, jobID);
150
+ // If this failed, return why.
151
+ if ('error' in extract) {
152
+ return extract;
170
153
  }
171
154
  const {url, description, reportTime} = extract;
172
155
  // Get whether this report has been superseded.
173
- const isSuperseded = extractProvided
174
- ? extract.superseded === true
175
- : (await getReportExtracts(true))
176
- .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
156
+ const isSuperseded = (await getReportExtracts(true))
157
+ .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
177
158
  // Get the basics about the report.
178
159
  const basics = {
179
160
  identifier: `${timeStamp}-${jobID}`,
@@ -187,7 +168,7 @@ export async function getReportBasics(
187
168
  };
188
169
  // Return them.
189
170
  return basics;
190
- }
171
+ };
191
172
  // Returns the specification of an issue.
192
173
  export const getIssueSpec = (issueID: string) => {
193
174
  // Get the issue specification.
@@ -93,11 +93,11 @@ const request = (method: string, requestPath: string, body: any = null): Promise
93
93
 
94
94
  // TESTS
95
95
 
96
- test('GET /api/listReports returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
97
- const {body} = await request('GET', '/api/listReports');
98
- assert.equal(body['tool name'], 'listReports');
99
- const reports = body['response content']['basics about all available reports'];
100
- assert.equal(reports.length, 8);
96
+ test('GET /api/listPages returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
97
+ const {body} = await request('GET', '/api/listPages');
98
+ assert.equal(body['tool name'], 'listPages');
99
+ const pages = body['response content']['basics about all tested pages'];
100
+ assert.equal(pages.length, 7);
101
101
  });
102
102
 
103
103
  test('POST /api/requestFeature parses a JSON body and returns a JSON response through the HTTP routing layer', async () => {