@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
package/util.test.ts CHANGED
@@ -1009,8 +1009,8 @@ test('processTestRequest returns "url" for a page matching a queued job by an eq
1009
1009
  });
1010
1010
 
1011
1011
  test('processTestRequest returns "nonreport" when the cited report does not exist', async () => {
1012
- // Cite a timeStamp/jobID that does not correspond to any available report.
1013
- const {result} = await processTestRequest('because', {timeStamp: '999999T9999', jobID: 'xxx'});
1012
+ // Cite a reportID that does not correspond to any available report.
1013
+ const {result} = await processTestRequest('because', {reportID: '999999T9999-xxx'});
1014
1014
  assert.equal(result, 'nonreport');
1015
1015
  });
1016
1016
 
@@ -1259,7 +1259,7 @@ test('getReport returns an error for an invalid report', async () => {
1259
1259
  process.env.DB_DIR = tmpDir;
1260
1260
  try {
1261
1261
  const {getReport} = await import('./util.ts');
1262
- const result = await getReport('260101T0000', 'bad') as any;
1262
+ const result = await getReport('260101T0000-bad') as any;
1263
1263
  assert.ok(result.error);
1264
1264
  assert.ok(result.error.includes('not usable'));
1265
1265
  }
@@ -1408,7 +1408,7 @@ test('testRequestsLock is a function (the lock returned by createLock)', () => {
1408
1408
  });
1409
1409
 
1410
1410
  test('getReportPath returns the path of a report file', () => {
1411
- const result = getReportPath('260101T0000', 'mix');
1411
+ const result = getReportPath('260101T0000-mix');
1412
1412
  assert.ok(result.endsWith('260101T0000-mix.json'));
1413
1413
  });
1414
1414
 
@@ -1422,29 +1422,28 @@ test('getReportEndTime returns an invalid date for a report with no completion t
1422
1422
  });
1423
1423
 
1424
1424
  test('getReportStats returns reportSize for a valid report', async () => {
1425
- const stats = await getReportStats('260101T0000', 'mix');
1425
+ const stats = await getReportStats('260101T0000-mix');
1426
1426
  assert.ok(stats);
1427
1427
  assert.equal(typeof stats.reportSize, 'number');
1428
1428
  assert.ok(stats.reportSize > 0);
1429
1429
  });
1430
1430
 
1431
1431
  test('getReportStats returns null for a nonexistent report', async () => {
1432
- const stats = await getReportStats('999999T9999', 'xxx');
1432
+ const stats = await getReportStats('999999T9999-xxx');
1433
1433
  assert.equal(stats, null);
1434
1434
  });
1435
1435
 
1436
1436
 
1437
1437
  test('getReportExtract returns an extract for a valid report', async () => {
1438
- const extract = await getReportExtract('260101T0000', 'mix') as any;
1439
- assert.equal(extract.timeStamp, '260101T0000');
1440
- assert.equal(extract.jobID, 'mix');
1438
+ const extract = await getReportExtract('260101T0000-mix') as any;
1439
+ assert.equal(extract.id, '260101T0000-mix');
1441
1440
  assert.equal(extract.description, 'Mixed Outcomes');
1442
1441
  assert.equal(extract.url, 'https://example.com/mixed');
1443
1442
  assert.ok(extract.reportTime);
1444
1443
  });
1445
1444
 
1446
1445
  test('getReportExtract returns an error for a nonexistent report', async () => {
1447
- const extract = await getReportExtract('999999T9999', 'xxx') as any;
1446
+ const extract = await getReportExtract('999999T9999-xxx') as any;
1448
1447
  assert.ok(extract.error);
1449
1448
  });
1450
1449
 
@@ -1465,7 +1464,7 @@ test('getReportExtract returns an error for an unusable report', async () => {
1465
1464
  jobData: {endTime: '26-01-01T00:00'}
1466
1465
  })
1467
1466
  );
1468
- const extract = await getReportExtract('260101T0000', 'bad') as any;
1467
+ const extract = await getReportExtract('260101T0000-bad') as any;
1469
1468
  assert.ok(extract.error);
1470
1469
  }
