@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.
Files changed (66) hide show
  1. package/api/getReport.test.ts +1 -1
  2. package/api/listIssues.test.ts +32 -0
  3. package/api/listIssues.ts +38 -8
  4. package/api/listPages.test.ts +92 -0
  5. package/api/listPages.ts +93 -0
  6. package/api/orderNewTest.test.ts +2 -2
  7. package/api/orderNewTest.ts +4 -4
  8. package/api/orderRetest.test.ts +1 -1
  9. package/api/requestNewTest.test.ts +1 -1
  10. package/api/requestNewTest.ts +6 -6
  11. package/api/requestRetest.test.ts +3 -3
  12. package/api/requestRetest.ts +2 -2
  13. package/api/routes.test.ts +1 -1
  14. package/api/routes.ts +5 -5
  15. package/api/schemas.ts +24 -7
  16. package/api/util.ts +26 -45
  17. package/apiIntegration.test.ts +5 -5
  18. package/docs/AI-TOOLS.md +1 -1
  19. package/docs/SERVICE.md +16 -5
  20. package/index.test.ts +31 -19
  21. package/index.ts +25 -13
  22. package/lhm.plugin.json +1433 -141
  23. package/linkValidation.test.ts +2 -2
  24. package/llms-full.txt +9 -9
  25. package/llms.txt +4 -4
  26. package/mcp.test.ts +9 -9
  27. package/mcp.ts +14 -14
  28. package/openapi.yaml +84 -30
  29. package/package.json +1 -1
  30. package/routing.test.ts +5 -5
  31. package/server.json +1 -1
  32. package/sitemap.xml +1 -1
  33. package/smokeTest.ts +2 -2
  34. package/style.css +8 -8
  35. package/test/fixtures/buildFixtures.ts +3 -3
  36. package/test/fixtures/db/reports/260101T0000-mix.json +2 -2
  37. package/test/fixtures/db/reports/260202T0000-new.json +2 -2
  38. package/util.test.ts +31 -22
  39. package/util.ts +16 -5
  40. package/web/enqueueForm/index.html +1 -1
  41. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  42. package/web/hideReportForm/index.ts +9 -6
  43. package/web/home/home.test.ts +19 -0
  44. package/{index.html → web/home/index.html} +2 -2
  45. package/web/home/index.ts +20 -0
  46. package/web/listDiagnoses/listDiagnoses.test.ts +1 -1
  47. package/web/listIssues/index.html +9 -10
  48. package/web/listIssues/index.ts +45 -3
  49. package/web/listIssues/listIssues.test.ts +82 -6
  50. package/web/{listReports → listPages}/index.html +1 -2
  51. package/web/listPages/index.ts +99 -0
  52. package/web/listPages/listPages.test.ts +196 -0
  53. package/web/listViolators/listViolators.test.ts +1 -1
  54. package/web/metrics/metrics.test.ts +2 -2
  55. package/web/requestNewTest/requestNewTest.test.ts +1 -1
  56. package/web/requestRetest/requestRetest.test.ts +1 -2
  57. package/web/requestRetestForm/index.ts +4 -2
  58. package/web/requestRetestForm/requestRetestForm.test.ts +12 -1
  59. package/web/unhideReportForm/index.ts +9 -6
  60. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  61. package/web/util.test.ts +87 -23
  62. package/web/util.ts +27 -31
  63. package/api/listReports.test.ts +0 -109
  64. package/api/listReports.ts +0 -87
  65. package/web/listReports/index.ts +0 -182
  66. package/web/listReports/listReports.test.ts +0 -300
package/api/util.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  objectSort,
16
16
  ruleEngines
17
17
  } from '../util.ts';
18
- import type {ReportExtract, TestRequestResult} from '../util.ts';
18
+ import type {TestRequestResult} from '../util.ts';
19
19
  import {issues as issueSpecs} from 'testaro-issues';
20
20
 
21
21
  // TYPES
