@jrpool/kilotest 58.2.0 → 59.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/api/listIssues.test.ts +32 -0
  2. package/api/listIssues.ts +38 -8
  3. package/api/listPages.test.ts +92 -0
  4. package/api/listPages.ts +93 -0
  5. package/api/orderNewTest.test.ts +1 -1
  6. package/api/orderNewTest.ts +4 -4
  7. package/api/requestNewTest.ts +6 -6
  8. package/api/requestRetest.ts +2 -2
  9. package/api/routes.test.ts +1 -1
  10. package/api/routes.ts +5 -5
  11. package/api/schemas.ts +24 -7
  12. package/api/util.ts +26 -45
  13. package/apiIntegration.test.ts +5 -5
  14. package/docs/SERVICE.md +16 -5
  15. package/index.html +1 -1
  16. package/index.test.ts +30 -18
  17. package/index.ts +22 -11
  18. package/lhm.plugin.json +1433 -141
  19. package/linkValidation.test.ts +2 -2
  20. package/llms-full.txt +9 -9
  21. package/llms.txt +4 -4
  22. package/mcp.test.ts +8 -8
  23. package/mcp.ts +14 -14
  24. package/openapi.yaml +84 -30
  25. package/package.json +1 -1
  26. package/routing.test.ts +5 -5
  27. package/server.json +1 -1
  28. package/sitemap.xml +1 -1
  29. package/smokeTest.ts +2 -2
  30. package/style.css +8 -8
  31. package/test/fixtures/buildFixtures.ts +1 -1
  32. package/util.test.ts +71 -12
  33. package/util.ts +61 -12
  34. package/web/enqueueForm/index.html +1 -1
  35. package/web/hideReportForm/hideReportForm.test.ts +5 -0
  36. package/web/hideReportForm/index.ts +9 -6
  37. package/web/listIssues/index.html +2 -1
  38. package/web/listIssues/index.ts +45 -3
  39. package/web/listIssues/listIssues.test.ts +76 -0
  40. package/web/{listReports → listPages}/index.html +1 -2
  41. package/web/listPages/index.ts +99 -0
  42. package/web/listPages/listPages.test.ts +196 -0
  43. package/web/metrics/metrics.test.ts +2 -2
  44. package/web/requestRetestForm/index.ts +4 -2
  45. package/web/requestRetestForm/requestRetestForm.test.ts +11 -0
  46. package/web/unhideReportForm/index.ts +9 -6
  47. package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
  48. package/web/util.test.ts +85 -11
  49. package/web/util.ts +26 -28
  50. package/api/listReports.test.ts +0 -109
  51. package/api/listReports.ts +0 -87
  52. package/web/listReports/index.ts +0 -182
  53. package/web/listReports/listReports.test.ts +0 -300
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.html CHANGED
@@ -87,7 +87,7 @@
87
87
  </details>
88
88
  <h2>What do you want to know?</h2>
89
89
  <ul class="nav">