1471
1470
  finally {
@@ -1485,7 +1484,7 @@ test('quarantineReport ignores a missing report', async () => {
1485
1484
  const savedDbDir = process.env.DB_DIR;
1486
1485
  process.env.DB_DIR = path.join(tmpRoot, 'db');
1487
1486
  try {
1488
- await quarantineReport('999999T9999', 'xxx', 'not usable');
1487
+ await quarantineReport('999999T9999-xxx', 'not usable');
1489
1488
  // Nothing was created or moved.
1490
1489
  assert.deepEqual(await fs.readdir(tmpRoot), []);
1491
1490
  }
@@ -1510,7 +1509,7 @@ test('quarantineReport leaves a recently modified report in place', async () =>
1510
1509
  const savedDbDir = process.env.DB_DIR;
1511
1510
  process.env.DB_DIR = path.join(tmpRoot, 'db');
1512
1511
  try {
1513
- await quarantineReport('260101T0000', 'bad', 'not usable');
1512
+ await quarantineReport('260101T0000-bad', 'not usable');
1514
1513
  // The fresh file may still be mid-write, so it was not moved.
1515
1514
  await fs.access(filePath);
1516
1515
  }
@@ -1538,7 +1537,7 @@ test('quarantineReport quarantines a report old enough to be quarantined', async
1538
1537
  const savedDbDir = process.env.DB_DIR;
1539
1538
  process.env.DB_DIR = path.join(tmpRoot, 'db');
1540
1539
  try {
1541
- await quarantineReport('260101T0000', 'bad', 'not usable');
1540
+ await quarantineReport('260101T0000-bad', 'not usable');
1542
1541
  // The file was moved to the unusable-reports directory.
1543
1542
  await fs.access(filePath).then(
1544
1543
  () => {throw new Error('Report should have been quarantined');},
@@ -1573,7 +1572,7 @@ test('quarantineReport leaves the file and logs when the move fails', async () =
1573
1572
  const savedDbDir = process.env.DB_DIR;
1574
1573
  process.env.DB_DIR = path.join(tmpRoot, 'db');
1575
1574
  try {
1576
- await quarantineReport('260101T0000', 'bad', 'not usable');
1575
+ await quarantineReport('260101T0000-bad', 'not usable');
1577
1576
  // The report stays where it was.
1578
1577
  await fs.access(filePath);
1579
1578
  }
@@ -1602,7 +1601,7 @@ test('getReport quarantines a malformed report file old enough to be quarantined
1602
1601
  process.env.DB_DIR = path.join(tmpRoot, 'db');
1603
1602
  try {
1604
1603
  const {getReport} = await import('./util.ts');
1605
- const result = await getReport('260101T0000', 'bad') as any;
1604
+ const result = await getReport('260101T0000-bad') as any;
1606
1605
  assert.ok(result.error);
1607
1606
  // The malformed file was quarantined.
1608
1607
  const quarantined = await fs.readdir(path.join(tmpRoot, 'db', 'unusableReports'));
@@ -1622,7 +1621,7 @@ test('getReport quarantines a malformed report file old enough to be quarantined
1622
1621
  test('getReportExtracts returns extracts of all available reports', async () => {
1623
1622
  const extracts = await getReportExtracts();
1624
1623
  assert.ok(extracts.length >= 8);
1625
- const ids = extracts.map(e => `${e.timeStamp}-${e.jobID}`);
1624
+ const ids = extracts.map(e => e.id);
1626
1625
  assert.ok(ids.includes('260101T0000-mix'));
1627
1626
  assert.ok(ids.includes('260101T0001-ct'));
1628
1627
  });
@@ -1631,7 +1630,7 @@ test('getReportExtracts with onlyLatest returns only the latest report for each
1631
1630
  const latest = await getReportExtracts(true);
1632
1631
  const mixReports = latest.filter(e => e.description === 'Mixed Outcomes');
1633
1632
  assert.equal(mixReports.length, 1);
1634
- assert.equal(mixReports[0]!.timeStamp, '260202T0000');
1633
+ assert.equal(mixReports[0]!.id, '260202T0000-new');
1635
1634
  });
1636
1635
 
1637
1636
  test('getReportExtracts reflects reports added, modified, and removed after a previous call', async () => {
@@ -1918,14 +1917,14 @@ test('getMetrics backfills every category, and generates a since stamp, from a b
1918
1917
  // TESTS FOR orderJob AND awaitJob (GitHub issue #111)
1919
1918
 
1920
1919
  test('orderJob enqueues a new-page order directly into the job queue', async () => {
1921
- const {result, description, url, jobID} = await orderJob(
1920
+ const {result, description, url, reportID} = await orderJob(
1922
1921
  {description: 'Ordered Page', url: 'https://example.com/ordered'}, 'because accessibility matters here'
1923
1922
  ) as any;
1924
1923
  assert.equal(result, 'ok');
1925
1924
  assert.equal(description, 'Ordered Page');
1926
1925
  assert.equal(url, 'https://example.com/ordered');
1927
- assert.match(jobID, /^\d{6}T\d{4}-[a-z0-9]{3}$/);
1928
- const queuedPath = path.join(jobsPath(), 'queue', `${jobID}.json`);
1926
+ assert.match(reportID, /^\d{6}T\d{4}-[a-z0-9]{3}$/);
1927
+ const queuedPath = path.join(jobsPath(), 'queue', `${reportID}.json`);
1929
1928
  try {
1930
1929
  const job = JSON.parse(await fs.readFile(queuedPath, 'utf8'));
1931
1930
  assert.equal(job.target.what, 'Ordered Page');
@@ -2010,7 +2009,7 @@ test('orderJob throws when a job file in a category directory is defective', asy
2010
2009
  });
2011
2010
 
2012
2011
  test('orderJob returns "nonreport" when a retest order cites a report that does not exist', async () => {
2013
- const {result} = await orderJob({timeStamp: '999999T9999', jobID: 'xxx'}, 'because accessibility matters here');
2012
+ const {result} = await orderJob({reportID: '999999T9999-xxx'}, 'because accessibility matters here');
2014
2013
  assert.equal(result, 'nonreport');
2015
2014
  });
2016
2015
 
@@ -2018,19 +2017,19 @@ test('orderJob returns "superseded" when a retest order cites a superseded repor
2018
2017
  // 260101T0000-mix is an earlier report of "Mixed Outcomes" than 260202T0000-new,
2019
2018
  // so it is superseded.
2020
2019
  const {result} = await orderJob(
2021
- {timeStamp: '260101T0000', jobID: 'mix'}, 'because accessibility matters here'
2020
+ {reportID: '260101T0000-mix'}, 'because accessibility matters here'
2022
2021
  );
2023
2022
  assert.equal(result, 'superseded');
2024
2023
  });
2025
2024
 
2026
2025
  test('orderJob enqueues a retest order for the latest report of a page', async () => {
2027
- const {result, description, url, jobID} = await orderJob(
2028
- {timeStamp: '260202T0000', jobID: 'new'}, 'because accessibility matters here'
2026
+ const {result, description, url, reportID} = await orderJob(
2027
+ {reportID: '260202T0000-new'}, 'because accessibility matters here'
2029
2028
  ) as any;
2030
2029
  assert.equal(result, 'ok');
2031
2030
  assert.equal(description, 'Mixed Outcomes');
2032
2031
  assert.equal(url, 'https://example.com/mixed');
2033
- const queuedPath = path.join(jobsPath(), 'queue', `${jobID}.json`);
2032
+ const queuedPath = path.join(jobsPath(), 'queue', `${reportID}.json`);
2034
2033
  try {
2035
2034
  const job = JSON.parse(await fs.readFile(queuedPath, 'utf8'));
2036
2035
  assert.equal(job.target.what, 'Mixed Outcomes');
@@ -2062,12 +2061,12 @@ test('orderJob never returns queueFull when JOB_QUEUE_MAX is 0 (no limit)', asyn
2062
2061
  const fillerPath = path.join(jobsPath(), 'queue', 'fi2.json');
2063
2062
  await fs.writeFile(fillerPath, getJSON({target: {what: 'Filler Queued Page Two', url: 'https://example.com/filler-queued-2'}}));
2064
2063
  try {
2065
- const {result, jobID} = await orderJob(
2064
+ const {result, reportID} = await orderJob(
2066
2065
  {description: 'One More Order Page', url: 'https://example.com/one-more-order'},
2067
2066
  'because accessibility matters here'
2068
2067
  ) as any;
2069
2068
  assert.equal(result, 'ok');
2070
- await fs.unlink(path.join(jobsPath(), 'queue', `${jobID}.json`));
2069
+ await fs.unlink(path.join(jobsPath(), 'queue', `${reportID}.json`));
2071
2070
  }
2072
2071
  finally {
2073
2072
  delete process.env.JOB_QUEUE_MAX;
@@ -2076,12 +2075,12 @@ test('orderJob never returns queueFull when JOB_QUEUE_MAX is 0 (no limit)', asyn
2076
2075
  });
2077
2076
 
2078
2077
  test('awaitJob returns "completed" immediately when a report already exists', async () => {
2079
- const outcome = await awaitJob('260101T0000', 'mix');
2078
+ const outcome = await awaitJob('260101T0000-mix');
2080
2079
  assert.equal(outcome, 'completed');
2081
2080
  });
2082
2081
 
2083
2082
  test('awaitJob returns "notFound" for an identifier naming neither a report nor a pending job', async () => {
2084
- const outcome = await awaitJob('999999T9999', 'xxx');
2083
+ const outcome = await awaitJob('999999T9999-xxx');
2085
2084
  assert.equal(outcome, 'notFound');
2086
2085
  });
2087
2086
 
@@ -2090,7 +2089,7 @@ test('awaitJob returns "failed" when the job has moved to the failed directory',
2090
2089
  await fs.mkdir(path.dirname(failedPath), {recursive: true});
2091
2090
  await fs.writeFile(failedPath, getJSON({target: {what: 'Failed Await Page', url: 'https://example.com/failed-await'}}));
2092
2091
  try {
2093
- const outcome = await awaitJob('260101T1111', 'flj');
2092
+ const outcome = await awaitJob('260101T1111-flj');
2094
2093
  assert.equal(outcome, 'failed');
2095
2094
  }
2096
2095
  finally {
@@ -2111,7 +2110,7 @@ test('awaitJob polls until a queued job completes, then returns "completed"', as
2111
2110
  await fs.writeFile(reportPath, getJSON({id: jobName, target: {what: 'Polled Page', url: 'https://example.com/polled'}}));
2112
2111
  }, 50);
2113
2112
  try {
2114
- const outcome = await awaitJob('260101T2222', 'plj');
2113
+ const outcome = await awaitJob('260101T2222-plj');
2115
2114
  assert.equal(outcome, 'completed');
2116
2115
  }
2117
2116
  finally {
@@ -2129,7 +2128,7 @@ test('awaitJob returns "timedOut" when a claimed job neither completes nor fails
2129
2128
  const claimedPath = path.join(jobsPath(), 'claimed', `${jobName}.json`);
2130
2129
  await fs.writeFile(claimedPath, getJSON({target: {what: 'Stuck Page', url: 'https://example.com/stuck'}}));
2131
2130
  try {
2132
- const outcome = await awaitJob('260101T3333', 'toj');
2131
+ const outcome = await awaitJob('260101T3333-toj');
2133
2132
  assert.equal(outcome, 'timedOut');
2134
2133
  }
2135
2134
  finally {
package/util.ts CHANGED
@@ -89,9 +89,9 @@ export type TestRequestResult
89
89
  = 'url' | 'description' | 'reportExists' | 'duplicate' | 'superseded' | 'nonreport' | 'queueFull' | 'ok';
90
90
 
91
91
  // Target of a new-test or retest request: a description and URL for a new-test request,
92
- // or the timeStamp and jobID of the cited report for a retest request.
92
+ // or the identifier of the cited report for a retest request.
93
93
  export type TestRequestTarget
94
- = {description: string; url: string} | {timeStamp: string; jobID: string};
94
+ = {description: string; url: string} | {reportID: string};
95
95
 
96
96
  // Result of processTestRequest. description and url are absent only when the result
97
97
  // is 'nonreport', i.e. when the target page could not be resolved; this is a
@@ -134,8 +134,7 @@ export type UsableReport = Omit<Report, 'target' | 'catalog' | 'jobData' | 'acts
134
134
 
135
135
  // An extract of an available report.
136
136
  export type ReportExtract = {
137
- timeStamp: string;
138
- jobID: string;
137
+ id: string;
139
138
  description: string;
140
139
  url: string;
141
140
  reportTime: string;
@@ -451,6 +450,13 @@ export const isJobID = (string: string): boolean => {
451
450
  export const isTimeStamp = (string: string): boolean => {
452
451
  return !!getDateString(string);
453
452
  };
453
+ // Returns whether a string is a report identifier: the identifier of the job
454
+ // that produced the report, composed of the job's creation time stamp and its
455
+ // random suffix, joined with a hyphen (example: 260503T0432-x9z).
456
+ export const isReportID = (string: string): boolean => {
457
+ const match = /^(\d{6}T\d{4})-([a-z0-9]{3})$/.exec(string);
458
+ return !!match && isTimeStamp(match[1]!) && isJobID(match[2]!);
459
+ };
454
460
  // Returns whether a string is a URL.
455
461
  export const isURL = (string: string): boolean => {
456
462
  try {
@@ -672,8 +678,8 @@ export const getTestActInstances = (
672
678
  return pairs;
673
679
  };
674
680
  // Returns the path of an available report file.
675
- export const getReportPath = (timeStamp: string, jobID: string): string => path
676
- .join(reportsPath(), `${timeStamp}-${jobID}.json`);
681
+ export const getReportPath = (reportID: string): string => path
682
+ .join(reportsPath(), `${reportID}.json`);
677
683
  // Returns whether a report is usable by Kilotest.
678
684
  export const isUsableReport = (report: unknown): report is UsableReport => {
679
685
  // Cast to any for the property checks inside the guard — this is appropriate
@@ -707,10 +713,9 @@ const quarantineMinAgeMs = () => getEnvMax('REPORT_QUARANTINE_MIN_AGE_MS', 60000
707
713
  // that a defective report is quarantined rather than merely hidden from whichever
708
714
  // listing encountered it. Never throws: a failure here must not displace the error
709
715
  // that getReport is already reporting.
710
- export const quarantineReport = async (timeStamp: string, jobID: string, defect: string): Promise<void> => {
711
- const jobName = `${timeStamp}-${jobID}`;
716
+ export const quarantineReport = async (reportID: string, defect: string): Promise<void> => {
712
717
  // Get the modification time of the report file. A missing file needs no move.
713
- const stat = await fs.stat(getReportPath(timeStamp, jobID)).catch(() => null);
718
+ const stat = await fs.stat(getReportPath(reportID)).catch(() => null);
714
719
  if (!stat) {
715
720
  return;
716
721
  }
@@ -721,26 +726,26 @@ export const quarantineReport = async (timeStamp: string, jobID: string, defect:
721
726
  try {
722
727
  await fs.mkdir(unusableReportsPath(), {recursive: true});
723
728
  await fs.rename(
724
- getReportPath(timeStamp, jobID), path.join(unusableReportsPath(), `${jobName}.json`)
729
+ getReportPath(reportID), path.join(unusableReportsPath(), `${reportID}.json`)
725
730
  );
726
731
  }
727
732
  catch (error: unknown) {
728
733
  // The report stays excluded regardless, so a failed move is only logged.
729
- console.error(`ERROR: Failed to quarantine report ${jobName} (${errorMessage(error)})`);
734
+ console.error(`ERROR: Failed to quarantine report ${reportID} (${errorMessage(error)})`);
730
735
  return;
731
736
  }
732
737
  // Alert a manager, who can repair the report in place or delete it.
733
738
  await sendAlert(
734
739
  'Kilotest: unusable report',
735
- `Report ${jobName} is ${defect}. It was moved to the unusableReports directory, where a manager can repair or delete it.`
740
+ `Report ${reportID} is ${defect}. It was moved to the unusableReports directory, where a manager can repair or delete it.`
736
741
  );
737
742
  };
738
743
  // Returns a report, or an error object if it is missing, unreadable, unparsable, or
739
744
  // unusable. A report that exists but cannot be read or is unusable is defective, so
740
745
  // it is quarantined rather than merely rejected by whichever read encountered it.
741
- export const getReport = async (timeStamp: string, jobID: string): Promise<UsableReport | {error: string}> => {
746
+ export const getReport = async (reportID: string): Promise<UsableReport | {error: string}> => {
742
747
  try {
743
- const reportJSON = await fs.readFile(getReportPath(timeStamp, jobID), 'utf8');
748
+ const reportJSON = await fs.readFile(getReportPath(reportID), 'utf8');
744
749
  const report = JSON.parse(reportJSON);
745
750
  // If it is usable:
746
751
  if (isUsableReport(report)) {
@@ -750,12 +755,12 @@ export const getReport = async (timeStamp: string, jobID: string): Promise<Usabl
750
755
  } catch (error: unknown) {
751
756
  // The file exists but cannot be read or parsed, or is already missing.
752
757
  // quarantineReport ignores a missing file, so no existence check is needed here.
753
- await quarantineReport(timeStamp, jobID, 'unreadable or not JSON');
754
- return {error: `Report ${timeStamp}-${jobID} is missing, unreadable, or not JSON (${errorMessage(error)})`};
758
+ await quarantineReport(reportID, 'unreadable or not JSON');
759
+ return {error: `Report ${reportID} is missing, unreadable, or not JSON (${errorMessage(error)})`};
755
760
  }
756
761
  // The report was read but is not usable, so quarantine it.
757
- await quarantineReport(timeStamp, jobID, 'not usable');
758
- return {error: `Report ${timeStamp}-${jobID} is not usable`};
762
+ await quarantineReport(reportID, 'not usable');
763
+ return {error: `Report ${reportID} is not usable`};
759
764
  };
760
765
  // Returns whether a getReport result is an error (rather than a usable report).
761
766
  // A type predicate is used instead of `if (report.error)` because UsableReport
@@ -804,10 +809,10 @@ export const annotateReportObject = async (report: UsableReport): Promise<void>
804
809
  }
805
810
  };
806
811
  // Returns the size of a report, or null if the report does not exist.
807
- export const getReportStats = async (timeStamp: string, jobID: string) => {
812
+ export const getReportStats = async (reportID: string) => {
808
813
  let reportStat;
809
814
  try {
810
- reportStat = await fs.stat(path.join(reportsPath(), `${timeStamp}-${jobID}.json`));
815
+ reportStat = await fs.stat(path.join(reportsPath(), `${reportID}.json`));
811
816
  }
812
817
  catch {
813
818
  return null;
@@ -825,8 +830,8 @@ export const getReportEndTime = (report: {jobData?: {endTime?: string}}): Date =
825
830
  // Returns an extract of an available report, or an error object if the report cannot
826
831
  // be used. An extract exists to expose a report in listings, so it shares getReport's
827
832
  // usability bar: a report too defective to expose must produce no extract.
828
- export const getReportExtract = async (timeStamp: string, jobID: string): Promise<ReportExtract | {error: string}> => {
829
- const report = await getReport(timeStamp, jobID);
833
+ export const getReportExtract = async (reportID: string): Promise<ReportExtract | {error: string}> => {
834
+ const report = await getReport(reportID);
830
835
  // If the report is not usable:
831
836
  if (isReportError(report)) {
832
837
  // Return the reason.
@@ -837,8 +842,7 @@ export const getReportExtract = async (timeStamp: string, jobID: string): Promis
837
842
  // Return an extract of it. Usability has already guaranteed these properties,
838
843
  // including a valid completion time.
839
844
  return {
840
- timeStamp,
841
- jobID,
845
+ id: reportID,
842
846
  description,
843
847
  url,
844
848
  reportTime: getReportEndTime({jobData}).toISOString()
@@ -884,8 +888,7 @@ export const getReportExtracts = async (onlyLatest: boolean = false): Promise<Re
884
888
  }
885
889
  // Otherwise, get a new extract of it. The stat precedes the read, so a change between
886
890
  // them leaves a stale modification time in the cache and causes a reread next time.
887
- const [timeStamp, jobID] = reportFileName.slice(0, -5).split('-') as [string, string];
888
- const extract = await getReportExtract(timeStamp, jobID);
891
+ const extract = await getReportExtract(reportFileName.slice(0, -5));
889
892
  // If this succeeded:
890
893
  if (!('error' in extract)) {
891
894
  // Cache it and add a copy of it to the array.
@@ -950,7 +953,7 @@ const getApprovedJobProperty = async (
950
953
  }
951
954
  };
952
955
  // Processes a new-test or retest request as a transaction and returns the result.
953
- // For a retest request (target has a timeStamp and jobID), resolves the description
956
+ // For a retest request (target has a reportID), resolves the description
954
957
  // and URL of the cited report itself, so a caller never needs its own
955
958
  // getReportExtract call. Approvability (whether the request may proceed) and, if so,
956
959
  // its addition to testRequests.json are both decided inside the lock, as a single
@@ -995,11 +998,11 @@ export const processTestRequest = (
995
998
  // Whether the cited report (for a retest) has been superseded by a later one.
996
999
  let superseded = false;
997
1000
  // If the target identifies a report to retest:
998
- if ('timeStamp' in target) {
1001
+ if ('reportID' in target) {
999
1002
  requestType = 'retest';
1000
- const {timeStamp, jobID} = target;
1003
+ const {reportID} = target;
1001
1004
  // Get the cited report.
1002
- const extract = await getReportExtract(timeStamp, jobID);
1005
+ const extract = await getReportExtract(reportID);
1003
1006
  // If it does not exist:
1004
1007
  if ('error' in extract) {
1005
1008
  // Return this.
@@ -1009,7 +1012,7 @@ export const processTestRequest = (
1009
1012
  // Get whether it has been superseded by a later report.
1010
1013
  const reportExtracts = await getReportExtracts();
1011
1014
  superseded = reportExtracts.some(
1012
- ex => ex.timeStamp === timeStamp && ex.jobID === jobID && ex.superseded
1015
+ ex => ex.id === reportID && ex.superseded
1013
1016
  );
1014
1017
  }
1015
1018
  // Otherwise, i.e. if the target is a page to test for the first time:
@@ -1099,10 +1102,11 @@ const jobQueueMax = () => getEnvMax('JOB_QUEUE_MAX', 20);
1099
1102
  // 'nonreport' (a retest order citing a report that does not exist), matching
1100
1103
  // ProcessRequestResult's own discriminated-union shape. There is no pending approval
1101
1104
  // inbox to compare against, so 'duplicate' (an identical pending request) never applies.
1102
- // jobID (the enqueued job's own identifier, timeStamp-jobID) is present only on 'ok'.
1105
+ // reportID (the enqueued job's own identifier, and eventually its report's) is
1106
+ // present only on 'ok'.
1103
1107
  export type OrderJobResult
1104
1108
  = {result: 'nonreport'}
1105
- | {result: 'ok'; description: string; url: string; jobID: string}
1109
+ | {result: 'ok'; description: string; url: string; reportID: string}
1106
1110
  | {result: Exclude<TestRequestResult, 'nonreport' | 'duplicate' | 'ok'>; description: string; url: string};
1107
1111
  // Orders a test or retest as a transaction: validates the target is not already queued,
1108
1112
  // claimed, or (for a new-page order) reported, then enqueues a job directly into
@@ -1125,10 +1129,10 @@ export const orderJob = (
1125
1129
  // Whether the cited report (for a retest order) has been superseded by a later one.
1126
1130
  let superseded = false;
1127
1131
  // If the target identifies a report to retest:
1128
- if ('timeStamp' in target) {
1129
- const {timeStamp, jobID} = target;
1132
+ if ('reportID' in target) {
1133
+ const {reportID} = target;
1130
1134
  // Get the cited report.
1131
- const extract = await getReportExtract(timeStamp, jobID);
1135
+ const extract = await getReportExtract(reportID);
1132
1136
  // If it does not exist:
1133
1137
  if ('error' in extract) {
1134
1138
  // Return this.
@@ -1138,7 +1142,7 @@ export const orderJob = (
1138
1142
  // Get whether it has been superseded by a later report.
1139
1143
  const reportExtracts = await getReportExtracts();
1140
1144
  superseded = reportExtracts.some(
1141
- ex => ex.timeStamp === timeStamp && ex.jobID === jobID && ex.superseded
1145
+ ex => ex.id === reportID && ex.superseded
1142
1146
  );
1143
1147
  }
1144
1148
  // Otherwise, i.e. if the target is a page to test for the first time:
@@ -1160,13 +1164,13 @@ export const orderJob = (
1160
1164
  };
1161
1165
  }
1162
1166
  // Otherwise, if the order is to retest a page that has been superseded:
1163
- if ('timeStamp' in target && superseded) {
1167
+ if ('reportID' in target && superseded) {
1164
1168
  // Return this.
1165
1169
  return {result: 'superseded', description, url};
1166
1170
  }
1167
1171
  // Otherwise, if the order is to test a new page and a report already exists about a
1168
1172
  // page with the same description or URL:
1169
- if (!('timeStamp' in target) && await isReportAvailable(description, url)) {
1173
+ if (!('reportID' in target) && await isReportAvailable(description, url)) {
1170
1174
  // Return this.
1171
1175
  return {result: 'reportExists', description, url};
1172
1176
  }
@@ -1194,7 +1198,7 @@ export const orderJob = (
1194
1198
  await fs.writeFile(path.join(jobsPath(), 'queue', `${jobName}.json`), getJSON(job));
1195
1199
  console.log(`Test ordered for ${description} as job ${jobName}`);
1196
1200
  // Return success, with the job identifier so the caller can report it.
1197
- return {result: 'ok', description, url, jobID: jobName};
1201
+ return {result: 'ok', description, url, reportID: jobName};
1198
1202
  // If an error occurred:
1199
1203
  } catch(error) {
1200
1204
  // Throw it.
@@ -1222,14 +1226,14 @@ export type AwaitJobResult = 'completed' | 'failed' | 'timedOut' | 'notFound';
1222
1226
  // polling, if the identifier names neither a completed report nor a job anywhere in the
1223
1227
  // queue/claimed/failed pipeline, since there is then nothing to wait for. No progress
1224
1228
  // notifications are emitted during the wait; this is a pure bounded poll.
1225
- export const awaitJob = async (timeStamp: string, jobID: string): Promise<AwaitJobResult> => {
1226
- const jobFileName = `${timeStamp}-${jobID}.json`;
1229
+ export const awaitJob = async (reportID: string): Promise<AwaitJobResult> => {
1230
+ const jobFileName = `${reportID}.json`;
1227
1231
  const pollMs = awaitJobPollMs();
1228
1232
  const timeoutMs = awaitJobTimeoutMs();
1229
1233
  const deadline = Date.now() + timeoutMs;
1230
1234
  // Returns the job's outcome if it is now determinable, or null if it is still pending.
1231
1235
  const checkOutcome = async (): Promise<AwaitJobResult | null> => {
1232
- if (await getReportStats(timeStamp, jobID)) {
1236
+ if (await getReportStats(reportID)) {
1233
1237
  return 'completed';
1234
1238
  }
1235
1239
  const jobNames = await getJobNames();
@@ -55,12 +55,11 @@ export const answer = async (_: any, search: string, method: string) => {
55
55
  const reportExtracts = await getReportExtracts();
56
56
  // For each report:
57
57
  for (const reportExtract of reportExtracts) {
58
- const {jobID, timeStamp, description, reportTime} = reportExtract;
58
+ const {id, description, reportTime} = reportExtract;
59
59
  // Add the report to the array.
60
60
  reportSpecs.push({
61
61
  description,
62
- timeStamp,
63
- jobID,
62
+ id,
64
63
  reportTime
65
64
  });
66
65
  }
@@ -75,12 +74,12 @@ export const answer = async (_: any, search: string, method: string) => {
75
74
  const margin = ' '.repeat(12);
76
75
  // For each available report:
77
76
  reportSpecs.forEach(spec => {
78
- const {jobID, timeStamp, description, reportTime} = spec;
77
+ const {id, description, reportTime} = spec;
79
78
  const when = getDateTimeString(new Date(reportTime));
80
- const specString = `${description} (job <code>${timeStamp}-${jobID}</code> completed ${when})`;
79
+ const specString = `${description} (job <code>${id}</code> completed ${when})`;
81
80
  // Add a line with a radio button to hide it.
82
81
  lines.push(
83
- `${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
82
+ `${margin}<p><input type="radio" name="report" value="${id}"> ${specString}</p>`
84
83
  );
85
84
  });
86
85
  const query: Record<string, string> = {
@@ -28,14 +28,13 @@ import {issues as issueSpecs} from 'testaro-issues';
28
28
  // Adds parameters to a query for the answer page.
29
29
  const populateQuery = async (
30
30
  issueID: string,
31
- timeStamp: string,
32
- jobID: string,
31
+ reportID: string,
33
32
  catalogIndex: string,
34
33
  pathID: string | null,
35
34
  query: Record<string, any>
36
35
  ) => {
37
36
  // Get the report.
38
- const report = await getReport(timeStamp, jobID);
37
+ const report = await getReport(reportID);
39
38
  // If this failed:
40
39
  if (isReportError(report)) {
41
40
  // Populate the query with the reason.
@@ -113,12 +112,12 @@ const populateQuery = async (
113
112
  };
114
113
  // Returns a page answering the diagnoses question.
115
114
  export const answer = async (pageArgs: string, search: string) => {
116
- const [issueID, timeStamp, jobID, catalogIndex] = pageArgs.split('/') as [string, string, string, string];
115
+ const [issueID, reportID, catalogIndex] = pageArgs.split('/') as [string, string, string];
117
116
  const params = new URLSearchParams(search);
118
117
  const pathID = params.get('pathID');
119
118
  const query: Record<string, any> = {};
120
119
  // Create a query to replace the placeholders.
121
- await populateQuery(issueID, timeStamp, jobID, catalogIndex, pathID, query);
120
+ await populateQuery(issueID, reportID, catalogIndex, pathID, query);
122
121
  // If this failed:
123
122
  if (query.error) {
124
123
  // Return why.