@@ -51,9 +51,9 @@ export const getResponseMetadata = () => ({
51
51
  export const getToolsFacts = () => ({
52
52
  'name': 'Kilotest',
53
53
  'description': {
54
- 'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the issues and the violations retrievable at four levels of granularity.',
54
+ 'what Kilotest does': 'Kilotest tools generate and make available findings about the front-end quality (i.e. accessibility, usability, and standards conformity) of web pages. A Kilotest job generates findings by using Testaro to test a page against about 1300 rules defined by an ensemble of twelve rule engines. Testaro produces a report of the job. The report describes violations of the rules. Kilotest uses Testilo to enhance the report with a classification of the rule violations into about 380 issues. Kilotest makes facts about the tested pages, the issues, and the violations retrievable at four levels of granularity.',
55
55
  'how to retrieve findings': {
56
- 'level 1': 'Use the listReports tool to get a list of available reports.',
56
+ 'level 1': 'Use the listPages tool to get a list of tested pages, each with its latest report. The listIssues output about any report lists all reports about the same page.',
57
57
  'level 2': 'Use the listIssues tool to get a list of issues in one report.',
58
58
  'level 3': 'Use the listViolators tool to get a list of elements on one page that were reported in one report as exhibiting one issue.',
59
59
  'level 4': 'Use the listDiagnoses tool to get a list of diagnoses of how one element on one page exhibited one issue in one report.',
@@ -62,7 +62,7 @@ export const getToolsFacts = () => ({
62
62
  'new testing': 'If no report is available yet about a page, use the requestNewTest tool to request that it be tested.',
63
63
  'retesting': 'If the listIssues tool shows that the latest report about a page is obsolete, because the page has been revised or for another reason, use the requestRetest tool to request that the page be retested.',
64
64
  'latency': 'Requests for testing and retesting are usually approved and fulfilled within one day.',
65
- 'confirmation': 'Use the listReports tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
65
+ 'confirmation': 'Use the listPages tool to determine whether a requested new report exists. There is currently no process for notification of the outcome of requests.'
66
66
  }
67
67
  },
68
68
  'URL': `${getThisHost()}/mcp`,
@@ -131,49 +131,30 @@ export const getRuleEnginesFacts = (ruleEngineIDSet: Iterable<string>) => {
131
131
  objectSort(ruleEnginesFacts, 'name', 'alpha');
132
132
  return ruleEnginesFacts;
133
133
  };
134
- // Returns the basics about a report.
135
- // Accepts an optional precomputed extract to avoid redundant reads when called
136
- // in a loop over all reports (e.g. by listReports). When the extract comes from
137
- // getReportExtracts, it carries a superseded flag; otherwise the flag is computed.
138
- // With an extract provided, failure is impossible, so the return type narrows
139
- // to ReportBasics; without one, an error object may be returned.
140
- // XXX Why are these overloads necessary? Can't we just have one function?
141
- export function getReportBasics(
142
- timeStamp: string, jobID: string, extract: ReportExtract
143
- ): Promise<ReportBasics>;
144
- export function getReportBasics(
145
- timeStamp: string, jobID: string, extract?: ReportExtract | null
146
- ): Promise<ReportBasics | {error: string}>;
147
- export async function getReportBasics(
148
- timeStamp: string, jobID: string, extract: ReportExtract | null = null
149
- ): Promise<ReportBasics | {error: string}> {
150
- const extractProvided = !!extract;
151
- // If an extract was not provided, verify the report exists and read it.
152
- if (!extract) {
153
- // Get the creation time of the report.
154
- const reportStats = await getReportStats(timeStamp, jobID);
155
- // If the report does not exist:
156
- if (!reportStats) {
157
- // Log and return this.
158
- console.error(`Report ${timeStamp}-${jobID} does not exist.`);
159
- return {
160
- error: `Report ${timeStamp}-${jobID} could not be retrieved.`
161
- };
162
- }
163
- // Otherwise, i.e. if it exists, get an extract of the report.
164
- const fetchedExtract = await getReportExtract(timeStamp, jobID);
165
- // If this failed, return why.
166
- if ('error' in fetchedExtract) {
167
- return fetchedExtract;
168
- }
169
- extract = fetchedExtract;
134
+ // Returns the basics about a report, or an error object if the report cannot be read.
135
+ export const getReportBasics = async (
136
+ timeStamp: string, jobID: string
137
+ ): Promise<ReportBasics | {error: string}> => {
138
+ // Verify that the report exists.
139
+ const reportStats = await getReportStats(timeStamp, jobID);
140
+ // If it does not exist:
141
+ if (!reportStats) {
142
+ // Log and return this.
143
+ console.error(`Report ${timeStamp}-${jobID} does not exist.`);
144
+ return {
145
+ error: `Report ${timeStamp}-${jobID} could not be retrieved.`
146
+ };
147
+ }
148
+ // Otherwise, i.e. if it exists, get an extract of the report.
149
+ const extract = await getReportExtract(timeStamp, jobID);
150
+ // If this failed, return why.
151
+ if ('error' in extract) {
152
+ return extract;
170
153
  }
171
154
  const {url, description, reportTime} = extract;
172
155
  // Get whether this report has been superseded.
173
- const isSuperseded = extractProvided
174
- ? extract.superseded === true
175
- : (await getReportExtracts(true))
176
- .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
156
+ const isSuperseded = (await getReportExtracts(true))
157
+ .every(ex => ex.timeStamp !== timeStamp || ex.jobID !== jobID);
177
158
  // Get the basics about the report.
178
159
  const basics = {
179
160
  identifier: `${timeStamp}-${jobID}`,
@@ -187,7 +168,7 @@ export async function getReportBasics(
187
168
  };
188
169
  // Return them.
189
170
  return basics;
190
- }
171
+ };
191
172
  // Returns the specification of an issue.
192
173
  export const getIssueSpec = (issueID: string) => {
193
174
  // Get the issue specification.
@@ -93,11 +93,11 @@ const request = (method: string, requestPath: string, body: any = null): Promise
93
93
 
94
94
  // TESTS
95
95
 
96
- test('GET /api/listReports returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
97
- const {body} = await request('GET', '/api/listReports');
98
- assert.equal(body['tool name'], 'listReports');
99
- const reports = body['response content']['basics about all available reports'];
100
- assert.equal(reports.length, 8);
96
+ test('GET /api/listPages returns valid JSON through the HTTP routing layer', {timeout: 500}, async () => {
97
+ const {body} = await request('GET', '/api/listPages');
98
+ assert.equal(body['tool name'], 'listPages');
99
+ const pages = body['response content']['basics about all tested pages'];
100
+ assert.equal(pages.length, 7);
101
101
  });
102
102
 
103
103
  test('POST /api/requestFeature parses a JSON body and returns a JSON response through the HTTP routing layer', async () => {
package/docs/AI-TOOLS.md CHANGED
@@ -21,7 +21,7 @@ The internal features that make Kilotest a collection of tools for language mode
21
21
 
22
22
  - An API, with specific functional and utility modules in the `api` directory.
23
23
  - Tests of the functionalities of the API.
24
- - A [`JSON-LD`](https://json-ld.org/) script in the `index.html` file, providing structured data about the Kilotest API.
24
+ - A [`JSON-LD`](https://json-ld.org/) script in the `web/home/index.html` file, providing structured data about the Kilotest API.
25
25
  - Environment variables in the `env.example` file.
26
26
  - An `llms.txt` file and an `llms-full.txt` file, documenting the use of Kilotest by language models, conforming to the [llms-txt](https://llmstxt.org/) specification.
27
27
  - A generated `openapi.yaml` file, documenting the Kilotest API, conforming to the [OpenAPI specification](https://spec.openapis.org/oas/v3.1.0).
package/docs/SERVICE.md CHANGED
@@ -323,7 +323,19 @@ Both logs are rotated; see “PM2 log rotation” above for the application log
323
323
 
324
324
  ## Version management
325
325
 
326
- When a new version of the `kilotest` package has been published, the service can be updated as follows:
326
+ When the Kilotest web, API, or MCP interface is revised in a feature branch:
327
+
328
+ 1. Update the version in `package.json`
329
+ 1. Update the version in `server.json`
330
+ 1. Update the version and any changes in the API in `lhm.plugin.json`
331
+ 1. If the API has changed, run the `generate:openapi` script
332
+ 1. Execute the command `npx @lobehub/market-cli plugin update --dir .`
333
+ 1. Commit and push the changes to the feature branch
334
+ 1. Create a pull request to merge the feature branch into `main`
335
+ 1. Once the checks succeed, squash and merge the branch into `main`
336
+ 1. Execute `npm publish`
337
+
338
+ When a new version of the `kilotest` package has been published, update the service as follows:
327
339
 
328
340
  1. Connect to the server: `ssh linuxuser@kilotest.com`
329
341
  1. Navigate to the package root: `cd /opt/jpdev/kilotest`
@@ -331,7 +343,6 @@ When a new version of the `kilotest` package has been published, the service can
331
343
  1. Delete the record of that discard: `git stash drop`
332
344
  1. Fetch and merge the new version: `git pull`
333
345
  1. Update the dependencies: `npm update`
334
- 1. Update the Playwright browsers: `npx playwright install`
335
346
  1. Restart the service: `pm2 restart kilotest`
336
347
 
337
348
  ## Branch protection
@@ -498,9 +509,9 @@ Kilotest records basic counts of how it is used, so the maintainer can answer qu
498
509
 
499
510
  `recordMetric(category, name)`, in `util.ts`, increments a count for a `(category, name)` pair and writes the result to `db/metrics.json`. It is called from three places:
500
511
 
501
- - The generic `.html` GET dispatch in `index.ts`, once per successfully served web page, recorded under the `pageViews` category by page name (for example `tutorialWeb`, `listReports`), unless the page is a manager-only page (see "Manager page activity" below), in which case it is recorded there instead.
512
+ - The generic `.html` GET dispatch in `index.ts`, once per successfully served web page, recorded under the `pageViews` category by page name (for example `tutorialWeb`, `listPages`), unless the page is a manager-only page (see "Manager page activity" below), in which case it is recorded there instead.
502
513
  - The home page (`/` and `/index.html`), served by its own branch in `index.ts` rather than through the generic `.html` dispatch, recorded under `pageViews` as `index`.
503
- - Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listReports`, `requestNewTest`).
514
+ - Each of the 9 MCP tool handlers in `mcp.ts`, once per successful tool call, recorded under the `mcpToolCalls` category by tool name (for example `listPages`, `requestNewTest`).
504
515
  - The `/api/*` GET and POST service branches in `index.ts`, once per call, recorded under the `apiOperations` category by operation name.
505
516
 
506
517
  `db/metrics.json` also stores a `since` time stamp, set when the file is first created, so the counts can be read as "since this date" rather than assumed to cover Kilotest's entire history. `getMetrics()` backfills any category absent from an existing `db/metrics.json` (for example one written before a category such as `managerActivity` existed), so an older file remains readable rather than causing every subsequent request to error.
@@ -513,7 +524,7 @@ This exclusion exists because manager pages reflect the maintainer operating Kil
513
524
 
514
525
  #### Excluding the maintainer's own manual testing
515
526
 
516
- The manager-page exclusion above only keeps the maintainer's use of manager-only pages out of `pageViews`; it does not, by itself, keep the maintainer's manual testing of *ordinary* pages (for example clicking through `tutorialWeb.html` or `listReports.html` after a deployment) from being counted as real usage. A separate mechanism addresses that: submitting a valid `authCode` on `/metrics.html` sets a cookie (`kilotestExclude`), valid for 30 days, whose value is a SHA-256 hash of `AUTH_CODE` rather than the code itself, so the secret is never placed in a long-lived browser cookie. Every subsequent request from that browser that carries a matching cookie is excluded from `pageViews` and `apiOperations` (not from `managerActivity`, which should keep counting regardless, since it exists to surface suspected abuse, including from the maintainer's own browser if its cookie were ever compromised).
527
+ The manager-page exclusion above only keeps the maintainer's use of manager-only pages out of `pageViews`; it does not, by itself, keep the maintainer's manual testing of *ordinary* pages (for example clicking through `tutorialWeb.html` or `listPages.html` after a deployment) from being counted as real usage. A separate mechanism addresses that: submitting a valid `authCode` on `/metrics.html` sets a cookie (`kilotestExclude`), valid for 30 days, whose value is a SHA-256 hash of `AUTH_CODE` rather than the code itself, so the secret is never placed in a long-lived browser cookie. Every subsequent request from that browser that carries a matching cookie is excluded from `pageViews` and `apiOperations` (not from `managerActivity`, which should keep counting regardless, since it exists to surface suspected abuse, including from the maintainer's own browser if its cookie were ever compromised).
517
528
 
518
529
  A fixed or guessable cookie value was deliberately avoided: Kilotest's source is public, so anyone could otherwise read it and set the same cookie in their own browser to exclude themselves from metrics at no cost, defeating the feature. Hashing `AUTH_CODE` means only someone who has already proven they know the real code can derive the correct cookie value.
519
530
 
package/index.test.ts CHANGED
@@ -198,7 +198,7 @@ test('routes has GET and POST arrays', () => {
198
198
  // TESTS: smoke-test short-circuit
199
199
 
200
200
  test('GET with x-kilotest-smoke header returns a perfunctory 200', async () => {
201
- const res = await request('GET', '/api/listReports', null, {'x-kilotest-smoke': '1'});
201
+ const res = await request('GET', '/api/listPages', null, {'x-kilotest-smoke': '1'});
202
202
  assert.equal(res.statusCode, 200);
203
203
  assert.equal(res.body, '{}');
204
204
  });
@@ -210,7 +210,7 @@ test('POST with x-kilotest-smoke header returns a perfunctory 200', async () =>
210
210
  });
211
211
 
212
212
  test('GET without x-kilotest-smoke header executes the handler', async () => {
213
- const res = await request('GET', '/api/listReports');
213
+ const res = await request('GET', '/api/listPages');
214
214
  assert.equal(res.statusCode, 200);
215
215
  assert.notEqual(res.body, '{}');
216
216
  });
@@ -332,13 +332,13 @@ test('GET /favicon.ico serves the favicon as an icon', async () => {
332
332
  assert.ok(res.headers['content-type'].includes('image/x-icon'));
333
333
  });
334
334
 
335
- test('GET /api/listReports returns JSON with report data and records an API-operation metric', async () => {
336
- const countBefore = await getMetricCount('apiOperations', 'listReports');
337
- const res = await request('GET', '/api/listReports');
335
+ test('GET /api/listPages returns JSON with report data and records an API-operation metric', async () => {
336
+ const countBefore = await getMetricCount('apiOperations', 'listPages');
337
+ const res = await request('GET', '/api/listPages');
338
338
  assert.equal(res.statusCode, 200);
339
339
  const body = jsonBody(res);
340
- assert.equal(body['tool name'], 'listReports');
341
- assert.equal(await getMetricCount('apiOperations', 'listReports'), countBefore + 1);
340
+ assert.equal(body['tool name'], 'listPages');
341
+ assert.equal(await getMetricCount('apiOperations', 'listPages'), countBefore + 1);
342
342
  });
343
343
 
344
344
  test('GET /api/listIssues/260101T0000/mix returns JSON with issue data', async () => {
@@ -471,7 +471,7 @@ test('POST /requestNewTest.html with valid data returns an HTML page', async ()
471
471
 
472
472
  test('POST /requestNewTest.html with an already-tested URL returns an error', async () => {
473
473
  const res = await formRequest('POST', '/requestNewTest.html', {
474
- description: 'Mixed Outcomes Page',
474
+ description: 'Mixed Outcomes',
475
475
  url: 'https://example.com/mixed',
476
476
  why: 'Because accessibility matters'
477
477
  });
@@ -688,14 +688,14 @@ test('PUT / returns an invalid method error', async () => {
688
688
 
689
689
  // TESTS: requestHandler catch boundary
690
690
 
691
- test('GET /listReports.html returns a 500 error when an unexpected internal error occurs', {timeout: 500}, async () => {
691
+ test('GET /listPages.html returns a 500 error when an unexpected internal error occurs', {timeout: 500}, async () => {
692
692
  // Corrupt testRequests.json so that getTestRequests throws, an error that should never occur in normal
693
693
  // operation and so is not handled by handleRequest itself, only by requestHandler's
694
694
  // outer catch boundary.
695
695
  const backup = await fs.readFile(testRequestsPath, 'utf8');
696
696
  await fs.writeFile(testRequestsPath, 'not valid json');
697
697
  try {
698
- const res = await request('GET', '/listReports.html');
698
+ const res = await request('GET', '/listPages.html');
699
699
  assert.equal(res.statusCode, 500);
700
700
  assert.ok(res.headers['content-type'].includes('application/json'));
701
701
  const body = jsonBody(res);
@@ -932,10 +932,22 @@ test('GET /rules.html/<segment> redirects to the equivalent /listRules.html/<seg
932
932
  assert.equal(res.headers['location'], '/listRules.html/allCaps');
933
933
  });
934
934
 
935
- test('GET /targets.html redirects to /listReports.html', async () => {
935
+ test('GET /targets.html redirects to /listPages.html', async () => {
936
936
  const res = await request('GET', '/targets.html', {}, {followRedirects: false});
937
937
  assert.equal(res.statusCode, 301);
938
- assert.equal(res.headers['location'], '/listReports.html');
938
+ assert.equal(res.headers['location'], '/listPages.html');
939
+ });
940
+
941
+ test('GET /listReports.html redirects to /listPages.html', async () => {
942
+ const res = await request('GET', '/listReports.html', {}, {followRedirects: false});
943
+ assert.equal(res.statusCode, 301);
944
+ assert.equal(res.headers['location'], '/listPages.html');
945
+ });
946
+
947
+ test('GET /api/listReports redirects to /api/listPages', async () => {
948
+ const res = await request('GET', '/api/listReports', {}, {followRedirects: false});
949
+ assert.equal(res.statusCode, 301);
950
+ assert.equal(res.headers['location'], '/api/listPages');
939
951
  });
940
952
 
941
953
  test('GET /requestTest.html redirects to /requestNewTest.html', async () => {
@@ -1463,7 +1475,7 @@ test('GET /requestRetest.html/260202T0000/new is rejected as an invalid GET requ
1463
1475
  // tested by the unit tests in web/*/*.test.cjs.
1464
1476
 
1465
1477
  const htmlPagePaths = [
1466
- '/listReports.html',
1478
+ '/listPages.html',
1467
1479
  '/listViolators.html/linkNoText/260101T0000/mix',
1468
1480
  '/listDiagnoses.html/linkNoText/260101T0000/mix/0',
1469
1481
  '/enqueueForm.html',
@@ -1520,11 +1532,11 @@ test('a request carrying the metrics-exclusion cookie does not record a pageView
1520
1532
  const {getExclusionCookieValue} = await import('./util.ts');
1521
1533
  const cookieHeader = `kilotestExclude=${getExclusionCookieValue()}`;
1522
1534
  const pageViewCountBefore = await getMetricCount('pageViews', 'tutorialWeb');
1523
- const apiOperationCountBefore = await getMetricCount('apiOperations', 'listReports');
1535
+ const apiOperationCountBefore = await getMetricCount('apiOperations', 'listPages');
1524
1536
  await request('GET', '/tutorialWeb.html', null, {cookie: cookieHeader});
1525
- await request('GET', '/api/listReports', null, {cookie: cookieHeader});
1537
+ await request('GET', '/api/listPages', null, {cookie: cookieHeader});
1526
1538
  assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), pageViewCountBefore);
1527
- assert.equal(await getMetricCount('apiOperations', 'listReports'), apiOperationCountBefore);
1539
+ assert.equal(await getMetricCount('apiOperations', 'listPages'), apiOperationCountBefore);
1528
1540
  });
1529
1541
 
1530
1542
  test('a request carrying an invalid exclusion cookie still records metrics normally', async () => {
@@ -1945,7 +1957,7 @@ test('a request logs a single-line JSON record with the expected fields', async
1945
1957
  const originalLog = console.log;
1946
1958
  console.log = (line: string) => logged.push(line);
1947
1959
  try {
1948
- await request('GET', '/listReports.html');
1960
+ await request('GET', '/listPages.html');
1949
1961
  }
1950
1962
  finally {
1951
1963
  console.log = originalLog;
@@ -1961,7 +1973,7 @@ test('a request logs a single-line JSON record with the expected fields', async
1961
1973
  assert.equal(requestLines.length, 1);
1962
1974
  const record = JSON.parse(requestLines[0]!);
1963
1975
  assert.equal(record.method, 'GET');
1964
- assert.equal(record.path, '/listReports.html');
1976
+ assert.equal(record.path, '/listPages.html');
1965
1977
  assert.equal(record.status, 200);
1966
1978
  assert.ok(record.ip);
1967
1979
  assert.ok(record.userAgent);
@@ -1991,7 +2003,7 @@ test('a smoke-test-flagged request is not logged as a general request', async ()
1991
2003
  const originalLog = console.log;
1992
2004
  console.log = (line: string) => logged.push(line);
1993
2005
  try {
1994
- await request('GET', '/listReports.html', null, {'x-kilotest-smoke': '1'});
2006
+ await request('GET', '/listPages.html', null, {'x-kilotest-smoke': '1'});
1995
2007
  }
1996
2008
  finally {
1997
2009
  console.log = originalLog;
package/index.ts CHANGED
@@ -40,6 +40,7 @@ import {handleMCP, mcpPath} from './mcp.ts';
40
40
  import fs from 'node:fs/promises';
41
41
  import {handleComment as handleTutorialWebComment} from './web/tutorialWeb/index.ts';
42
42
  import {answer as tutorialWeb} from './web/tutorialWeb/index.ts';
43
+ import {answer as home} from './web/home/index.ts';
43
44
  import {answer as privacy} from './web/privacy/index.ts';
44
45
  import {answer as tutorialAI, handleComment as handleTutorialAIComment} from './web/tutorialAI/index.ts';
45
46
  import http, {type IncomingMessage, type ServerResponse} from 'node:http';
@@ -54,7 +55,7 @@ import {answer as expungeReportsForm} from './web/expungeReportsForm/index.ts';
54
55
  import {answer as hideReportForm} from './web/hideReportForm/index.ts';
55
56
  import {answer as listDiagnosesPage} from './web/listDiagnoses/index.ts';
56
57
  import {answer as listIssuesPage} from './web/listIssues/index.ts';
57
- import {answer as listReportsPage} from './web/listReports/index.ts';
58
+ import {answer as listPagesPage} from './web/listPages/index.ts';
58
59
  import {answer as listRules} from './web/listRules/index.ts';
59
60
  import {answer as listTopIssues} from './web/listTopIssues/index.ts';
60
61
  import {answer as listViolatorsPage} from './web/listViolators/index.ts';
@@ -75,7 +76,7 @@ import {answer as unhideReportForm} from './web/unhideReportForm/index.ts';
75
76
  import {response as getReportAPI} from './api/getReport.ts';
76
77
  import {response as listDiagnosesAPI} from './api/listDiagnoses.ts';
77
78
  import {response as listIssuesAPI} from './api/listIssues.ts';
78
- import {response as listReportsAPI} from './api/listReports.ts';
79
+ import {response as listPagesAPI} from './api/listPages.ts';
79
80
  import {response as listViolatorsAPI} from './api/listViolators.ts';
80
81
  import {response as requestFeatureAPI} from './api/requestFeature.ts';
81
82
  import {response as requestRetestAPI} from './api/requestRetest.ts';
@@ -112,7 +113,7 @@ const answer: {
112
113
  hideReportForm: PageHandler;
113
114
  listDiagnoses: PageHandler;
114
115
  listIssues: PageHandler;
115
- listReports: PageHandler;
116
+ listPages: PageHandler;
116
117
  listRules: PageHandler;
117
118
  listTopIssues: PageHandler;
118
119
  listViolators: PageHandler;
@@ -143,7 +144,7 @@ const answer: {
143
144
  hideReportForm,
144
145
  listDiagnoses: listDiagnosesPage,
145
146
  listIssues: listIssuesPage,
146
- listReports: listReportsPage,
147
+ listPages: listPagesPage,
147
148
  listRules,
148
149
  listTopIssues,
149
150
  listViolators: listViolatorsPage,
@@ -171,7 +172,7 @@ const apiRespond: {
171
172
  getReport: ApiResponder;
172
173
  listDiagnoses: ApiResponder;
173
174
  listIssues: ApiResponder;
174
- listReports: ApiResponder;
175
+ listPages: ApiResponder;
175
176
  listViolators: ApiResponder;
176
177
  requestFeature: ApiResponder;
177
178
  requestRetest: ApiResponder;
@@ -183,7 +184,7 @@ const apiRespond: {
183
184
  getReport: getReportAPI,
184
185
  listDiagnoses: listDiagnosesAPI,
185
186
  listIssues: listIssuesAPI,
186
- listReports: listReportsAPI,
187
+ listPages: listPagesAPI,
187
188
  listViolators: listViolatorsAPI,
188
189
  requestFeature: requestFeatureAPI,
189
190
  requestRetest: requestRetestAPI,
@@ -291,12 +292,16 @@ const noDirectGetPages = new Set(['unhideReportForm']);
291
292
  // on 2026-09-28 (see the newTest/retest naming-ambiguity fix), to distinguish the
292
293
  // new-test-only page from bare "test" language used elsewhere.
293
294
  // TEMPORARY: retire this redirect after 2027-03-28 once crawlers have re-indexed.
295
+ // listReports was renamed to listPages on 2026-10-09, since it lists tested pages, each
296
+ // linked to its latest report.
297
+ // TEMPORARY: retire this redirect after 2027-04-09 once crawlers have re-indexed.
294
298
  const renamedPagePrefixes: Record<string, string> = {
295
299
  diagnoses: 'listDiagnoses',
296
300
  reportIssue: 'listViolators',
297
301
  reportIssues: 'listIssues',
298
302
  rules: 'listRules',
299
- targets: 'listReports',
303
+ targets: 'listPages',
304
+ listReports: 'listPages',
300
305
  requestTest: 'requestNewTest',
301
306
  requestTestForm: 'requestNewTestForm'
302
307
  };
@@ -638,10 +643,10 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
638
643
  else if (['/', '/index.html'].includes(pathname)) {
639
644
  await recordPageMetric('pageViews', 'index');
640
645
  // Get the home page.
641
- const homePage = await fs.readFile('index.html', 'utf8');
646
+ const {answerPage} = await home();
642
647
  // Serve it.
643
648
  setHeaders('text/html', '/index.html', 'medium');
644
- response.end(homePage);
649
+ response.end(answerPage);
645
650
  }
646
651
  // Otherwise, if it is for the crawler specification:
647
652
  else if (pageName === 'robots.txt') {
@@ -800,15 +805,22 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
800
805
  // Otherwise, if it is for an API service:
801
806
  else if (pageName === 'api') {
802
807
  const [service, ...specs] = pathTail.split('/');
803
- // If the service lists the available reports:
804
- if (service === 'listReports') {
808
+ // If the service lists the tested pages:
809
+ if (service === 'listPages') {
805
810
  // Get the response body.
806
- const responseBody = await apiRespond.listReports([]);
807
- await recordPageMetric('apiOperations', 'listReports');
811
+ const responseBody = await apiRespond.listPages([]);
812
+ await recordPageMetric('apiOperations', 'listPages');
808
813
  // Send it.
809
814
  setHeaders('application/json', null, 'ultra');
810
815
  response.end(JSON.stringify(responseBody));
811
816
  }
817
+ // Otherwise, if the service has the name that listPages had before 2026-10-09:
818
+ // TEMPORARY: retire this redirect after 2027-04-09.
819
+ else if (service === 'listReports') {
820
+ // Redirect the request to listPages.
821
+ response.writeHead(301, {Location: '/api/listPages'});
822
+ response.end();
823
+ }
812
824
  // Otherwise, if the service lists the issues in a report:
813
825
  else if (service === 'listIssues') {
814
826
  // Get the response body.