@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.
- 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 +1 -1
- package/api/orderNewTest.ts +4 -4
- package/api/requestNewTest.ts +6 -6
- 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/SERVICE.md +16 -5
- package/index.html +1 -1
- package/index.test.ts +30 -18
- package/index.ts +22 -11
- 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 +8 -8
- 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 +1 -1
- package/util.test.ts +71 -12
- package/util.ts +61 -12
- package/web/enqueueForm/index.html +1 -1
- package/web/hideReportForm/hideReportForm.test.ts +5 -0
- package/web/hideReportForm/index.ts +9 -6
- package/web/listIssues/index.html +2 -1
- package/web/listIssues/index.ts +45 -3
- package/web/listIssues/listIssues.test.ts +76 -0
- 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/metrics/metrics.test.ts +2 -2
- package/web/requestRetestForm/index.ts +4 -2
- package/web/requestRetestForm/requestRetestForm.test.ts +11 -0
- package/web/unhideReportForm/index.ts +9 -6
- package/web/unhideReportForm/unhideReportForm.test.ts +8 -5
- package/web/util.test.ts +85 -11
- package/web/util.ts +26 -28
- 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/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
|
|
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`, `
|
|
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 `
|
|
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 `
|
|
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="
|
|
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/
|
|
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/
|
|
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/
|
|
336
|
-
const countBefore = await getMetricCount('apiOperations', '
|
|
337
|
-
const res = await request('GET', '/api/
|
|
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'], '
|
|
341
|
-
assert.equal(await getMetricCount('apiOperations', '
|
|
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 /
|
|
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', '/
|
|
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 /
|
|
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'], '/
|
|
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
|
-
'/
|
|
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', '
|
|
1535
|
+
const apiOperationCountBefore = await getMetricCount('apiOperations', 'listPages');
|
|
1524
1536
|
await request('GET', '/tutorialWeb.html', null, {cookie: cookieHeader});
|
|
1525
|
-
await request('GET', '/api/
|
|
1537
|
+
await request('GET', '/api/listPages', null, {cookie: cookieHeader});
|
|
1526
1538
|
assert.equal(await getMetricCount('pageViews', 'tutorialWeb'), pageViewCountBefore);
|
|
1527
|
-
assert.equal(await getMetricCount('apiOperations', '
|
|
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', '/
|
|
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, '/
|
|
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', '/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
|
804
|
-
if (service === '
|
|
807
|
+
// If the service lists the tested pages:
|
|
808
|
+
if (service === 'listPages') {
|
|
805
809
|
// Get the response body.
|
|
806
|
-
const responseBody = await apiRespond.
|
|
807
|
-
await recordPageMetric('apiOperations', '
|
|
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.
|