@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/README.md CHANGED
@@ -10,7 +10,7 @@ An ensemble testing and reporting service for front-end web quality
10
10
 
11
11
  ## Features
12
12
 
13
- This application uses an ensemble of 12 rule engines to test web pages for front-end quality (i.e. accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG (HTML, CSS, ARIA, WCAG)).
13
+ This application uses an ensemble of 12 rule engines to test web pages for front-end quality (i.e. accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG).
14
14
 
15
15
  The testing paradigm employed by Kilotest is discussed in these papers:
16
16
 
@@ -55,10 +55,7 @@ See the [`SERVICE.md`](docs/SERVICE.md) file for instructions on how to make Kil
55
55
 
56
56
  Kilotest is not only a web application but also an API and an MCP server. AI platforms and models can use Kilotest via those interfaces.
57
57
 
58
- The URL for models using the MCP server is `https://kilotest.com/mcp`. The MCP server is listed at these services:
59
-
60
- - [Smithery](https://smithery.ai/servers/pool/kilotest)
61
- - [Glama](https://glama.ai/mcp/connectors/com.kilotest/kilotest)
58
+ The public URL for models using the MCP server at `kilotest.com` is `https://kilotest.com/mcp`. The MCP server is registered at several registries. See [AI-TOOLS.md](docs/AI-TOOLS.md#External-features) for details.
62
59
 
63
60
  There is additional documentation at:
64
61
 
@@ -43,24 +43,24 @@ after(() => {
43
43
 
44
44
  // TESTS
45
45
 
46
- test('awaitTest rejects a malformed timestamp or job identifier', async () => {
47
- const body = await response(['999999T9999', 'xyz']);
46
+ test('awaitTest rejects a malformed report identifier', async () => {
47
+ const body = await response(['999999T9999-xyz']);
48
48
  const details = body['response content']['details about your wait'] as any;
49
49
  assert.ok(details.error.includes('malformed'));
50
50
  assert.equal(body['response content']['disposition of your wait'], null);
51
51
  });
52
52
 
53
53
  test('awaitTest returns "completed" for an identifier with an already-available report', async () => {
54
- const body = await response(['260101T0000', 'mix']);
54
+ const body = await response(['260101T0000-mix']);
55
55
  const details = body['response content']['details about your wait'] as any;
56
- assert.deepEqual(details['report identifier'], {timeStamp: '260101T0000', jobID: 'mix'});
56
+ assert.equal(details['report identifier'], '260101T0000-mix');
57
57
  const disposition = body['response content']['disposition of your wait'] as any;
58
58
  assert.equal(disposition.outcome, 'completed');
59
59
  assert.ok(disposition['how you can check for completion'].includes('listIssues'));
60
60
  });
61
61
 
62
62
  test('awaitTest returns an error for an identifier naming neither a report nor a pending job', async () => {
63
- const body = await response(['260101T0000', 'zzz']);
63
+ const body = await response(['260101T0000-zzz']);
64
64
  const details = body['response content']['details about your wait'] as any;
65
65
  assert.ok(details.error.includes('no report'));
66
66
  assert.equal(body['response content']['disposition of your wait'], null);
@@ -71,7 +71,7 @@ test('awaitTest returns "failed" when the job has moved to the failed directory'
71
71
  await fs.mkdir(path.dirname(failedPath), {recursive: true});
72
72
  await fs.writeFile(failedPath, JSON.stringify({target: {what: 'Failed Page', url: 'https://example.com/failed'}}));
73
73
  try {
74
- const body = await response(['260101T4444', 'flj']);
74
+ const body = await response(['260101T4444-flj']);
75
75
  const disposition = body['response content']['disposition of your wait'] as any;
76
76
  assert.equal(disposition.outcome, 'failed');
77
77
  assert.equal(disposition['how you can check for completion'], 'Not applicable.');
@@ -85,7 +85,7 @@ test('awaitTest returns "timedOut" when a claimed job neither completes nor fail
85
85
  const claimedPath = path.join(fixtureDBDir, 'jobs', 'claimed', '260101T5555-toj.json');
86
86
  await fs.writeFile(claimedPath, JSON.stringify({target: {what: 'Stuck Page', url: 'https://example.com/stuck'}}));
87
87
  try {
88
- const body = await response(['260101T5555', 'toj']);
88
+ const body = await response(['260101T5555-toj']);
89
89
  const disposition = body['response content']['disposition of your wait'] as any;
90
90
  assert.equal(disposition.outcome, 'timedOut');
91
91
  assert.ok(disposition['how you can check for completion'].includes('listIssues'));
@@ -96,14 +96,14 @@ test('awaitTest returns "timedOut" when a claimed job neither completes nor fail
96
96
  });
97
97
 
98
98
  test('awaitTest response has no web UI equivalents for this request or its ancestors', async () => {
99
- const body = await response(['260101T0000', 'mix']);
99
+ const body = await response(['260101T0000-mix']);
100
100
  const similarWeb = body['URLs of similar requests for web users'] as any;
101
101
  assert.equal(similarWeb['this request'], null);
102
102
  assert.equal(similarWeb['closest ancestor request'], null);
103
103
  });
104
104
 
105
105
  test('awaitTest reports both orderNewTest and orderRetest as possible ancestors', async () => {
106
- const body = await response(['260101T0000', 'mix']);
106
+ const body = await response(['260101T0000-mix']);
107
107
  const ancestors = (body['this request'] as any)['closest ancestor request'];
108
108
  assert.equal(ancestors.length, 2);
109
109
  const names = ancestors.map((a: any) => a['tool name']).sort();
package/api/awaitTest.ts CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  import {z} from 'zod';
9
9
  import {getResponseMetadata, getThisHost, getToolsFacts} from './util.ts';
10
- import {awaitJob, isJobID, isTimeStamp} from '../util.ts';
10
+ import {awaitJob, isReportID} from '../util.ts';
11
11
  import {awaitTestResponseSchema} from './schemas.ts';
12
12
 
13
13
  // TYPES
@@ -19,35 +19,35 @@ type ResponseContent = z.infer<typeof awaitTestResponseSchema>['response content
19
19
 
20
20
  // Returns the response body.
21
21
  export const response = async (args: string[]) => {
22
- const [timeStamp = '', jobID = ''] = args;
22
+ const [reportID = ''] = args;
23
23
  const thisHost = getThisHost();
24
24
  // Initialize the response-content properties.
25
25
  let requestDetails: ResponseContent['details about your wait'];
26
26
  let requestDisposition: ResponseContent['disposition of your wait'] = null;
27
- // If the timestamp or job ID is not syntactically valid:
28
- if (!isTimeStamp(timeStamp) || !isJobID(jobID)) {
27
+ // If the report identifier is not syntactically valid:
28
+ if (!isReportID(reportID)) {
29
29
  requestDetails = {
30
- error: 'wait invalid: the report timeStamp or jobID is malformed'
30
+ error: 'wait invalid: the reportID is malformed'
31
31
  };
32
32
  }
33
33
  // Otherwise, i.e. if the identifier is syntactically valid:
34
34
  else {
35
35
  requestDetails = {
36
- 'report identifier': {timeStamp, jobID}
36
+ 'report identifier': reportID
37
37
  };
38
38
  // Wait for the job to complete, fail, or time out. No progress notices are sent
39
39
  // during this wait; the caller receives only this one, final response.
40
- const outcome = await awaitJob(timeStamp, jobID);
40
+ const outcome = await awaitJob(reportID);
41
41
  if (outcome === 'notFound') {
42
42
  requestDetails = {
43
- error: `wait invalid: no report with timeStamp "${timeStamp}" and jobID "${jobID}" is completed, pending, or in progress`
43
+ error: `wait invalid: no report with identifier "${reportID}" is completed, pending, or in progress`
44
44
  };
45
45
  }
46
46
  else if (outcome === 'completed') {
47
47
  requestDisposition = {
48
48
  outcome: 'completed',
49
49
  'what happened': 'The report is ready.',
50
- 'how you can check for completion': `Call the listIssues tool with timeStamp "${timeStamp}" and jobID "${jobID}" to retrieve the report.`
50
+ 'how you can check for completion': `Call the listIssues tool with reportID "${reportID}" to retrieve the report.`
51
51
  };
52
52
  }
53
53
  else if (outcome === 'failed') {
@@ -61,7 +61,7 @@ export const response = async (args: string[]) => {
61
61
  requestDisposition = {
62
62
  outcome: 'timedOut',
63
63
  'what happened': 'The job was not completed within the maximum wait time, but it may still be running.',
64
- 'how you can check for completion': `Call the listIssues tool with timeStamp "${timeStamp}" and jobID "${jobID}" after a few minutes to check whether the report is ready.`
64
+ 'how you can check for completion': `Call the listIssues tool with reportID "${reportID}" after a few minutes to check whether the report is ready.`
65
65
  };
66
66
  }
67
67
  }
@@ -75,9 +75,9 @@ export const response = async (args: string[]) => {
75
75
  'tool collection': getToolsFacts(),
76
76
  'tool name': 'awaitTest',
77
77
  'this request': {
78
- description: 'Wait for a job ordered via the orderNewTest or orderRetest tool to complete. The timeStamp and jobID parameters are the report identifier that orderNewTest or orderRetest returned. This call blocks until the report is ready, the job fails, or a maximum wait time elapses.',
78
+ description: 'Wait for a job ordered via the orderNewTest or orderRetest tool to complete. The reportID parameter is the report identifier that orderNewTest or orderRetest returned. This call blocks until the report is ready, the job fails, or a maximum wait time elapses.',
79
79
  method: 'POST',
80
- URL: `${thisHost}/api/awaitTest/${timeStamp}/${jobID}`,
80
+ URL: `${thisHost}/api/awaitTest/${reportID}`,
81
81
  // awaitTest's own parameters do not say whether the job was ordered via orderNewTest
82
82
  // or orderRetest, so both are reported as possible ancestors, rather than
83
83
  // arbitrarily naming just one.
@@ -29,7 +29,7 @@ after(() => {
29
29
  // TESTS
30
30
 
31
31
  test('getReport returns the full report for a valid report', async () => {
32
- const body = await response(['260101T0000', 'mix']);
32
+ const body = await response(['260101T0000-mix']);
33
33
  const content = body['response content'];
34
34
  assert.equal(typeof content['size of the report in bytes'], 'number');
35
35
  assert.ok((content['size of the report in bytes'] as number) > 0);
@@ -41,21 +41,21 @@ test('getReport returns the full report for a valid report', async () => {
41
41
  });
42
42
 
43
43
  test('getReport returns an error for a nonexistent report', async () => {
44
- const body = await response(['999999T9999', 'xyz']);
44
+ const body = await response(['999999T9999-xyz']);
45
45
  const content = body['response content'];
46
46
  assert.equal(content['full report'], null);
47
47
  assert.equal(typeof content['size of the report in bytes'], 'string');
48
48
  });
49
49
 
50
50
  test('getReport returns an error for the hidden report', async () => {
51
- const body = await response(['260101T0007', 'hid']);
51
+ const body = await response(['260101T0007-hid']);
52
52
  const content = body['response content'];
53
53
  assert.equal(content['full report'], null);
54
54
  assert.equal(typeof content['size of the report in bytes'], 'string');
55
55
  });
56
56
 
57
57
  test('getReport includes tool name and metadata', async () => {
58
- const body = await response(['260101T0000', 'mix']);
58
+ const body = await response(['260101T0000-mix']);
59
59
  assert.equal(body['tool name'], 'getReport');
60
60
  assert.ok(body['response metadata'].identifier);
61
61
  });
package/api/getReport.ts CHANGED
@@ -23,18 +23,18 @@ type ResponseContent = z.infer<typeof getReportResponseSchema>['response content
23
23
 
24
24
  // Returns the response body.
25
25
  export const response = async (args: string[]) => {
26
- const [timeStamp = '', jobID = ''] = args;
26
+ const [reportID = ''] = args;
27
27
  const thisHost = getThisHost();
28
- // Get the report size. A malformed timestamp or job ID resolves to a nonexistent file
28
+ // Get the report size. A malformed report identifier resolves to a nonexistent file
29
29
  // path, which getReportStats already reports as unavailable, so no separate syntactic
30
30
  // check is needed here.
31
- const reportStats = await getReportStats(timeStamp, jobID);
31
+ const reportStats = await getReportStats(reportID);
32
32
  // Create the response content.
33
33
  const responseContent: ResponseContent = reportStats
34
34
  ? {
35
35
  'size of the report in bytes': reportStats.reportSize,
36
36
  // Get the report (which may be only an error message).
37
- 'full report': await getReport(timeStamp, jobID)
37
+ 'full report': await getReport(reportID)
38
38
  }
39
39
  : {
40
40
  'size of the report in bytes': 'Error: The report could not be accessed for an unknown reason.',
@@ -45,19 +45,19 @@ export const response = async (args: string[]) => {
45
45
  'tool collection': getToolsFacts(),
46
46
  'tool name': 'getReport',
47
47
  'this request': {
48
- description: 'Provide one full report in JSON format. The timeStamp and jobID parameters identify the report that I want details about. Those parameters were in the response to my earlier listIssues request.',
48
+ description: 'Provide one full report in JSON format. The reportID parameter identifies the report that I want details about. It was in the response to my earlier listIssues request.',
49
49
  method: 'GET',
50
- URL: `${thisHost}/api/getReport/${timeStamp}/${jobID}`,
50
+ URL: `${thisHost}/api/getReport/${reportID}`,
51
51
  'closest ancestor request': {
52
52
  'tool name': 'listIssues',
53
53
  description: 'Provide details about one report, including basics about the issues reported in it.',
54
54
  method: 'GET',
55
- URL: `${thisHost}/api/listIssues/${timeStamp}/${jobID}`
55
+ URL: `${thisHost}/api/listIssues/${reportID}`
56
56
  }
57
57
  },
58
58
  'URLs of similar requests for web users': {
59
- 'this request': `${thisHost}/fullReport.json/${timeStamp}/${jobID}`,
60
- 'closest ancestor request': `${thisHost}/listIssues.html/${timeStamp}/${jobID}`
59
+ 'this request': `${thisHost}/fullReport.json/${reportID}`,
60
+ 'closest ancestor request': `${thisHost}/listIssues.html/${reportID}`
61
61
  },
62
62
  'response metadata': getResponseMetadata(),
63
63
  'response content': responseContent
@@ -29,7 +29,7 @@ after(() => {
29
29
  // TESTS
30
30
 
31
31
  test('listDiagnoses returns 2 diagnoses for catalogIndex 0, linkNoText, in the mixed report (axe and alfa, excluding cantTell)', async () => {
32
- const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
32
+ const body = await response(['0', 'linkNoText', '260101T0000-mix']);
33
33
  const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'] as any;
34
34
  assert.equal(diagnoses.length, 2);
35
35
  const rules = diagnoses.map((d: any) => d['identifier of the violated rule']).sort();
@@ -39,13 +39,13 @@ test('listDiagnoses returns 2 diagnoses for catalogIndex 0, linkNoText, in the m
39
39
  });
40
40
 
41
41
  test('listDiagnoses returns 0 diagnoses when all instances are cantTell', async () => {
42
- const body = await response(['0', 'focusIndicationBad', '260101T0001', 'ct']);
42
+ const body = await response(['0', 'focusIndicationBad', '260101T0001-ct']);
43
43
  const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'] as any;
44
44
  assert.equal(diagnoses.length, 0);
45
45
  });
46
46
 
47
47
  test('listDiagnoses treats missing outcome as a violation', async () => {
48
- const body = await response(['0', 'linkNoText', '260101T0002', 'no']);
48
+ const body = await response(['0', 'linkNoText', '260101T0002-no']);
49
49
  const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'] as any;
50
50
  assert.equal(diagnoses.length, 1);
51
51
  assert.equal(diagnoses[0]['description of the violation'], 'Button has no accessible name');
@@ -53,7 +53,7 @@ test('listDiagnoses treats missing outcome as a violation', async () => {
53
53
  });
54
54
 
55
55
  test('listDiagnoses returns element basics from the catalog', async () => {
56
- const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
56
+ const body = await response(['0', 'linkNoText', '260101T0000-mix']);
57
57
  const elementBasics = body['response content']['basics about the element'] as any;
58
58
  assert.equal(elementBasics.identifier, '0');
59
59
  assert.equal(elementBasics['tag name'], 'A');
@@ -61,7 +61,7 @@ test('listDiagnoses returns element basics from the catalog', async () => {
61
61
  });
62
62
 
63
63
  test('listDiagnoses returns element details from the catalog', async () => {
64
- const body = await response(['0', 'linkNoText', '260101T0000', 'mix']);
64
+ const body = await response(['0', 'linkNoText', '260101T0000-mix']);
65
65
  const elementDetails = body['response content']['details about the element'] as any;
66
66
  assert.equal(elementDetails['start tag'], '<a>');
67
67
  assert.equal(elementDetails['XPath'], '/html/body/a[1]');
@@ -69,39 +69,39 @@ test('listDiagnoses returns element details from the catalog', async () => {
69
69
  });
70
70
 
71
71
  test('listDiagnoses returns an error for an unknown issue ID', async () => {
72
- const body = await response(['0', 'nonexistentIssue', '260101T0000', 'mix']);
72
+ const body = await response(['0', 'nonexistentIssue', '260101T0000-mix']);
73
73
  const basics = body['response content']['basics about the issue'] as any;
74
74
  assert.ok(basics.error);
75
75
  });
76
76
 
77
77
  test('listDiagnoses returns an error for a nonexistent catalog index', async () => {
78
- const body = await response(['999', 'linkNoText', '260101T0000', 'mix']);
78
+ const body = await response(['999', 'linkNoText', '260101T0000-mix']);
79
79
  const basics = body['response content']['basics about the element'] as any;
80
80
  assert.ok(basics.error);
81
81
  });
82
82
 
83
83
  test('listDiagnoses returns an error for a nonexistent report', async () => {
84
- const body = await response(['0', 'linkNoText', '999999T9999', 'xyz']);
84
+ const body = await response(['0', 'linkNoText', '999999T9999-xyz']);
85
85
  const basics = body['response content']['basics about the report'] as any;
86
86
  assert.ok(basics.error);
87
87
  });
88
88
 
89
89
  test('listDiagnoses returns guideline layer for an issue with a short WCAG code', async () => {
90
- const body = await response(['0', 'duplicateID', '260101T0009', 'brd']);
90
+ const body = await response(['0', 'duplicateID', '260101T0009-brd']);
91
91
  const issueBasics = body['response content']['basics about the issue'] as any;
92
92
  assert.equal(issueBasics['related WCAG standard'].layer, 'guideline');
93
93
  assert.equal(issueBasics['related WCAG standard'].identifier, '4.1');
94
94
  });
95
95
 
96
96
  test('listDiagnoses returns null tag name and inner text for a catalog item missing them', async () => {
97
- const body = await response(['1', 'duplicateID', '260101T0009', 'brd']);
97
+ const body = await response(['1', 'duplicateID', '260101T0009-brd']);
98
98
  const elementBasics = body['response content']['basics about the element'] as any;
99
99
  assert.equal(elementBasics['tag name'], null);
100
100
  assert.equal(elementBasics['inner text'], null);
101
101
  });
102
102
 
103
103
  test('listDiagnoses returns null rule ID when ruleID equals what', async () => {
104
- const body = await response(['0', 'duplicateID', '260101T0009', 'brd']);
104
+ const body = await response(['0', 'duplicateID', '260101T0009-brd']);
105
105
  const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'] as any;
106
106
  const axeDiagnosis = diagnoses.find((d: any) => d['description of the violation'] === 'r99');
107
107
  assert.ok(axeDiagnosis);
@@ -109,7 +109,7 @@ test('listDiagnoses returns null rule ID when ruleID equals what', async () => {
109
109
  });
110
110
 
111
111
  test('listDiagnoses defaults count to 1 when count is missing', async () => {
112
- const body = await response(['1', 'duplicateID', '260101T0009', 'brd']);
112
+ const body = await response(['1', 'duplicateID', '260101T0009-brd']);
113
113
  const diagnoses = body['response content']['diagnoses of how the element exhibited the issue'] as any;
114
114
  const ibmDiagnosis = diagnoses.find((d: any) => d['description of the violation'] === 'Element has no role');
115
115
  assert.ok(ibmDiagnosis);
@@ -117,7 +117,7 @@ test('listDiagnoses defaults count to 1 when count is missing', async () => {
117
117
  });
118
118
 
119
119
  test('listDiagnoses returns null for missing start tag, XPath, and bounding box', async () => {
120
- const body = await response(['2', 'duplicateID', '260101T0009', 'brd']);
120
+ const body = await response(['2', 'duplicateID', '260101T0009-brd']);
121
121
  const elementDetails = body['response content']['details about the element'] as any;
122
122
  assert.equal(elementDetails['start tag'], null);
123
123
  assert.equal(elementDetails['XPath'], null);
@@ -125,7 +125,7 @@ test('listDiagnoses returns null for missing start tag, XPath, and bounding box'
125
125
  });
126
126
 
127
127
  test('listDiagnoses returns issue error for a falsy issue ID', async () => {
128
- const body = await response(['0', '', '260101T0009', 'brd']);
128
+ const body = await response(['0', '', '260101T0009-brd']);
129
129
  const issueBasics = body['response content']['basics about the issue'] as any;
130
130
  assert.ok(issueBasics.error);
131
131
  });
@@ -27,15 +27,15 @@ type Diagnoses = Extract<ResponseContent['diagnoses of how the element exhibited
27
27
 
28
28
  // Returns the response body.
29
29
  export const response = async (args: string[]) => {
30
- const [catalogIndex = '', issueID = '', timeStamp = '', jobID = ''] = args;
30
+ const [catalogIndex = '', issueID = '', reportID = ''] = args;
31
31
  const thisHost = getThisHost();
32
32
  // Get the report.
33
- const report = await getReport(timeStamp, jobID);
33
+ const report = await getReport(reportID);
34
34
  // Get the specification of the issue.
35
35
  const issueSpec = issueID ? getIssueSpec(issueID) : null;
36
36
  // Initialize the response content.
37
37
  const responseContent: ResponseContent = {
38
- 'basics about the report': isReportError(report) ? report : await getReportBasics(timeStamp, jobID),
38
+ 'basics about the report': isReportError(report) ? report : await getReportBasics(reportID),
39
39
  'basics about the issue': {
40
40
  'error': 'No information about the specified issue is available'
41
41
  },
@@ -115,19 +115,19 @@ export const response = async (args: string[]) => {
115
115
  'tool collection': getToolsFacts(),
116
116
  'tool name': 'listDiagnoses',
117
117
  'this request': {
118
- description: 'Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue. The catalogIndex, issueID, timeStamp, and jobID parameters identify the element, issue, and report that I want details about. Those parameters were in the response to my earlier listViolators request.',
118
+ description: 'Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue. The catalogIndex, issueID, and reportID parameters identify the element, issue, and report that I want details about. Those parameters were in the response to my earlier listViolators request.',
119
119
  method: 'GET',
120
- URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${timeStamp}/${jobID}`,
120
+ URL: `${thisHost}/api/listDiagnoses/${catalogIndex}/${issueID}/${reportID}`,
121
121
  'closest ancestor request': {
122
122
  'tool name': 'listViolators',
123
123
  description: 'Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.',
124
124
  method: 'GET',
125
- URL: `${thisHost}/api/listViolators/${issueID}/${timeStamp}/${jobID}`
125
+ URL: `${thisHost}/api/listViolators/${issueID}/${reportID}`
126
126
  }
127
127
  },
128
128
  'URLs of similar requests for web users': {
129
- 'this request': `${thisHost}/listDiagnoses.html/${issueID}/${timeStamp}/${jobID}/${catalogIndex}`,
130
- 'closest ancestor request': `${thisHost}/listViolators.html/${issueID}/${timeStamp}/${jobID}`
129
+ 'this request': `${thisHost}/listDiagnoses.html/${issueID}/${reportID}/${catalogIndex}`,
130
+ 'closest ancestor request': `${thisHost}/listViolators.html/${issueID}/${reportID}`
131
131
  },
132
132
  'response metadata': getResponseMetadata(),
133
133
  'response content': responseContent
@@ -29,7 +29,7 @@ after(() => {
29
29
  // TESTS
30
30
 
31
31
  test('listIssues returns 2 issues for the mixed-outcomes report, excluding cantTell', async () => {
32
- const body = await response(['260101T0000', 'mix']);
32
+ const body = await response(['260101T0000-mix']);
33
33
  const issues = body['response content']['basics about all issues reported in the report'];
34
34
  assert.ok(issues);
35
35
  assert.equal(issues.length, 2);
@@ -38,14 +38,14 @@ test('listIssues returns 2 issues for the mixed-outcomes report, excluding cantT
38
38
  });
39
39
 
40
40
  test('listIssues returns 0 issues for the all-cantTell report', async () => {
41
- const body = await response(['260101T0001', 'ct']);
41
+ const body = await response(['260101T0001-ct']);
42
42
  const issues = body['response content']['basics about all issues reported in the report'];
43
43
  assert.ok(issues);
44
44
  assert.equal(issues.length, 0);
45
45
  });
46
46
 
47
47
  test('listIssues treats missing outcome as a violation, not cantTell', async () => {
48
- const body = await response(['260101T0002', 'no']);
48
+ const body = await response(['260101T0002-no']);
49
49
  const issues = body['response content']['basics about all issues reported in the report'];
50
50
  assert.ok(issues);
51
51
  assert.equal(issues.length, 1);
@@ -53,14 +53,14 @@ test('listIssues treats missing outcome as a violation, not cantTell', async ()
53
53
  });
54
54
 
55
55
  test('listIssues returns 0 issues for the empty report', async () => {
56
- const body = await response(['260101T0005', 'emp']);
56
+ const body = await response(['260101T0005-emp']);
57
57
  const issues = body['response content']['basics about all issues reported in the report'];
58
58
  assert.ok(issues);
59
59
  assert.equal(issues.length, 0);
60
60
  });
61
61
 
62
62
  test('listIssues reports prevented rule engines for the prevented report', async () => {
63
- const body = await response(['260101T0006', 'prv']);
63
+ const body = await response(['260101T0006-prv']);
64
64
  const details = body['response content']['details about the report'] as any;
65
65
  const preventions = details['test results']['rule engines that could not test the page'];
66
66
  assert.equal(preventions.length, 1);
@@ -69,7 +69,7 @@ test('listIssues reports prevented rule engines for the prevented report', async
69
69
  });
70
70
 
71
71
  test('listIssues includes reporter names for each issue in the mixed report', async () => {
72
- const body = await response(['260101T0000', 'mix']);
72
+ const body = await response(['260101T0000-mix']);
73
73
  const issues = body['response content']['basics about all issues reported in the report'];
74
74
  assert.ok(issues);
75
75
  const linkIssue = issues.find(i => i.identifier === 'linkNoText');
@@ -81,7 +81,7 @@ test('listIssues includes reporter names for each issue in the mixed report', as
81
81
  });
82
82
 
83
83
  test('listIssues counts issues by priority correctly for the mixed report', async () => {
84
- const body = await response(['260101T0000', 'mix']);
84
+ const body = await response(['260101T0000-mix']);
85
85
  const details = body['response content']['details about the report'] as any;
86
86
  const counts = details['test results']['counts of issues by priority'];
87
87
  // linkNoText weight 4 (highest), allCaps weight 1 (lowest).
@@ -92,19 +92,19 @@ test('listIssues counts issues by priority correctly for the mixed report', asyn
92
92
  });
93
93
 
94
94
  test('listIssues reports the superseded status for the mixed report', async () => {
95
- const body = await response(['260101T0000', 'mix']);
95
+ const body = await response(['260101T0000-mix']);
96
96
  const basics = body['response content']['basics about the report'] as any;
97
97
  assert.equal(basics['whether a later report about the same page exists'], true);
98
98
  });
99
99
 
100
100
  test('listIssues returns an error for a nonexistent report', async () => {
101
- const body = await response(['999999T9999', 'xyz']);
101
+ const body = await response(['999999T9999-xyz']);
102
102
  const basics = body['response content']['basics about the report'] as any;
103
103
  assert.ok(basics.error);
104
104
  });
105
105
 
106
106
  test('listIssues handles instances with missing issueID and null instances without error', async () => {
107
- const body = await response(['260101T0009', 'brd']);
107
+ const body = await response(['260101T0009-brd']);
108
108
  const issues = body['response content']['basics about all issues reported in the report'];
109
109
  // The brd fixture has one instance with duplicateID (a valid issue) and one with no issueID (skipped).
110
110
  // The alfa act has no instances array, so it should be handled as empty.
@@ -115,7 +115,7 @@ test('listIssues handles instances with missing issueID and null instances witho
115
115
  });
116
116
 
117
117
  test('listIssues lists the history of reports about the page, from oldest to latest', async () => {
118
- const body = await response(['260101T0000', 'mix']);
118
+ const body = await response(['260101T0000-mix']);
119
119
  const history = body['response content']['history of reports about the page']!;
120
120
  assert.deepEqual(history.map(entry => entry.identifier), ['260101T0000-mix', '260202T0000-new']);
121
121
  assert.deepEqual(
@@ -128,12 +128,12 @@ test('listIssues lists the history of reports about the page, from oldest to lat
128
128
  );
129
129
  assert.equal(history[1]!['completion date and time'], '2026-02-02T00:10:00.000Z');
130
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'));
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
133
  });
134
134
 
135
135
  test('listIssues gives a one-report history for the only report about a page', async () => {
136
- const body = await response(['260101T0001', 'ct']);
136
+ const body = await response(['260101T0001-ct']);
137
137
  const history = body['response content']['history of reports about the page']!;
138
138
  assert.equal(history.length, 1);
139
139
  assert.equal(history[0]!['whether it is the oldest report about the page'], true);
@@ -142,6 +142,6 @@ test('listIssues gives a one-report history for the only report about a page', a
142
142
  });
143
143
 
144
144
  test('listIssues gives no history for a nonexistent report', async () => {
145
- const body = await response(['999999T9999', 'xyz']);
145
+ const body = await response(['999999T9999-xyz']);
146
146
  assert.equal(body['response content']['history of reports about the page'], null);
147
147
  });