@jrpool/kilotest 59.1.4 → 60.0.1

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 (62) 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 +43 -1
  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 +1 -1
  37. package/routing.test.ts +1 -1
  38. package/scripts/generate-openapi.ts +83 -1
  39. package/server.json +1 -1
  40. package/util.test.ts +32 -33
  41. package/util.ts +47 -43
  42. package/web/hideReportForm/index.ts +5 -6
  43. package/web/listDiagnoses/index.ts +4 -5
  44. package/web/listDiagnoses/listDiagnoses.test.ts +13 -13
  45. package/web/listIssues/index.ts +11 -12
  46. package/web/listIssues/listIssues.test.ts +24 -24
  47. package/web/listPages/index.ts +2 -2
  48. package/web/listPages/listPages.test.ts +2 -2
  49. package/web/listTopIssues/index.ts +2 -2
  50. package/web/listViolators/index.ts +5 -6
  51. package/web/listViolators/listViolators.test.ts +12 -12
  52. package/web/reannotate/index.ts +3 -3
  53. package/web/reannotateForm/index.ts +1 -2
  54. package/web/reportDeletion.ts +9 -12
  55. package/web/requestRetest/index.ts +2 -2
  56. package/web/requestRetest/requestRetest.test.ts +7 -7
  57. package/web/requestRetestForm/index.html +2 -2
  58. package/web/requestRetestForm/index.ts +3 -4
  59. package/web/requestRetestForm/requestRetestForm.test.ts +7 -8
  60. package/web/unhideReportForm/index.ts +4 -6
  61. package/web/util.test.ts +29 -29
  62. package/web/util.ts +1 -1