90
- <li><a href="listReports.html">Which web pages have been tested?</a></li>
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
93
  <li><a href="privacy.html">What is the privacy policy?</a></li>
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 () => {
@@ -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
@@ -54,7 +54,7 @@ import {answer as expungeReportsForm} from './web/expungeReportsForm/index.ts';
54
54
  import {answer as hideReportForm} from './web/hideReportForm/index.ts';
55
55
  import {answer as listDiagnosesPage} from './web/listDiagnoses/index.ts';
56
56
  import {answer as listIssuesPage} from './web/listIssues/index.ts';
57
- import {answer as listReportsPage} from './web/listReports/index.ts';
57
+ import {answer as listPagesPage} from './web/listPages/index.ts';
58
58
  import {answer as listRules} from './web/listRules/index.ts';
59
59
  import {answer as listTopIssues} from './web/listTopIssues/index.ts';
60
60
  import {answer as listViolatorsPage} from './web/listViolators/index.ts';
@@ -75,7 +75,7 @@ import {answer as unhideReportForm} from './web/unhideReportForm/index.ts';
75
75
  import {response as getReportAPI} from './api/getReport.ts';
76
76
  import {response as listDiagnosesAPI} from './api/listDiagnoses.ts';
77
77
  import {response as listIssuesAPI} from './api/listIssues.ts';
78
- import {response as listReportsAPI} from './api/listReports.ts';
78
+ import {response as listPagesAPI} from './api/listPages.ts';
79
79
  import {response as listViolatorsAPI} from './api/listViolators.ts';
80
80
  import {response as requestFeatureAPI} from './api/requestFeature.ts';
81
81
  import {response as requestRetestAPI} from './api/requestRetest.ts';
@@ -112,7 +112,7 @@ const answer: {
112
112
  hideReportForm: PageHandler;
113
113
  listDiagnoses: PageHandler;
114
114
  listIssues: PageHandler;
115
- listReports: PageHandler;
115
+ listPages: PageHandler;
116
116
  listRules: PageHandler;
117
117
  listTopIssues: PageHandler;
118
118
  listViolators: PageHandler;
@@ -143,7 +143,7 @@ const answer: {
143
143
  hideReportForm,
144
144
  listDiagnoses: listDiagnosesPage,
145
145
  listIssues: listIssuesPage,
146
- listReports: listReportsPage,
146
+ listPages: listPagesPage,
147
147
  listRules,
148
148
  listTopIssues,
149
149
  listViolators: listViolatorsPage,
@@ -171,7 +171,7 @@ const apiRespond: {
171
171
  getReport: ApiResponder;
172
172
  listDiagnoses: ApiResponder;
173
173
  listIssues: ApiResponder;
174
- listReports: ApiResponder;
174
+ listPages: ApiResponder;
175
175
  listViolators: ApiResponder;
176
176
  requestFeature: ApiResponder;
177
177
  requestRetest: ApiResponder;
@@ -183,7 +183,7 @@ const apiRespond: {
183
183
  getReport: getReportAPI,
184
184
  listDiagnoses: listDiagnosesAPI,
185
185
  listIssues: listIssuesAPI,
186
- listReports: listReportsAPI,
186
+ listPages: listPagesAPI,
187
187
  listViolators: listViolatorsAPI,
188
188
  requestFeature: requestFeatureAPI,
189
189
  requestRetest: requestRetestAPI,
@@ -291,12 +291,16 @@ const noDirectGetPages = new Set(['unhideReportForm']);
291
291
  // on 2026-09-28 (see the newTest/retest naming-ambiguity fix), to distinguish the
292
292
  // new-test-only page from bare "test" language used elsewhere.
293
293
  // TEMPORARY: retire this redirect after 2027-03-28 once crawlers have re-indexed.
294
+ // listReports was renamed to listPages on 2026-10-09, since it lists tested pages, each
295
+ // linked to its latest report.
296
+ // TEMPORARY: retire this redirect after 2027-04-09 once crawlers have re-indexed.
294
297
  const renamedPagePrefixes: Record<string, string> = {
295
298
  diagnoses: 'listDiagnoses',
296
299
  reportIssue: 'listViolators',
297
300
  reportIssues: 'listIssues',
298
301
  rules: 'listRules',
299
- targets: 'listReports',
302
+ targets: 'listPages',
303
+ listReports: 'listPages',
300
304
  requestTest: 'requestNewTest',
301
305
  requestTestForm: 'requestNewTestForm'
302
306
  };
@@ -800,15 +804,22 @@ const handleRequest = async (request: IncomingMessage, response: ServerResponse)
800
804
  // Otherwise, if it is for an API service:
801
805
  else if (pageName === 'api') {
802
806
  const [service, ...specs] = pathTail.split('/');
803
- // If the service lists the available reports:
804
- if (service === 'listReports') {
807
+ // If the service lists the tested pages:
808
+ if (service === 'listPages') {
805
809
  // Get the response body.
806
- const responseBody = await apiRespond.listReports([]);
807
- await recordPageMetric('apiOperations', 'listReports');
810
+ const responseBody = await apiRespond.listPages([]);
811
+ await recordPageMetric('apiOperations', 'listPages');
808
812
  // Send it.
809
813
  setHeaders('application/json', null, 'ultra');
810
814
  response.end(JSON.stringify(responseBody));
811
815
  }
816
+ // Otherwise, if the service has the name that listPages had before 2026-10-09:
817
+ // TEMPORARY: retire this redirect after 2027-04-09.
818
+ else if (service === 'listReports') {
819
+ // Redirect the request to listPages.
820
+ response.writeHead(301, {Location: '/api/listPages'});
821
+ response.end();
822
+ }
812
823
  // Otherwise, if the service lists the issues in a report:
813
824
  else if (service === 'listIssues') {
814
825
  // Get the response body.