@jrpool/kilotest 58.2.4 → 59.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/getReport.test.ts +1 -1
- package/api/listIssues.test.ts +32 -0
- package/api/listIssues.ts +38 -8
- package/api/listPages.test.ts +92 -0
- package/api/listPages.ts +93 -0
- package/api/orderNewTest.test.ts +2 -2
- package/api/orderNewTest.ts +4 -4
- package/api/orderRetest.test.ts +1 -1
- package/api/requestNewTest.test.ts +1 -1
- package/api/requestNewTest.ts +6 -6
- package/api/requestRetest.test.ts +3 -3
- package/api/requestRetest.ts +2 -2
- package/api/routes.test.ts +1 -1
- package/api/routes.ts +5 -5
- package/api/schemas.ts +24 -7
- package/api/util.ts +26 -45
- package/apiIntegration.test.ts +5 -5
- package/docs/AI-TOOLS.md +1 -1
- package/docs/SERVICE.md +16 -5
- package/index.test.ts +31 -19
- package/index.ts +25 -13
- package/lhm.plugin.json +1433 -141
- package/linkValidation.test.ts +2 -2
- package/llms-full.txt +9 -9
- package/llms.txt +4 -4
- package/mcp.test.ts +9 -9
- package/mcp.ts +14 -14
- package/openapi.yaml +84 -30
- package/package.json +1 -1
- package/routing.test.ts +5 -5
- package/server.json +1 -1
- package/sitemap.xml +1 -1
- package/smokeTest.ts +2 -2
- package/style.css +8 -8
- package/test/fixtures/buildFixtures.ts +3 -3
- package/test/fixtures/db/reports/260101T0000-mix.json +2 -2
- package/test/fixtures/db/reports/260202T0000-new.json +2 -2
- package/util.test.ts +31 -22
- package/util.ts +16 -5
- package/web/enqueueForm/index.html +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +5 -0
- package/web/hideReportForm/index.ts +9 -6
- package/web/home/home.test.ts +19 -0
- package/{index.html → web/home/index.html} +2 -2
- package/web/home/index.ts +20 -0
- package/web/listDiagnoses/listDiagnoses.test.ts +1 -1
- package/web/listIssues/index.html +9 -10
- package/web/listIssues/index.ts +45 -3
- package/web/listIssues/listIssues.test.ts +82 -6
- package/web/{listReports → listPages}/index.html +1 -2
- package/web/listPages/index.ts +99 -0
- package/web/listPages/listPages.test.ts +196 -0
- package/web/listViolators/listViolators.test.ts +1 -1
- package/web/metrics/metrics.test.ts +2 -2
- package/web/requestNewTest/requestNewTest.test.ts +1 -1
- package/web/requestRetest/requestRetest.test.ts +1 -2
- package/web/requestRetestForm/index.ts +4 -2
- package/web/requestRetestForm/requestRetestForm.test.ts +12 -1
- package/web/unhideReportForm/index.ts +9 -6
- package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
- package/web/util.test.ts +87 -23
- package/web/util.ts +27 -31
- package/api/listReports.test.ts +0 -109
- package/api/listReports.ts +0 -87
- package/web/listReports/index.ts +0 -182
- package/web/listReports/listReports.test.ts +0 -300
package/util.test.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
getObject,
|
|
39
39
|
getPlainText,
|
|
40
40
|
getRandomString,
|
|
41
|
+
getReportEndTime,
|
|
41
42
|
getReportExtract,
|
|
42
43
|
getReportExtracts,
|
|
43
44
|
getReportPath,
|
|
@@ -768,14 +769,14 @@ test('processTestRequest returns a duplicate error for a repeated request', {tim
|
|
|
768
769
|
test('processTestRequest returns a reportExists error when a report already exists for the page', async () => {
|
|
769
770
|
// The fixture database already has a report for this description and URL.
|
|
770
771
|
const {result} = await processTestRequest(
|
|
771
|
-
'because accessibility', {description: 'Mixed Outcomes
|
|
772
|
+
'because accessibility', {description: 'Mixed Outcomes', url: 'https://example.com/mixed'}
|
|
772
773
|
);
|
|
773
774
|
assert.equal(result, 'reportExists');
|
|
774
775
|
});
|
|
775
776
|
|
|
776
777
|
test('processTestRequest returns a reportExists error when a report exists about a page with the same description', async () => {
|
|
777
778
|
const {result} = await processTestRequest(
|
|
778
|
-
'because accessibility', {description: 'Mixed Outcomes
|
|
779
|
+
'because accessibility', {description: 'Mixed Outcomes', url: 'https://example.com/drifted'}
|
|
779
780
|
);
|
|
780
781
|
assert.equal(result, 'reportExists');
|
|
781
782
|
});
|
|
@@ -907,7 +908,7 @@ test('annotateReportObject handles a test act with no standardResult', async ()
|
|
|
907
908
|
});
|
|
908
909
|
|
|
909
910
|
test('isReportAvailable returns true for a known page description', async () => {
|
|
910
|
-
const result = await isReportAvailable('Mixed Outcomes
|
|
911
|
+
const result = await isReportAvailable('Mixed Outcomes', 'https://example.com/nonexistent');
|
|
911
912
|
assert.equal(result, true);
|
|
912
913
|
});
|
|
913
914
|
|
|
@@ -1408,10 +1409,18 @@ test('getReportPath returns the path of a report file', () => {
|
|
|
1408
1409
|
assert.ok(result.endsWith('260101T0000-mix.json'));
|
|
1409
1410
|
});
|
|
1410
1411
|
|
|
1411
|
-
test('
|
|
1412
|
+
test('getReportEndTime returns the completion time of a report', () => {
|
|
1413
|
+
const endTime = getReportEndTime({jobData: {endTime: '26-03-04T05:06'}});
|
|
1414
|
+
assert.equal(endTime.toISOString(), '2026-03-04T05:06:00.000Z');
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
test('getReportEndTime returns an invalid date for a report with no completion time', () => {
|
|
1418
|
+
assert.ok(isNaN(getReportEndTime({}).getTime()));
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
test('getReportStats returns reportSize for a valid report', async () => {
|
|
1412
1422
|
const stats = await getReportStats('260101T0000', 'mix');
|
|
1413
1423
|
assert.ok(stats);
|
|
1414
|
-
assert.ok(stats.reportTime instanceof Date);
|
|
1415
1424
|
assert.equal(typeof stats.reportSize, 'number');
|
|
1416
1425
|
assert.ok(stats.reportSize > 0);
|
|
1417
1426
|
});
|
|
@@ -1426,7 +1435,7 @@ test('getReportExtract returns an extract for a valid report', async () => {
|
|
|
1426
1435
|
const extract = await getReportExtract('260101T0000', 'mix') as any;
|
|
1427
1436
|
assert.equal(extract.timeStamp, '260101T0000');
|
|
1428
1437
|
assert.equal(extract.jobID, 'mix');
|
|
1429
|
-
assert.equal(extract.description, 'Mixed Outcomes
|
|
1438
|
+
assert.equal(extract.description, 'Mixed Outcomes');
|
|
1430
1439
|
assert.equal(extract.url, 'https://example.com/mixed');
|
|
1431
1440
|
assert.ok(extract.reportTime);
|
|
1432
1441
|
});
|
|
@@ -1446,7 +1455,7 @@ test('getReportExtracts returns extracts of all available reports', async () =>
|
|
|
1446
1455
|
|
|
1447
1456
|
test('getReportExtracts with onlyLatest returns only the latest report for each page', async () => {
|
|
1448
1457
|
const latest = await getReportExtracts(true);
|
|
1449
|
-
const mixReports = latest.filter(e => e.description === 'Mixed Outcomes
|
|
1458
|
+
const mixReports = latest.filter(e => e.description === 'Mixed Outcomes');
|
|
1450
1459
|
assert.equal(mixReports.length, 1);
|
|
1451
1460
|
assert.equal(mixReports[0]!.timeStamp, '260202T0000');
|
|
1452
1461
|
});
|
|
@@ -1464,7 +1473,7 @@ test('getReportExtracts reflects reports added, modified, and removed after a pr
|
|
|
1464
1473
|
const firstPath = path.join(tmpReportsDir, '260101T0000-aaa.json');
|
|
1465
1474
|
await fs.writeFile(firstPath, JSON.stringify(report));
|
|
1466
1475
|
let extracts = await getReportExtracts();
|
|
1467
|
-
assert.deepEqual(extracts.map(e => e.description), ['Mixed Outcomes
|
|
1476
|
+
assert.deepEqual(extracts.map(e => e.description), ['Mixed Outcomes']);
|
|
1468
1477
|
// Modify the report and give it a later modification time, as reannotation would.
|
|
1469
1478
|
report.target.what = 'Revised Page';
|
|
1470
1479
|
await fs.writeFile(firstPath, JSON.stringify(report));
|
|
@@ -1572,11 +1581,11 @@ test('recordMetric increments an existing count rather than overwriting it', asy
|
|
|
1572
1581
|
process.env.DB_DIR = tmpDir;
|
|
1573
1582
|
try {
|
|
1574
1583
|
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1575
|
-
await recordMetric('mcpToolCalls', '
|
|
1576
|
-
await recordMetric('mcpToolCalls', '
|
|
1584
|
+
await recordMetric('mcpToolCalls', 'listPages');
|
|
1585
|
+
await recordMetric('mcpToolCalls', 'listPages');
|
|
1577
1586
|
await recordMetric('mcpToolCalls', 'getReport');
|
|
1578
1587
|
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1579
|
-
assert.equal(metrics.mcpToolCalls.
|
|
1588
|
+
assert.equal(metrics.mcpToolCalls.listPages, 2);
|
|
1580
1589
|
assert.equal(metrics.mcpToolCalls.getReport, 1);
|
|
1581
1590
|
}
|
|
1582
1591
|
finally {
|
|
@@ -1627,11 +1636,11 @@ test('recordMetric keeps categories independent for the same name', async () =>
|
|
|
1627
1636
|
process.env.DB_DIR = tmpDir;
|
|
1628
1637
|
try {
|
|
1629
1638
|
const {recordMetric, metricsPath} = await import('./util.ts');
|
|
1630
|
-
await recordMetric('apiOperations', '
|
|
1631
|
-
await recordMetric('mcpToolCalls', '
|
|
1639
|
+
await recordMetric('apiOperations', 'listPages');
|
|
1640
|
+
await recordMetric('mcpToolCalls', 'listPages');
|
|
1632
1641
|
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1633
|
-
assert.equal(metrics.apiOperations.
|
|
1634
|
-
assert.equal(metrics.mcpToolCalls.
|
|
1642
|
+
assert.equal(metrics.apiOperations.listPages, 1);
|
|
1643
|
+
assert.equal(metrics.mcpToolCalls.listPages, 1);
|
|
1635
1644
|
}
|
|
1636
1645
|
finally {
|
|
1637
1646
|
process.env.DB_DIR = savedDbDir;
|
|
@@ -1686,7 +1695,7 @@ test('getMetrics backfills categories missing from a metrics.json written before
|
|
|
1686
1695
|
// Simulate a metrics.json written before the managerActivity category was added.
|
|
1687
1696
|
fsSync.writeFileSync(tmpDir + '/metrics.json', JSON.stringify({
|
|
1688
1697
|
since: '260101T0000',
|
|
1689
|
-
pageViews: {
|
|
1698
|
+
pageViews: {listPages: 1},
|
|
1690
1699
|
mcpToolCalls: {},
|
|
1691
1700
|
apiOperations: {}
|
|
1692
1701
|
}));
|
|
@@ -1698,7 +1707,7 @@ test('getMetrics backfills categories missing from a metrics.json written before
|
|
|
1698
1707
|
await recordMetric('managerActivity', 'metrics', 'ok');
|
|
1699
1708
|
const metrics = JSON.parse(fsSync.readFileSync(metricsPath(), 'utf8'));
|
|
1700
1709
|
assert.equal(metrics.since, '260101T0000');
|
|
1701
|
-
assert.equal(metrics.pageViews.
|
|
1710
|
+
assert.equal(metrics.pageViews.listPages, 1);
|
|
1702
1711
|
assert.deepEqual(metrics.managerActivity.metrics, {ok: 1, error: 0});
|
|
1703
1712
|
}
|
|
1704
1713
|
finally {
|
|
@@ -1759,14 +1768,14 @@ test('orderJob enqueues a new-page order directly into the job queue', async ()
|
|
|
1759
1768
|
|
|
1760
1769
|
test('orderJob returns a reportExists error when a report already exists for the ordered page', async () => {
|
|
1761
1770
|
const {result} = await orderJob(
|
|
1762
|
-
{description: 'Mixed Outcomes
|
|
1771
|
+
{description: 'Mixed Outcomes', url: 'https://example.com/mixed'}, 'because accessibility matters here'
|
|
1763
1772
|
);
|
|
1764
1773
|
assert.equal(result, 'reportExists');
|
|
1765
1774
|
});
|
|
1766
1775
|
|
|
1767
1776
|
test('orderJob returns a reportExists error when a report exists about a page with the same description', async () => {
|
|
1768
1777
|
const {result} = await orderJob(
|
|
1769
|
-
{description: 'Mixed Outcomes
|
|
1778
|
+
{description: 'Mixed Outcomes', url: 'https://example.com/drifted'}, 'because accessibility matters here'
|
|
1770
1779
|
);
|
|
1771
1780
|
assert.equal(result, 'reportExists');
|
|
1772
1781
|
});
|
|
@@ -1832,7 +1841,7 @@ test('orderJob returns "nonreport" when a retest order cites a report that does
|
|
|
1832
1841
|
});
|
|
1833
1842
|
|
|
1834
1843
|
test('orderJob returns "superseded" when a retest order cites a superseded report', async () => {
|
|
1835
|
-
// 260101T0000-mix is an earlier report of "Mixed Outcomes
|
|
1844
|
+
// 260101T0000-mix is an earlier report of "Mixed Outcomes" than 260202T0000-new,
|
|
1836
1845
|
// so it is superseded.
|
|
1837
1846
|
const {result} = await orderJob(
|
|
1838
1847
|
{timeStamp: '260101T0000', jobID: 'mix'}, 'because accessibility matters here'
|
|
@@ -1845,12 +1854,12 @@ test('orderJob enqueues a retest order for the latest report of a page', async (
|
|
|
1845
1854
|
{timeStamp: '260202T0000', jobID: 'new'}, 'because accessibility matters here'
|
|
1846
1855
|
) as any;
|
|
1847
1856
|
assert.equal(result, 'ok');
|
|
1848
|
-
assert.equal(description, 'Mixed Outcomes
|
|
1857
|
+
assert.equal(description, 'Mixed Outcomes');
|
|
1849
1858
|
assert.equal(url, 'https://example.com/mixed');
|
|
1850
1859
|
const queuedPath = path.join(jobsPath(), 'queue', `${jobID}.json`);
|
|
1851
1860
|
try {
|
|
1852
1861
|
const job = JSON.parse(await fs.readFile(queuedPath, 'utf8'));
|
|
1853
|
-
assert.equal(job.target.what, 'Mixed Outcomes
|
|
1862
|
+
assert.equal(job.target.what, 'Mixed Outcomes');
|
|
1854
1863
|
}
|
|
1855
1864
|
finally {
|
|
1856
1865
|
await fs.unlink(queuedPath);
|
package/util.ts
CHANGED
|
@@ -689,7 +689,7 @@ export const isUsableReport = (report: unknown): report is UsableReport => {
|
|
|
689
689
|
)
|
|
690
690
|
&& typeof r.jobData === 'object'
|
|
691
691
|
&& r.jobData.endTime
|
|
692
|
-
&& !isNaN(
|
|
692
|
+
&& !isNaN(getReportEndTime(r).getTime())
|
|
693
693
|
&& typeof r.catalog === 'object';
|
|
694
694
|
};
|
|
695
695
|
// Returns a report.
|
|
@@ -754,7 +754,7 @@ export const annotateReportObject = async (report: UsableReport): Promise<void>
|
|
|
754
754
|
);
|
|
755
755
|
}
|
|
756
756
|
};
|
|
757
|
-
// Returns the
|
|
757
|
+
// Returns the size of a report, or null if the report does not exist.
|
|
758
758
|
export const getReportStats = async (timeStamp: string, jobID: string) => {
|
|
759
759
|
let reportStat;
|
|
760
760
|
try {
|
|
@@ -763,10 +763,15 @@ export const getReportStats = async (timeStamp: string, jobID: string) => {
|
|
|
763
763
|
catch {
|
|
764
764
|
return null;
|
|
765
765
|
}
|
|
766
|
-
const reportTime = reportStat.birthtime;
|
|
767
766
|
const reportSize = reportStat.size;
|
|
768
|
-
return {
|
|
767
|
+
return {reportSize};
|
|
769
768
|
};
|
|
769
|
+
// Returns the completion time of a report. This, not the time stamp in the job ID nor the
|
|
770
|
+
// file system time of the report file, is the time of a report. The date is invalid if the
|
|
771
|
+
// report has no valid completion time.
|
|
772
|
+
export const getReportEndTime = (report: {jobData?: {endTime?: string}}): Date => new Date(
|
|
773
|
+
`20${report.jobData?.endTime}Z`
|
|
774
|
+
);
|
|
770
775
|
|
|
771
776
|
// Returns an extract of an available report, or an error object if it cannot be read or parsed.
|
|
772
777
|
export const getReportExtract = async (timeStamp: string, jobID: string): Promise<ReportExtract | {error: string}> => {
|
|
@@ -784,7 +789,7 @@ export const getReportExtract = async (timeStamp: string, jobID: string): Promis
|
|
|
784
789
|
jobID,
|
|
785
790
|
description,
|
|
786
791
|
url,
|
|
787
|
-
reportTime:
|
|
792
|
+
reportTime: getReportEndTime({jobData}).toISOString()
|
|
788
793
|
};
|
|
789
794
|
}
|
|
790
795
|
catch {
|
|
@@ -861,6 +866,12 @@ export const getReportExtracts = async (onlyLatest: boolean = false): Promise<Re
|
|
|
861
866
|
// Return the array, excluding extracts of superseded reports if so specified.
|
|
862
867
|
return onlyLatest ? extracts.filter(extract => !extract.superseded) : extracts;
|
|
863
868
|
};
|
|
869
|
+
// Returns extracts of all available reports about the page with a description, from oldest
|
|
870
|
+
// to latest.
|
|
871
|
+
export const getPageReportExtracts = async (description: string): Promise<ReportExtract[]> => {
|
|
872
|
+
const reportExtracts = await getReportExtracts();
|
|
873
|
+
return reportExtracts.filter(extract => extract.description === description);
|
|
874
|
+
};
|
|
864
875
|
// Returns whether a report with a description or URL is available.
|
|
865
876
|
export const isReportAvailable = async (description: string, url: string): Promise<boolean> => {
|
|
866
877
|
const reportExtracts = await getReportExtracts();
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<main>
|
|
15
15
|
<h1><a href="/">Kilotest</a>: Request approval or rejection</h1>
|
|
16
16
|
<p>To approve a test or retest request, check its requested page name.</p>
|
|
17
|
-
<p>To approve testing a page that has not yet been requested, first <a href="
|
|
17
|
+
<p>To approve testing a page that has not yet been requested, first <a href="listPages.html">request it</a>.</p>
|
|
18
18
|
<p>To reject all requests for a page, check the radio button of its URL.</p>
|
|
19
19
|
<form action="/requestAction.html" method="post">
|
|
20
20
|
<fieldset>
|
|
@@ -53,6 +53,11 @@ test('hideReportForm displays a list of reports on a GET request', async () => {
|
|
|
53
53
|
assert.ok(radios.length > 0);
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
test('hideReportForm describes each report by its completion time', async () => {
|
|
57
|
+
const result: any = await answer(null, '', 'GET');
|
|
58
|
+
assert.ok(result.answerPage.includes('(job <code>260202T0000-new</code> completed 2026-02-02 at 00:10)'));
|
|
59
|
+
});
|
|
60
|
+
|
|
56
61
|
test('hideReportForm ignores a report query string on a GET request', async () => {
|
|
57
62
|
const reportPath = path.join(reportsPath(), '260101T0009-brd.json');
|
|
58
63
|
const hiddenPath = path.join(hiddenReportsPath(), '260101T0009-brd.json');
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
getReportExtracts, hiddenReportsPath, isValidAuthCode, populateTemplate, reportsPath
|
|
10
10
|
} from '../../util.ts';
|
|
11
|
+
import {getDateTimeString} from '../util.ts';
|
|
11
12
|
import fs from 'node:fs/promises';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
|
|
@@ -54,18 +55,19 @@ export const answer = async (_: any, search: string, method: string) => {
|
|
|
54
55
|
const reportExtracts = await getReportExtracts();
|
|
55
56
|
// For each report:
|
|
56
57
|
for (const reportExtract of reportExtracts) {
|
|
57
|
-
const {jobID, timeStamp, description} = reportExtract;
|
|
58
|
+
const {jobID, timeStamp, description, reportTime} = reportExtract;
|
|
58
59
|
// Add the report to the array.
|
|
59
60
|
reportSpecs.push({
|
|
60
61
|
description,
|
|
61
62
|
timeStamp,
|
|
62
|
-
jobID
|
|
63
|
+
jobID,
|
|
64
|
+
reportTime
|
|
63
65
|
});
|
|
64
66
|
}
|
|
65
|
-
// Sort the data by page name and then by time
|
|
67
|
+
// Sort the data by page name and then by completion time.
|
|
66
68
|
reportSpecs.sort((a, b) => {
|
|
67
69
|
if (a.description === b.description) {
|
|
68
|
-
return a.
|
|
70
|
+
return a.reportTime.localeCompare(b.reportTime);
|
|
69
71
|
}
|
|
70
72
|
return a.description.localeCompare(b.description);
|
|
71
73
|
});
|
|
@@ -73,8 +75,9 @@ export const answer = async (_: any, search: string, method: string) => {
|
|
|
73
75
|
const margin = ' '.repeat(12);
|
|
74
76
|
// For each available report:
|
|
75
77
|
reportSpecs.forEach(spec => {
|
|
76
|
-
const {jobID, timeStamp, description} = spec;
|
|
77
|
-
const
|
|
78
|
+
const {jobID, timeStamp, description, reportTime} = spec;
|
|
79
|
+
const when = getDateTimeString(new Date(reportTime));
|
|
80
|
+
const specString = `${description} (job <code>${timeStamp}-${jobID}</code> completed ${when})`;
|
|
78
81
|
// Add a line with a radio button to hide it.
|
|
79
82
|
lines.push(
|
|
80
83
|
`${margin}<p><input type="radio" name="report" value="${timeStamp}-${jobID}"> ${specString}</p>`
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
home.test.ts
|
|
3
|
+
Unit tests for web/home/index.ts, covering the answer export.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import {test} from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import {answer} from './index.ts';
|
|
11
|
+
|
|
12
|
+
// TESTS
|
|
13
|
+
|
|
14
|
+
test('answer returns the home page', async () => {
|
|
15
|
+
const result = await answer();
|
|
16
|
+
assert.equal(result.status, 'ok');
|
|
17
|
+
assert.ok(result.answerPage.includes('<h1>Kilotest</h1>'));
|
|
18
|
+
assert.ok(result.answerPage.includes('application/ld+json'));
|
|
19
|
+
});
|
|
@@ -87,10 +87,10 @@
|
|
|
87
87
|
</details>
|
|
88
88
|
<h2>What do you want to know?</h2>
|
|
89
89
|
<ul class="nav">
|
|
90
|
-
<li><a href="
|
|
90
|
+
<li><a href="listPages.html">Which web pages have been tested?</a></li>
|
|
91
91
|
<li><a href="listTopIssues.html">Which issues have been reported?</a></li>
|
|
92
92
|
<li><a href="manage.html">What can Kilotest managers do?</a></li>
|
|
93
|
-
<li><a href="privacy.html">
|
|
93
|
+
<li><a href="privacy.html">Privacy policy</a></li>
|
|
94
94
|
</ul>
|
|
95
95
|
</main>
|
|
96
96
|
</body>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index.ts
|
|
3
|
+
Serves the home page.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// IMPORTS
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs/promises';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
// FUNCTIONS
|
|
12
|
+
|
|
13
|
+
// Returns the home page.
|
|
14
|
+
export const answer = async () => {
|
|
15
|
+
const answerPage = await fs.readFile(path.join(import.meta.dirname, 'index.html'), 'utf8');
|
|
16
|
+
return {
|
|
17
|
+
status: 'ok',
|
|
18
|
+
answerPage
|
|
19
|
+
};
|
|
20
|
+
};
|
|
@@ -200,7 +200,7 @@ test('listDiagnoses returns an error for an unknown issue with a valid report',
|
|
|
200
200
|
|
|
201
201
|
test('listDiagnoses returns an error when report facts are not obtained', async () => {
|
|
202
202
|
pageDataStringsOverride = {
|
|
203
|
-
what: 'Mixed Outcomes
|
|
203
|
+
what: 'Mixed Outcomes',
|
|
204
204
|
url: 'https://example.com/mixed',
|
|
205
205
|
urlLink: '<a href="https://example.com/mixed">https://example.com/mixed</a>'
|
|
206
206
|
};
|
|
@@ -15,17 +15,9 @@
|
|
|
15
15
|
<h1><a href="/">Kilotest</a>: Issues reported for __target__</h1>
|
|
16
16
|
|
|
17
17
|
<h2>About the tests</h2>
|
|
18
|
-
<details>
|
|
19
|
-
<summary>How to deal with the issues</summary>
|
|
20
|
-
<h3>Do it yourself</h3>
|
|
21
|
-
<p>Explore the issues below in as much detail as you want. Decide which ones are actual defects. Plan and implement corrections.</p>
|
|
22
|
-
<h3>Or use AI</h3>
|
|
23
|
-
<p>Make Kilotest available to your AI assistant. There are <a href="/tutorialAI.html">step-by-step instructions</a> for this.</p>
|
|
24
|
-
<p>After that, you can ask:</p>
|
|
25
|
-
<blockquote>Kilotest reports defects on the __target__ web page. Summarize them and propose a plan to resolve the genuine ones.</blockquote>
|
|
26
|
-
</details>
|
|
27
18
|
__pageFacts__
|
|
28
|
-
<
|
|
19
|
+
<h2>History</h2>
|
|
20
|
+
__history__
|
|
29
21
|
<h2>Issues reported</h2>
|
|
30
22
|
<details>
|
|
31
23
|
<summary>Highest priority: __highestCount__</summary>
|
|
@@ -51,6 +43,13 @@ __pageFacts__
|
|
|
51
43
|
__lowestDetails__
|
|
52
44
|
</ul>
|
|
53
45
|
</details>
|
|
46
|
+
<h2>What you can do</h2>
|
|
47
|
+
<p><strong>Choose how</strong> to address these issues:</p>
|
|
48
|
+
<h3>Do it yourself</h3>
|
|
49
|
+
<p>Explore the issues, following the links above. Decide which issues are actual defects. Plan and implement corrections. Then retest the page.</p>
|
|
50
|
+
<h3>Use artificial intelligence</h3>
|
|
51
|
+
<p><a href="/tutorialAI.html">Make Kilotest available to an AI assistant</a>. Then give it a prompt like this:</p>
|
|
52
|
+
<blockquote><q>Kilotest reports issues on the __target__ web page. Describe them and propose a plan to resolve the genuine defects.</q></blockquote>
|
|
54
53
|
</main>
|
|
55
54
|
</body>
|
|
56
55
|
</html>
|
package/web/listIssues/index.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// IMPORTS
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
getPageReportExtracts,
|
|
9
10
|
getReport,
|
|
10
11
|
getTestActInstances,
|
|
11
12
|
isReportError,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from '../../util.ts';
|
|
15
16
|
import type {UsableReport} from '../../util.ts';
|
|
16
17
|
import {
|
|
18
|
+
getAgoString,
|
|
17
19
|
getIssueFactsLines,
|
|
18
20
|
getPageDataStrings,
|
|
19
21
|
getPageFactsLines,
|
|
@@ -121,6 +123,47 @@ const getIssuesData = (report: UsableReport): ResultsSummary & Record<string, an
|
|
|
121
123
|
// Return the data.
|
|
122
124
|
return final;
|
|
123
125
|
};
|
|
126
|
+
// Returns the lines of the history of the reports about the page of a report.
|
|
127
|
+
const getHistoryLines = async (
|
|
128
|
+
timeStamp: string, jobID: string, description: string, margin: string
|
|
129
|
+
): Promise<string[]> => {
|
|
130
|
+
// Get extracts of all reports about the page, from oldest to latest.
|
|
131
|
+
const pageExtracts = await getPageReportExtracts(description);
|
|
132
|
+
const lastIndex = pageExtracts.length - 1;
|
|
133
|
+
// Initialize the list items and whether the report is the latest about the page.
|
|
134
|
+
const itemLines: string[] = [];
|
|
135
|
+
let isLatest = false;
|
|
136
|
+
// For each report about the page:
|
|
137
|
+
pageExtracts.forEach((extract, index) => {
|
|
138
|
+
const isThis = extract.timeStamp === timeStamp && extract.jobID === jobID;
|
|
139
|
+
isLatest ||= isThis && index === lastIndex;
|
|
140
|
+
// Get the tags that identify its position and whether it is this report.
|
|
141
|
+
const tags = [
|
|
142
|
+
index === 0 ? 'oldest' : '',
|
|
143
|
+
index === lastIndex ? 'latest' : '',
|
|
144
|
+
isThis ? 'this report' : ''
|
|
145
|
+
]
|
|
146
|
+
.filter(tag => tag);
|
|
147
|
+
const tagString = tags.length ? ` (${tags.join('; ')})` : '';
|
|
148
|
+
const label = `${getAgoString(new Date(extract.reportTime))} ago${tagString}`;
|
|
149
|
+
// Add a list item describing it, linked unless it is this report, to the items.
|
|
150
|
+
const href = `/listIssues.html/${extract.timeStamp}/${extract.jobID}`;
|
|
151
|
+
const content = isThis ? label : `<a href="${href}">${label}</a>`;
|
|
152
|
+
itemLines.push(`${margin} <li>${content}</li>`);
|
|
153
|
+
});
|
|
154
|
+
// Initialize the lines with a list of the reports, or a statement that there is only one.
|
|
155
|
+
const lines = itemLines.length > 1
|
|
156
|
+
? [`${margin}<ol>`, ...itemLines, `${margin}</ol>`]
|
|
157
|
+
: [`${margin}<p>This is the only report about the page.</p>`];
|
|
158
|
+
// If the report is the latest about the page:
|
|
159
|
+
if (isLatest) {
|
|
160
|
+
// Add a link to the form for requesting a retest to the lines.
|
|
161
|
+
const href = `/requestRetestForm.html/${timeStamp}/${jobID}`;
|
|
162
|
+
lines.push(`${margin}<p><a href="${href}">Should Kilotest retest the page?</a></p>`);
|
|
163
|
+
}
|
|
164
|
+
// Return the lines.
|
|
165
|
+
return lines;
|
|
166
|
+
};
|
|
124
167
|
// Adds parameters to a query for the answer page.
|
|
125
168
|
const populateQuery = async (timeStamp: string, jobID: string, query: Record<string, any>) => {
|
|
126
169
|
// Get the report.
|
|
@@ -150,10 +193,9 @@ const populateQuery = async (timeStamp: string, jobID: string, query: Record<str
|
|
|
150
193
|
const margin = ' '.repeat(6);
|
|
151
194
|
// Add the page-facts list to the query.
|
|
152
195
|
query.pageFacts = getPageFactsLines(pageInfo, issuesData, margin).join('\n');
|
|
196
|
+
// Add the history of reports about the page to the query.
|
|
197
|
+
query.history = (await getHistoryLines(timeStamp, jobID, description, margin)).join('\n');
|
|
153
198
|
const {issues} = issuesData;
|
|
154
|
-
// Add report data to the query.
|
|
155
|
-
query.timeStamp = timeStamp;
|
|
156
|
-
query.jobID = jobID;
|
|
157
199
|
// Add a summary of the issues to the query.
|
|
158
200
|
query.highestCount = issues[4].length;
|
|
159
201
|
query.highCount = issues[3].length;
|
|
@@ -58,17 +58,17 @@ test('listIssues returns an ok status with valid HTML for the mixed report', asy
|
|
|
58
58
|
|
|
59
59
|
test('listIssues includes the page description in the HTML for the mixed report', async () => {
|
|
60
60
|
const result = await answer('260101T0000/mix');
|
|
61
|
-
assert.ok(result.answerPage.includes('Mixed Outcomes
|
|
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
65
|
const result = await answer('260101T0000/mix');
|
|
66
66
|
const html = parse(result.answerPage);
|
|
67
|
-
const blockQuote = html.querySelector('
|
|
67
|
+
const blockQuote = html.querySelector('blockquote');
|
|
68
68
|
assert.ok(blockQuote);
|
|
69
|
-
assert.ok(
|
|
70
|
-
|
|
71
|
-
);
|
|
69
|
+
assert.ok(blockQuote.textContent.startsWith(
|
|
70
|
+
'Kilotest reports issues on the Mixed Outcomes web page.'
|
|
71
|
+
));
|
|
72
72
|
// No placeholder may remain uninterpolated.
|
|
73
73
|
assert.ok(!blockQuote.textContent.includes('__target__'));
|
|
74
74
|
});
|
|
@@ -153,7 +153,7 @@ test('listIssues returns an error when getPageDataStrings fails after getReport
|
|
|
153
153
|
|
|
154
154
|
test('listIssues returns an error when report facts are not obtained', async () => {
|
|
155
155
|
pageDataStringsOverride = {
|
|
156
|
-
what: 'Mixed Outcomes
|
|
156
|
+
what: 'Mixed Outcomes',
|
|
157
157
|
url: 'https://example.com/mixed',
|
|
158
158
|
urlLink: '<a href="https://example.com/mixed">https://example.com/mixed</a>'
|
|
159
159
|
};
|
|
@@ -205,3 +205,79 @@ test('listIssues handles acts with no standardResult instances', async () => {
|
|
|
205
205
|
await fs.unlink(reportPath).catch(() => {});
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
|
+
|
|
209
|
+
// Returns the History section of a listIssues page, from its heading to the next heading.
|
|
210
|
+
const getHistory = (answerPage: string) => {
|
|
211
|
+
const start = answerPage.indexOf('<h2>History</h2>');
|
|
212
|
+
const end = answerPage.indexOf('<h2>Issues reported</h2>');
|
|
213
|
+
assert.ok(start > -1 && end > start, 'History section exists before the issues');
|
|
214
|
+
return parse(answerPage.slice(start, end));
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Returns the text, and the link destination if any, of each item in a History list.
|
|
218
|
+
const getHistoryItems = (history: ReturnType<typeof parse>) => history
|
|
219
|
+
.querySelectorAll('ol > li')
|
|
220
|
+
.map(li => ({
|
|
221
|
+
text: li.text.trim(),
|
|
222
|
+
href: li.querySelector('a')?.getAttribute('href') ?? null
|
|
223
|
+
}));
|
|
224
|
+
|
|
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');
|
|
227
|
+
const history = getHistory(result.answerPage);
|
|
228
|
+
const items = getHistoryItems(history);
|
|
229
|
+
assert.equal(items.length, 2);
|
|
230
|
+
assert.match(items[0]!.text, /^\d+ days? ago \(oldest; this report\)$/);
|
|
231
|
+
assert.equal(items[0]!.href, null);
|
|
232
|
+
assert.match(items[1]!.text, /^\d+ days? ago \(latest\)$/);
|
|
233
|
+
assert.equal(items[1]!.href, '/listIssues.html/260202T0000/new');
|
|
234
|
+
assert.ok(!result.answerPage.includes('Should Kilotest retest the page?'));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('listIssues History of the latest report ends with the retest question', async () => {
|
|
238
|
+
const result = await answer('260202T0000/new');
|
|
239
|
+
const history = getHistory(result.answerPage);
|
|
240
|
+
const items = getHistoryItems(history);
|
|
241
|
+
assert.equal(items.length, 2);
|
|
242
|
+
assert.match(items[0]!.text, /^\d+ days? ago \(oldest\)$/);
|
|
243
|
+
assert.equal(items[0]!.href, '/listIssues.html/260101T0000/mix');
|
|
244
|
+
assert.match(items[1]!.text, /^\d+ days? ago \(latest; this report\)$/);
|
|
245
|
+
assert.equal(items[1]!.href, null);
|
|
246
|
+
const retestLink = history.querySelector('ol + p > a');
|
|
247
|
+
assert.equal(retestLink?.text, 'Should Kilotest retest the page?');
|
|
248
|
+
assert.equal(retestLink?.getAttribute('href'), '/requestRetestForm.html/260202T0000/new');
|
|
249
|
+
});
|
|
250
|
+
|
|
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');
|
|
253
|
+
const history = getHistory(result.answerPage);
|
|
254
|
+
assert.equal(getHistoryItems(history).length, 0);
|
|
255
|
+
const paragraphs = history.querySelectorAll('p');
|
|
256
|
+
assert.equal(paragraphs[0]?.text, 'This is the only report about the page.');
|
|
257
|
+
assert.equal(paragraphs[1]?.querySelector('a')?.getAttribute('href'), '/requestRetestForm.html/260101T0001/ct');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test('listIssues History tags only the ends of a list of 3 reports and the current report', async () => {
|
|
261
|
+
const fs = await import('node:fs/promises');
|
|
262
|
+
const mixJSON = await fs.readFile(path.join(realUtil.reportsPath(), '260101T0000-mix.json'), 'utf8');
|
|
263
|
+
const report = JSON.parse(mixJSON);
|
|
264
|
+
report.jobData.endTime = '26-01-15T00:10';
|
|
265
|
+
const reportPath = path.join(realUtil.reportsPath(), '260115T0000-mid.json');
|
|
266
|
+
await fs.writeFile(reportPath, JSON.stringify(report));
|
|
267
|
+
try {
|
|
268
|
+
// From the middle report:
|
|
269
|
+
let items = getHistoryItems(getHistory((await answer('260115T0000/mid')).answerPage));
|
|
270
|
+
assert.equal(items.length, 3);
|
|
271
|
+
assert.match(items[0]!.text, /\(oldest\)$/);
|
|
272
|
+
assert.match(items[1]!.text, /^\d+ days? ago \(this report\)$/);
|
|
273
|
+
assert.equal(items[1]!.href, null);
|
|
274
|
+
assert.match(items[2]!.text, /\(latest\)$/);
|
|
275
|
+
// From the latest report:
|
|
276
|
+
items = getHistoryItems(getHistory((await answer('260202T0000/new')).answerPage));
|
|
277
|
+
assert.match(items[1]!.text, /^\d+ days? ago$/);
|
|
278
|
+
assert.equal(items[1]!.href, '/listIssues.html/260115T0000/mid');
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
await fs.unlink(reportPath);
|
|
282
|
+
}
|
|
283
|
+
});
|