@@ -64,7 +64,7 @@ after(() => {
64
64
  // TESTS
65
65
 
66
66
  test('listDiagnoses returns an ok status with valid HTML for catalogIndex 0, linkNoText, in the mixed report', async () => {
67
- const result = await answer('linkNoText/260101T0000/mix/0');
67
+ const result = await answer('linkNoText/260101T0000-mix/0');
68
68
  assert.equal(result.status, 'ok');
69
69
  assert.ok(result.answerPage);
70
70
  const html = parse(result.answerPage);
@@ -72,34 +72,34 @@ test('listDiagnoses returns an ok status with valid HTML for catalogIndex 0, lin
72
72
  });
73
73
 
74
74
  test('listDiagnoses includes the issue summary in the HTML', async () => {
75
- const result = await answer('linkNoText/260101T0000/mix/0');
75
+ const result = await answer('linkNoText/260101T0000-mix/0');
76
76
  assert.ok(result.answerPage.includes('link not named'));
77
77
  });
78
78
 
79
79
  test('listDiagnoses includes the element tag name and text from the catalog', async () => {
80
- const result = await answer('linkNoText/260101T0000/mix/0');
80
+ const result = await answer('linkNoText/260101T0000-mix/0');
81
81
  assert.ok(result.answerPage.includes('About Us'));
82
82
  });
83
83
 
84
84
  test('listDiagnoses includes diagnosis descriptions from the rule engines', async () => {
85
- const result = await answer('linkNoText/260101T0000/mix/0');
85
+ const result = await answer('linkNoText/260101T0000-mix/0');
86
86
  assert.ok(result.answerPage.includes('The link does not have an accessible name'));
87
87
  });
88
88
 
89
89
  test('listDiagnoses excludes cantTell instances from the diagnoses list', async () => {
90
- const result = await answer('focusIndicationBad/260101T0000/mix/0');
90
+ const result = await answer('focusIndicationBad/260101T0000-mix/0');
91
91
  assert.equal(result.status, 'ok');
92
92
  assert.ok(!result.answerPage.includes('Focus Visible'));
93
93
  });
94
94
 
95
95
 
96
96
  test('listDiagnoses returns an error status for a nonexistent report', async () => {
97
- const result = await answer('linkNoText/999999T9999/xyz/0');
97
+ const result = await answer('linkNoText/999999T9999-xyz/0');
98
98
  assert.equal(result.status, 'error');
99
99
  });
100
100
 
101
101
  test('listDiagnoses returns ok with no diagnoses for a nonexistent catalog index', async () => {
102
- const result = await answer('linkNoText/260101T0000/mix/999');
102
+ const result = await answer('linkNoText/260101T0000-mix/999');
103
103
  assert.equal(result.status, 'ok');
104
104
  const html = parse(result.answerPage);
105
105
  const diagnosisItems = html.querySelectorAll('li');
@@ -112,7 +112,7 @@ test('listDiagnoses returns an error when getReport fails', async () => {
112
112
  getReportCallCount = 0;
113
113
  failGetReportOnCall = 1;
114
114
  try {
115
- const result = await answer('linkNoText/260101T0000/mix/0');
115
+ const result = await answer('linkNoText/260101T0000-mix/0');
116
116
  assert.equal(result.status, 'error');
117
117
  assert.ok(result.message.includes('invalid'));
118
118
  }
@@ -124,7 +124,7 @@ test('listDiagnoses returns an error when getReport fails', async () => {
124
124
  test('listDiagnoses returns an error when getPageDataStrings fails after getReport succeeds', async () => {
125
125
  pageDataStringsOverride = {error: 'Page data strings error'};
126
126
  try {
127
- const result = await answer('linkNoText/260101T0000/mix/0');
127
+ const result = await answer('linkNoText/260101T0000-mix/0');
128
128
  assert.equal(result.status, 'error');
129
129
  assert.equal(result.message, 'Page data strings error');
130
130
  }
@@ -183,7 +183,7 @@ test('listDiagnoses includes a take-me-there link for a text-linkable catalog it
183
183
  checkpoints: []
184
184
  };
185
185
  await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
186
- const result = await answer('linkNoText/260101T0003/tlk/0');
186
+ const result = await answer('linkNoText/260101T0003-tlk/0');
187
187
  assert.equal(result.status, 'ok');
188
188
  assert.ok(result.answerPage.includes('Take me there'));
189
189
  }
@@ -193,7 +193,7 @@ test('listDiagnoses includes a take-me-there link for a text-linkable catalog it
193
193
  });
194
194
 
195
195
  test('listDiagnoses returns an error for an unknown issue with a valid report', async () => {
196
- const result = await answer('nonexistentIssue/260101T0000/mix/0');
196
+ const result = await answer('nonexistentIssue/260101T0000-mix/0');
197
197
  assert.equal(result.status, 'error');
198
198
  assert.equal(result.message, 'Issue not found');
199
199
  });
@@ -205,7 +205,7 @@ test('listDiagnoses returns an error when report facts are not obtained', async
205
205
  urlLink: '<a href="https://example.com/mixed">https://example.com/mixed</a>'
206
206
  };
207
207
  try {
208
- const result = await answer('linkNoText/260101T0000/mix/0');
208
+ const result = await answer('linkNoText/260101T0000-mix/0');
209
209
  assert.equal(result.status, 'error');
210
210
  assert.equal(result.message, 'Report facts not obtained');
211
211
  }
@@ -248,7 +248,7 @@ test('listDiagnoses handles acts with no standardResult instances gracefully', a
248
248
  };
249
249
  await fs.writeFile(reportPath, JSON.stringify(report));
250
250
  try {
251
- const result = await answer('linkNoText/260101T0004/nst/0');
251
+ const result = await answer('linkNoText/260101T0004-nst/0');
252
252
  assert.equal(result.status, 'ok');
253
253
  }
254
254
  finally {
@@ -125,15 +125,14 @@ const getIssuesData = (report: UsableReport): ResultsSummary & Record<string, an
125
125
  };
126
126
  // Returns data and HTML lines about the history of the reports about the page of a report.
127
127
  const getHistory = async (
128
- timeStamp: string, jobID: string, description: string, margin: string = ''
128
+ reportID: string, description: string, margin: string = ''
129
129
  ): Promise<{isLatest: boolean, lines: string[]}> => {
130
130
  // Get extracts of all reports about the page, from oldest to latest.
131
131
  const pageExtracts = await getPageReportExtracts(description);
132
132
  // The report's own extract may be absent if the report was quarantined between the
133
133
  // report read and the extract listing, so an empty list means "not latest".
134
134
  const lastExtract = pageExtracts[pageExtracts.length - 1];
135
- const isLatest = lastExtract !== undefined
136
- && lastExtract.timeStamp === timeStamp && lastExtract.jobID === jobID;
135
+ const isLatest = lastExtract !== undefined && lastExtract.id === reportID;
137
136
  // Initialize the history.
138
137
  const history = {
139
138
  isLatest,
@@ -142,7 +141,7 @@ const getHistory = async (
142
141
  // For each report about the page, from oldest to latest:
143
142
  pageExtracts.forEach((extract, index) => {
144
143
  // Get whether it is this report.
145
- const isThis = extract.timeStamp === timeStamp && extract.jobID === jobID;
144
+ const isThis = extract.id === reportID;
146
145
  // Get the tags that identify its position and whether it is this report.
147
146
  const tags = [
148
147
  index === 0 ? 'oldest' : '',
@@ -158,7 +157,7 @@ const getHistory = async (
158
157
  content = label;
159
158
  }
160
159
  else {
161
- const href = `/listIssues.html/${extract.timeStamp}/${extract.jobID}`;
160
+ const href = `/listIssues.html/${extract.id}`;
162
161
  content = `<a href="${href}">${label}</a>`;
163
162
  }
164
163
  // Add a list item describing the report, linked if not this report, to the history.
@@ -172,7 +171,7 @@ const getHistory = async (
172
171
  if (isLatest) {
173
172
  // Add a link to the form for requesting a retest to the lines.
174
173
  lines.push(
175
- `${margin}<p><a href="/requestRetestForm.html/${timeStamp}/${jobID}">Request a retest</a></p>`
174
+ `${margin}<p><a href="/requestRetestForm.html/${reportID}">Request a retest</a></p>`
176
175
  );
177
176
  }
178
177
  // Replace the bare list items with the assembled section lines.
@@ -181,9 +180,9 @@ const getHistory = async (
181
180
  return history;
182
181
  };
183
182
  // Adds parameters to a query for the answer page.
184
- const populateQuery = async (timeStamp: string, jobID: string, query: Record<string, any>) => {
183
+ const populateQuery = async (reportID: string, query: Record<string, any>) => {
185
184
  // Get the report.
186
- const report = await getReport(timeStamp, jobID);
185
+ const report = await getReport(reportID);
187
186
  // If this failed:
188
187
  if (isReportError(report)) {
189
188
  // Populate the query with the reason.
@@ -207,7 +206,7 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
207
206
  query.target = description;
208
207
  query.testInfo = testInfo;
209
208
  const margin = ' '.repeat(6);
210
- const history = await getHistory(timeStamp, jobID, description, ' '.repeat(8));
209
+ const history = await getHistory(reportID, description, ' '.repeat(8));
211
210
  // Add a warning if the report is not the latest about the page.
212
211
  query.oldWarn = history.isLatest
213
212
  ? ''
@@ -248,7 +247,7 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
248
247
  detailsLines.push(`${margin} <ul class="nav">`);
249
248
  const whereQuestionString = 'Where was the issue found?';
250
249
  const labelString = `Where was the ${summary} issue found on the ${description} page?`;
251
- const href = `href="/listViolators.html/${issueID}/${timeStamp}/${jobID}"`;
250
+ const href = `href="/listViolators.html/${issueID}/${reportID}"`;
252
251
  const label = `aria-label="${labelString}"`;
253
252
  const whereLink = `<a ${href} ${label}>${whereQuestionString}</a>`;
254
253
  // Add a violations link to the lines.
@@ -269,10 +268,10 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
269
268
  };
270
269
  // Returns a page answering the target-issues question.
271
270
  export const answer = async (pageArgs: string) => {
272
- const [timeStamp, jobID] = pageArgs.split('/') as [string, string];
271
+ const reportID = pageArgs;
273
272
  const query: Record<string, any> = {};
274
273
  // Create a query to replace the placeholders.
275
- await populateQuery(timeStamp, jobID, query);
274
+ await populateQuery(reportID, query);
276
275
  // If this failed:
277
276
  if (query.error) {
278
277
  // Return the error.
@@ -49,7 +49,7 @@ after(() => {
49
49
  // TESTS
50
50
 
51
51
  test('listIssues returns an ok status with valid HTML for the mixed report', async () => {
52
- const result = await answer('260101T0000/mix');
52
+ const result = await answer('260101T0000-mix');
53
53
  assert.equal(result.status, 'ok');
54
54
  assert.ok(result.answerPage);
55
55
  const html = parse(result.answerPage);
@@ -57,12 +57,12 @@ test('listIssues returns an ok status with valid HTML for the mixed report', asy
57
57
  });
58
58
 
59
59
  test('listIssues includes the page description in the HTML for the mixed report', async () => {
60
- const result = await answer('260101T0000/mix');
60
+ const result = await answer('260101T0000-mix');
61
61
  assert.ok(result.answerPage.includes('Mixed Outcomes'));
62
62
  });
63
63
 
64
64
  test('listIssues interpolates the page description into the AI prompt block quote', async () => {
65
- const result = await answer('260101T0000/mix');
65
+ const result = await answer('260101T0000-mix');
66
66
  const html = parse(result.answerPage);
67
67
  const blockQuote = html.querySelector('blockquote');
68
68
  assert.ok(blockQuote);
@@ -74,7 +74,7 @@ test('listIssues interpolates the page description into the AI prompt block quot
74
74
  });
75
75
 
76
76
  test('listIssues includes links to listViolators for issues in the mixed report', async () => {
77
- const result = await answer('260101T0000/mix');
77
+ const result = await answer('260101T0000-mix');
78
78
  const html = parse(result.answerPage);
79
79
  const violatorLinks = html.querySelectorAll('a[href*="listViolators.html"]');
80
80
  assert.ok(violatorLinks.length >= 2);
@@ -84,12 +84,12 @@ test('listIssues includes links to listViolators for issues in the mixed report'
84
84
  });
85
85
 
86
86
  test('listIssues returns an error status for a nonexistent report', async () => {
87
- const result = await answer('999999T9999/xyz');
87
+ const result = await answer('999999T9999-xyz');
88
88
  assert.equal(result.status, 'error');
89
89
  });
90
90
 
91
91
  test('listIssues for the all-cantTell report shows no issue links', async () => {
92
- const result = await answer('260101T0001/ct');
92
+ const result = await answer('260101T0001-ct');
93
93
  assert.equal(result.status, 'ok');
94
94
  const html = parse(result.answerPage);
95
95
  const violatorLinks = html.querySelectorAll('a[href*="listViolators.html"]');
@@ -97,7 +97,7 @@ test('listIssues for the all-cantTell report shows no issue links', async () =>
97
97
  });
98
98
 
99
99
  test('listIssues for the empty report shows no issue links', async () => {
100
- const result = await answer('260101T0005/emp');
100
+ const result = await answer('260101T0005-emp');
101
101
  assert.equal(result.status, 'ok');
102
102
  const html = parse(result.answerPage);
103
103
  const violatorLinks = html.querySelectorAll('a[href*="listViolators.html"]');
@@ -106,7 +106,7 @@ test('listIssues for the empty report shows no issue links', async () => {
106
106
 
107
107
 
108
108
  test('listIssues counts the prevented rule engine as called but not able to test', async () => {
109
- const result = await answer('260101T0006/prv');
109
+ const result = await answer('260101T0006-prv');
110
110
  assert.equal(result.status, 'ok');
111
111
  assert.ok(result.answerPage.includes('<li>Called: 2</li>'));
112
112
  assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
@@ -114,7 +114,7 @@ test('listIssues counts the prevented rule engine as called but not able to test
114
114
  });
115
115
 
116
116
  test('listIssues counts a rule engine that reported no violations as called and able to test', async () => {
117
- const result = await answer('260101T0005/emp');
117
+ const result = await answer('260101T0005-emp');
118
118
  assert.equal(result.status, 'ok');
119
119
  assert.ok(result.answerPage.includes('<li>Called: 1</li>'));
120
120
  assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
@@ -129,7 +129,7 @@ test('listIssues counts an unknown prevented engine as called but not able to te
129
129
  const reportPath = path.join(realUtil.reportsPath(), '260103T0000-unk.json');
130
130
  await fs.writeFile(reportPath, JSON.stringify(report));
131
131
  try {
132
- const result = await answer('260103T0000/unk');
132
+ const result = await answer('260103T0000-unk');
133
133
  assert.equal(result.status, 'ok');
134
134
  assert.ok(result.answerPage.includes('<li>Called: 3</li>'));
135
135
  assert.ok(result.answerPage.includes('<li>Were able to test: 1</li>'));
@@ -142,7 +142,7 @@ test('listIssues counts an unknown prevented engine as called but not able to te
142
142
  test('listIssues returns an error when getPageDataStrings fails after getReport succeeds', async () => {
143
143
  pageDataStringsOverride = {error: 'Page data strings error'};
144
144
  try {
145
- const result = await answer('260101T0000/mix');
145
+ const result = await answer('260101T0000-mix');
146
146
  assert.equal(result.status, 'error');
147
147
  assert.equal(result.message, 'Page data strings error');
148
148
  }
@@ -158,7 +158,7 @@ test('listIssues returns an error when report facts are not obtained', async ()
158
158
  urlLink: '<a href="https://example.com/mixed">https://example.com/mixed</a>'
159
159
  };
160
160
  try {
161
- const result = await answer('260101T0000/mix');
161
+ const result = await answer('260101T0000-mix');
162
162
  assert.equal(result.status, 'error');
163
163
  assert.equal(result.message, 'Report facts not obtained');
164
164
  }
@@ -169,7 +169,7 @@ test('listIssues returns an error when report facts are not obtained', async ()
169
169
 
170
170
  test('listIssues shows violation and violator counts for an issue with multiple violators', async () => {
171
171
  // The mul report has linkNoText with 4 violations by 3 violators.
172
- const result = await answer('260101T0008/mul');
172
+ const result = await answer('260101T0008-mul');
173
173
  assert.equal(result.status, 'ok');
174
174
  assert.ok(result.answerPage.includes('<li>Violations: 4</li>'));
175
175
  assert.ok(result.answerPage.includes('<li>Violators: 3</li>'));
@@ -198,7 +198,7 @@ test('listIssues handles acts with no standardResult instances', async () => {
198
198
  };
199
199
  await fs.writeFile(reportPath, JSON.stringify(report));
200
200
  try {
201
- const result = await answer('260101T0004/nsi');
201
+ const result = await answer('260101T0004-nsi');
202
202
  assert.equal(result.status, 'ok');
203
203
  }
204
204
  finally {
@@ -223,38 +223,38 @@ const getHistoryItems = (history: ReturnType<typeof parse>) => history
223
223
  }));
224
224
 
225
225
  test('listIssues History of an older report lists all reports, with this one unlinked and no retest question', async () => {
226
- const result = await answer('260101T0000/mix');
226
+ const result = await answer('260101T0000-mix');
227
227
  const history = getHistory(result.answerPage);
228
228
  const items = getHistoryItems(history);
229
229
  assert.equal(items.length, 2);
230
230
  assert.match(items[0]!.text, /^\d+ days? ago \(oldest; this report\)$/);
231
231
  assert.equal(items[0]!.href, null);
232
232
  assert.match(items[1]!.text, /^\d+ days? ago \(latest\)$/);
233
- assert.equal(items[1]!.href, '/listIssues.html/260202T0000/new');
233
+ assert.equal(items[1]!.href, '/listIssues.html/260202T0000-new');
234
234
  assert.ok(!result.answerPage.includes('Request a retest'));
235
235
  });
236
236
 
237
237
  test('listIssues History of the latest report ends with the retest question', async () => {
238
- const result = await answer('260202T0000/new');
238
+ const result = await answer('260202T0000-new');
239
239
  const history = getHistory(result.answerPage);
240
240
  const items = getHistoryItems(history);
241
241
  assert.equal(items.length, 2);
242
242
  assert.match(items[0]!.text, /^\d+ days? ago \(oldest\)$/);
243
- assert.equal(items[0]!.href, '/listIssues.html/260101T0000/mix');
243
+ assert.equal(items[0]!.href, '/listIssues.html/260101T0000-mix');
244
244
  assert.match(items[1]!.text, /^\d+ days? ago \(latest; this report\)$/);
245
245
  assert.equal(items[1]!.href, null);
246
246
  const retestLink = history.querySelector('ol + p > a');
247
247
  assert.equal(retestLink?.text, 'Request a retest');
248
- assert.equal(retestLink?.getAttribute('href'), '/requestRetestForm.html/260202T0000/new');
248
+ assert.equal(retestLink?.getAttribute('href'), '/requestRetestForm.html/260202T0000-new');
249
249
  });
250
250
 
251
251
  test('listIssues History of the only report about a page says so and asks about retesting', async () => {
252
- const result = await answer('260101T0001/ct');
252
+ const result = await answer('260101T0001-ct');
253
253
  const history = getHistory(result.answerPage);
254
254
  assert.equal(getHistoryItems(history).length, 0);
255
255
  const paragraphs = history.querySelectorAll('p');
256
256
  assert.equal(paragraphs[0]?.text, 'This is the only available report about the page.');
257
- assert.equal(paragraphs[1]?.querySelector('a')?.getAttribute('href'), '/requestRetestForm.html/260101T0001/ct');
257
+ assert.equal(paragraphs[1]?.querySelector('a')?.getAttribute('href'), '/requestRetestForm.html/260101T0001-ct');
258
258
  });
259
259
 
260
260
  test('listIssues History tags only the ends of a list of 3 reports and the current report', async () => {
@@ -266,16 +266,16 @@ test('listIssues History tags only the ends of a list of 3 reports and the curre
266
266
  await fs.writeFile(reportPath, JSON.stringify(report));
267
267
  try {
268
268
  // From the middle report:
269
- let items = getHistoryItems(getHistory((await answer('260115T0000/mid')).answerPage));
269
+ let items = getHistoryItems(getHistory((await answer('260115T0000-mid')).answerPage));
270
270
  assert.equal(items.length, 3);
271
271
  assert.match(items[0]!.text, /\(oldest\)$/);
272
272
  assert.match(items[1]!.text, /^\d+ days? ago \(this report\)$/);
273
273
  assert.equal(items[1]!.href, null);
274
274
  assert.match(items[2]!.text, /\(latest\)$/);
275
275
  // From the latest report:
276
- items = getHistoryItems(getHistory((await answer('260202T0000/new')).answerPage));
276
+ items = getHistoryItems(getHistory((await answer('260202T0000-new')).answerPage));
277
277
  assert.match(items[1]!.text, /^\d+ days? ago$/);
278
- assert.equal(items[1]!.href, '/listIssues.html/260115T0000/mid');
278
+ assert.equal(items[1]!.href, '/listIssues.html/260115T0000-mid');
279
279
  }
280
280
  finally {
281
281
  await fs.unlink(reportPath);
@@ -69,9 +69,9 @@ const populateQuery = async (query: Record<string, any>) => {
69
69
  const pageCount = latestExtracts.length;
70
70
  // For each tested page:
71
71
  for (const extract of latestExtracts) {
72
- const {jobID, timeStamp, description} = extract;
72
+ const {id, description} = extract;
73
73
  // Add a link to the issues in the latest report about it to the lines.
74
- const link = `<a href="listIssues.html/${timeStamp}/${jobID}" aria-label="Get test results for ${description}">${description}</a>`;
74
+ const link = `<a href="listIssues.html/${id}" aria-label="Get test results for ${description}">${description}</a>`;
75
75
  lines.tested.push(`${margin} <li>${link}</li>`);
76
76
  }
77
77
  // Add the lines to the query, as an introduced list if there are any, or else a no-pages message.
@@ -76,9 +76,9 @@ test('listPages links each page to the issues in its latest report', async () =>
76
76
  const result: any = await answer();
77
77
  const links = getPageLinks(result.answerPage);
78
78
  const mixedLink = links.find(a => a.text === 'Mixed Outcomes');
79
- assert.equal(mixedLink?.getAttribute('href'), 'listIssues.html/260202T0000/new');
79
+ assert.equal(mixedLink?.getAttribute('href'), 'listIssues.html/260202T0000-new');
80
80
  const canttellLink = links.find(a => a.text === 'All CantTell Page');
81
- assert.equal(canttellLink?.getAttribute('href'), 'listIssues.html/260101T0001/ct');
81
+ assert.equal(canttellLink?.getAttribute('href'), 'listIssues.html/260101T0001-ct');
82
82
  });
83
83
 
84
84
  test('listPages contains no report summaries', async () => {
@@ -36,9 +36,9 @@ const getIssuesSummary = async () => {
36
36
  const latestReportExtracts = await getReportExtracts(true);
37
37
  // For each of them:
38
38
  for (const reportExtract of latestReportExtracts) {
39
- const {timeStamp, jobID} = reportExtract;
39
+ const {id} = reportExtract;
40
40
  // Get the corresponding report.
41
- const report = await getReport(timeStamp, jobID);
41
+ const report = await getReport(id);
42
42
  // If this failed:
43
43
  if (isReportError(report)) {
44
44
  // Populate the summary with the reason.
@@ -27,12 +27,11 @@ import {issues as issueSpecs} from 'testaro-issues';
27
27
  // Adds parameters to a query for the answer page.
28
28
  const populateQuery = async (
29
29
  issueID: string,
30
- timeStamp: string,
31
- jobID: string,
30
+ reportID: string,
32
31
  query: Record<string, any>
33
32
  ) => {
34
33
  // Get the report.
35
- const report = await getReport(timeStamp, jobID);
34
+ const report = await getReport(reportID);
36
35
  // If this failed:
37
36
  if (isReportError(report)) {
38
37
  // Populate the query with the reason.
@@ -110,7 +109,7 @@ const populateQuery = async (
110
109
  }
111
110
  }
112
111
  const href
113
- = `/listDiagnoses.html/${issueID}/${timeStamp}/${jobID}/${catalogIndex}?pathID=${pathID}`;
112
+ = `/listDiagnoses.html/${issueID}/${reportID}/${catalogIndex}?pathID=${pathID}`;
114
113
  const questionString = 'What diagnoses were reported';
115
114
  const labelString = `${questionString} for violator ${index + 1}?`;
116
115
  lines.push(
@@ -130,10 +129,10 @@ const populateQuery = async (
130
129
  };
131
130
  // Returns a page answering the violators question.
132
131
  export const answer = async (pageArgs: string) => {
133
- const [issueID, timeStamp, jobID] = pageArgs.split('/') as [string, string, string];
132
+ const [issueID, reportID] = pageArgs.split('/') as [string, string];
134
133
  const query: Record<string, any> = {};
135
134
  // Create a query to replace the placeholders.
136
- await populateQuery(issueID, timeStamp, jobID, query);
135
+ await populateQuery(issueID, reportID, query);
137
136
  // If this failed:
138
137
  if (query.error) {
139
138
  // Return the error.
@@ -64,7 +64,7 @@ after(() => {
64
64
  // TESTS
65
65
 
66
66
  test('listViolators returns an ok status with valid HTML for linkNoText in the mixed report', async () => {
67
- const result = await answer('linkNoText/260101T0000/mix');
67
+ const result = await answer('linkNoText/260101T0000-mix');
68
68
  assert.equal(result.status, 'ok');
69
69
  assert.ok(result.answerPage);
70
70
  const html = parse(result.answerPage);
@@ -72,19 +72,19 @@ test('listViolators returns an ok status with valid HTML for linkNoText in the m
72
72
  });
73
73
 
74
74
  test('listViolators includes the issue summary in the HTML', async () => {
75
- const result = await answer('linkNoText/260101T0000/mix');
75
+ const result = await answer('linkNoText/260101T0000-mix');
76
76
  assert.ok(result.answerPage.includes('link not named'));
77
77
  });
78
78
 
79
79
  test('listViolators includes links to listDiagnoses for violators', async () => {
80
- const result = await answer('linkNoText/260101T0000/mix');
80
+ const result = await answer('linkNoText/260101T0000-mix');
81
81
  const html = parse(result.answerPage);
82
82
  const diagnosisLinks = html.querySelectorAll('a[href*="listDiagnoses.html"]');
83
83
  assert.ok(diagnosisLinks.length > 0);
84
84
  });
85
85
 
86
86
  test('listViolators excludes cantTell instances from the violator count', async () => {
87
- const result = await answer('focusIndicationBad/260101T0000/mix');
87
+ const result = await answer('focusIndicationBad/260101T0000-mix');
88
88
  assert.equal(result.status, 'ok');
89
89
  assert.ok(result.answerPage.includes('<li>Violators: 0</li>'));
90
90
  assert.ok(!result.answerPage.includes('<h3>Element'));
@@ -92,12 +92,12 @@ test('listViolators excludes cantTell instances from the violator count', async
92
92
 
93
93
 
94
94
  test('listViolators returns an error status for a nonexistent report', async () => {
95
- const result = await answer('linkNoText/999999T9999/xyz');
95
+ const result = await answer('linkNoText/999999T9999-xyz');
96
96
  assert.equal(result.status, 'error');
97
97
  });
98
98
 
99
99
  test('listViolators returns an error status for an unknown issue', async () => {
100
- const result = await answer('nonexistentIssue/260101T0000/mix');
100
+ const result = await answer('nonexistentIssue/260101T0000-mix');
101
101
  assert.equal(result.status, 'error');
102
102
  });
103
103
 
@@ -105,7 +105,7 @@ test('listViolators returns an error when getReport fails', async () => {
105
105
  getReportCallCount = 0;
106
106
  failGetReportOnCall = 1;
107
107
  try {
108
- const result = await answer('linkNoText/260101T0000/mix');
108
+ const result = await answer('linkNoText/260101T0000-mix');
109
109
  assert.equal(result.status, 'error');
110
110
  assert.ok(result.message.includes('invalid'));
111
111
  }
@@ -117,7 +117,7 @@ test('listViolators returns an error when getReport fails', async () => {
117
117
  test('listViolators returns an error when getPageDataStrings fails after getReport succeeds', async () => {
118
118
  pageDataStringsOverride = {error: 'Page data strings error'};
119
119
  try {
120
- const result = await answer('linkNoText/260101T0000/mix');
120
+ const result = await answer('linkNoText/260101T0000-mix');
121
121
  assert.equal(result.status, 'error');
122
122
  assert.equal(result.message, 'Page data strings error');
123
123
  }
@@ -176,7 +176,7 @@ test('listViolators includes take-me-there links for text-linkable violators', a
176
176
  checkpoints: []
177
177
  };
178
178
  await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
179
- const result = await answer('linkNoText/260101T0003/tlk');
179
+ const result = await answer('linkNoText/260101T0003-tlk');
180
180
  assert.equal(result.status, 'ok');
181
181
  assert.ok(result.answerPage.includes('Take me there'));
182
182
  assert.ok(result.answerPage.includes('Take me there</q> links will open'));
@@ -193,7 +193,7 @@ test('listViolators returns an error when report facts are not obtained', async
193
193
  urlLink: '<a href="https://example.com/mixed">https://example.com/mixed</a>'
194
194
  };
195
195
  try {
196
- const result = await answer('linkNoText/260101T0000/mix');
196
+ const result = await answer('linkNoText/260101T0000-mix');
197
197
  assert.equal(result.status, 'error');
198
198
  assert.equal(result.message, 'Report facts not obtained');
199
199
  }
@@ -269,7 +269,7 @@ test('listViolators handles instances with missing catalogIndex and acts with no
269
269
  };
270
270
  await fs.writeFile(reportPath, JSON.stringify(report));
271
271
  try {
272
- const result = await answer('linkNoText/260101T0004/nvi');
272
+ const result = await answer('linkNoText/260101T0004-nvi');
273
273
  assert.equal(result.status, 'ok');
274
274
  // Should show plural violator count.
275
275
  assert.ok(result.answerPage.includes('<li>Violators: 2</li>'));
@@ -336,7 +336,7 @@ test('listViolators uses HTML fallback when catalog has no tagName and pathID is
336
336
  };
337
337
  await fs.writeFile(reportPath, JSON.stringify(report));
338
338
  try {
339
- const result = await answer('linkNoText/260101T0005/htm');
339
+ const result = await answer('linkNoText/260101T0005-htm');
340
340
  assert.equal(result.status, 'ok');
341
341
  // The tagName should fall back to HTML.
342
342
  assert.ok(result.answerPage.includes('HTML'));
@@ -31,9 +31,9 @@ export const answer = async (authCode: string) => {
31
31
  if (reportFileNames.length) {
32
32
  // For each report file:
33
33
  for (const reportFileName of reportFileNames) {
34
- const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-') as [string, string];
34
+ const reportID = reportFileName.slice(0, -5);
35
35
  // Get the report.
36
- const report = await getReport(timeStamp, jobID);
36
+ const report = await getReport(reportID);
37
37
  // If this failed:
38
38
  if (isReportError(report)) {
39
39
  // Return an error page.
@@ -45,7 +45,7 @@ export const answer = async (authCode: string) => {
45
45
  // Otherwise, i.e. if it succeeded, reannotate it in place.
46
46
  await annotateReportObject(report);
47
47
  // Save the reannotated report.
48
- await fs.writeFile(getReportPath(timeStamp, jobID), getJSON(report));
48
+ await fs.writeFile(getReportPath(reportID), getJSON(report));
49
49
  }
50
50
  }
51
51
  // Otherwise, i.e. if it failed:
@@ -20,9 +20,8 @@ const populateQuery = async (query: Record<string, any>) => {
20
20
  const reportExtracts = await getReportExtracts();
21
21
  // For each report:
22
22
  for (const reportExtract of reportExtracts) {
23
- const {jobID, timeStamp} = reportExtract;
24
23
  // Get it.
25
- const report = await getReport(timeStamp, jobID);
24
+ const report = await getReport(reportExtract.id);
26
25
  // If this failed:
27
26
  if (isReportError(report)) {
28
27
  // Populate the query with the reason.
@@ -25,8 +25,7 @@ import path from 'node:path';
25
25
 
26
26
  // A summary of an available report, as shown in a deletion form.
27
27
  type ReportSpec = {
28
- timeStamp: string;
29
- jobID: string;
28
+ id: string;
30
29
  issueCount: number;
31
30
  preventedEngineCount: number;
32
31
  url: string;
@@ -91,9 +90,9 @@ export const reportDeletionForm = async (
91
90
  const reportSpecs: ReportSpec[] = [];
92
91
  // For each report:
93
92
  for (const reportName of reportNames) {
94
- const [timeStamp, jobID] = reportName.slice(0, -5).split('-') as [string, string];
93
+ const reportID = reportName.slice(0, -5);
95
94
  // Get the report.
96
- const report = await getReport(timeStamp, jobID);
95
+ const report = await getReport(reportID);
97
96
  // If this failed:
98
97
  if (isReportError(report)) {
99
98
  // Return why.
@@ -107,29 +106,27 @@ export const reportDeletionForm = async (
107
106
  const {issueCount, preventedEngineCount, url} = reportFacts;
108
107
  // Otherwise, i.e. if it succeeded, add the summary to the array.
109
108
  reportSpecs.push({
110
- timeStamp,
111
- jobID,
109
+ id: reportID,
112
110
  issueCount,
113
111
  preventedEngineCount,
114
112
  url
115
113
  });
116
114
  }
117
- // Sort the summaries primarily by URL and secondarily by time stamp.
118
- objectSort(reportSpecs, 'timeStamp', 'alpha');
115
+ // Sort the summaries primarily by URL and secondarily by report identifier.
116
+ objectSort(reportSpecs, 'id', 'alpha');
119
117
  objectSort(reportSpecs, 'url', 'alpha');
120
118
  const lines: string[] = [];
121
119
  const margin = ' '.repeat(12);
122
120
  let anyDeletable = false;
123
121
  // For each summary:
124
122
  reportSpecs.forEach((reportSpec, index) => {
125
- const {timeStamp, jobID, issueCount, preventedEngineCount, url} = reportSpec;
126
- const jobName = `${timeStamp}-${jobID}`;
127
- const specString = `<code>${url}</code> (<code>${jobName}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
123
+ const {id, issueCount, preventedEngineCount, url} = reportSpec;
124
+ const specString = `<code>${url}</code> (<code>${id}</code>): preventions ${preventedEngineCount}, issues ${issueCount}`;
128
125
  // If its report is deletable:
129
126
  if (isDeletable(reportSpecs, index)) {
130
127
  // Add a line with a deletion checkbox.
131
128
  lines.push(
132
- `${margin}<p><input type="checkbox" name="report" value="${jobName}"> ${specString}</p>`
129
+ `${margin}<p><input type="checkbox" name="report" value="${id}"> ${specString}</p>`
133
130
  );
134
131
  anyDeletable = true;
135
132
  }