@jrpool/kilotest 62.0.0 → 63.1.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/README.md CHANGED
@@ -97,5 +97,6 @@ When you **make an API or tool contract change** in a feature branch:
97
97
  8. Wait until `npmjs.com` has processed the publication.
98
98
  9. Use `git pull`, `npm ci`, and `pm2 restart kilotest` (but **not** `npm audit fix`) to deploy the revised `main` branch on the public server.
99
99
  10. Visit the [RapidAPI Studio](https://rapidapi.com/studio) and update the API definition at `Hub Listing` → `Definitions` → `CI/CD` → `Import OpenAPI` by uploading the regenerated `openapi-3.0.yaml`.
100
+ 11. Visit the [Claude directory](https://claude.ai/directory/manage/kilotest?tab=edit), edit any changed entries, and submit the changes.
100
101
 
101
102
  If any step fails, see the details in `docs/AI-TOOLS.md` for troubleshooting instructions.
@@ -29,6 +29,7 @@ before(() => {
29
29
  });
30
30
 
31
31
  import {response} from './awaitTest.ts';
32
+ import {awaitTestResponseSchema} from './schemas.ts';
32
33
 
33
34
  after(() => {
34
35
  delete process.env.AWAIT_JOB_POLL_MS;
@@ -112,3 +113,11 @@ test('awaitTest reports both orderNewTest and orderRetest as possible ancestors'
112
113
  assert.equal(ancestor.method, 'POST');
113
114
  }
114
115
  });
116
+
117
+ test('awaitTest response conforms to its response schema', async () => {
118
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
119
+ // original catches extra keys that zod strips but that the strict JSON schema
120
+ // advertised to MCP clients (additionalProperties: false) rejects.
121
+ const body = await response(['999999T9999-xyz']);
122
+ assert.deepEqual(awaitTestResponseSchema.parse(body), body);
123
+ });
@@ -8,6 +8,7 @@
8
8
  import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import {response} from './getReport.ts';
11
+ import {getReportResponseSchema} from './schemas.ts';
11
12
 
12
13
  // SETUP AND TEARDOWN
13
14
 
@@ -59,3 +60,11 @@ test('getReport includes tool name and metadata', async () => {
59
60
  assert.equal(body['tool name'], 'getReport');
60
61
  assert.ok(body['response metadata'].identifier);
61
62
  });
63
+
64
+ test('getReport response conforms to its response schema', async () => {
65
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
66
+ // original catches extra keys that zod strips but that the strict JSON schema
67
+ // advertised to MCP clients (additionalProperties: false) rejects.
68
+ const body = await response(['260101T0000-mix']);
69
+ assert.deepEqual(getReportResponseSchema.parse(body), body);
70
+ });
@@ -8,6 +8,7 @@
8
8
  import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import {response} from './listDiagnoses.ts';
11
+ import {listDiagnosesResponseSchema} from './schemas.ts';
11
12
 
12
13
  // SETUP AND TEARDOWN
13
14
 
@@ -129,3 +130,11 @@ test('listDiagnoses returns issue error for a falsy issue ID', async () => {
129
130
  const issueBasics = body['response content']['basics about the issue'] as any;
130
131
  assert.ok(issueBasics.error);
131
132
  });
133
+
134
+ test('listDiagnoses response conforms to its response schema', async () => {
135
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
136
+ // original catches extra keys that zod strips but that the strict JSON schema
137
+ // advertised to MCP clients (additionalProperties: false) rejects.
138
+ const body = await response(['0', 'linkNoText', '260101T0000-mix']);
139
+ assert.deepEqual(listDiagnosesResponseSchema.parse(body), body);
140
+ });
@@ -8,6 +8,7 @@
8
8
  import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import {response} from './listIssues.ts';
11
+ import {listIssuesResponseSchema} from './schemas.ts';
11
12
 
12
13
  // SETUP AND TEARDOWN
13
14
 
@@ -145,3 +146,11 @@ test('listIssues gives no history for a nonexistent report', async () => {
145
146
  const body = await response(['999999T9999-xyz']);
146
147
  assert.equal(body['response content']['history of reports about the page'], null);
147
148
  });
149
+
150
+ test('listIssues response conforms to its response schema', async () => {
151
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
152
+ // original catches extra keys that zod strips but that the strict JSON schema
153
+ // advertised to MCP clients (additionalProperties: false) rejects.
154
+ const body = await response(['260101T0000-mix']);
155
+ assert.deepEqual(listIssuesResponseSchema.parse(body), body);
156
+ });
@@ -8,6 +8,7 @@
8
8
  import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import {response} from './listPages.ts';
11
+ import {listPagesResponseSchema} from './schemas.ts';
11
12
 
12
13
  // SETUP AND TEARDOWN
13
14
 
@@ -91,3 +92,11 @@ test('listPages includes tool name and metadata', async () => {
91
92
  assert.ok(body['response metadata'].identifier);
92
93
  assert.ok(body['response metadata']['date and time']);
93
94
  });
95
+
96
+ test('listPages response conforms to its response schema', async () => {
97
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
98
+ // original catches extra keys that zod strips but that the strict JSON schema
99
+ // advertised to MCP clients (additionalProperties: false) rejects.
100
+ const body = await response();
101
+ assert.deepEqual(listPagesResponseSchema.parse(body), body);
102
+ });
@@ -8,6 +8,7 @@
8
8
  import {test, before, after} from 'node:test';
9
9
  import assert from 'node:assert/strict';
10
10
  import {response} from './listViolators.ts';
11
+ import {listViolatorsResponseSchema} from './schemas.ts';
11
12
 
12
13
  // SETUP AND TEARDOWN
13
14
 
@@ -105,3 +106,11 @@ test('listViolators returns an error for a falsy issue ID', async () => {
105
106
  const issueBasics = body['response content']['basics about the issue'] as any;
106
107
  assert.ok(issueBasics.error);
107
108
  });
109
+
110
+ test('listViolators response conforms to its response schema', async () => {
111
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
112
+ // original catches extra keys that zod strips but that the strict JSON schema
113
+ // advertised to MCP clients (additionalProperties: false) rejects.
114
+ const body = await response(['linkNoText', '260101T0000-mix']);
115
+ assert.deepEqual(listViolatorsResponseSchema.parse(body), body);
116
+ });
@@ -45,6 +45,7 @@ beforeEach(async () => {
45
45
  });
46
46
 
47
47
  import {response} from './orderNewTest.ts';
48
+ import {orderNewTestResponseSchema} from './schemas.ts';
48
49
 
49
50
  after(() => {
50
51
  console.log = originalLog;
@@ -210,3 +211,11 @@ test('orderNewTest response omits a web UI URL for this request but includes the
210
211
  assert.equal(similarWeb['this request'], null);
211
212
  assert.ok(similarWeb['closest ancestor request'].includes('listPages.html'));
212
213
  });
214
+
215
+ test('orderNewTest response conforms to its response schema', async () => {
216
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
217
+ // original catches extra keys that zod strips but that the strict JSON schema
218
+ // advertised to MCP clients (additionalProperties: false) rejects.
219
+ const body = await response(['Schema Conformance Ordered Page', 'https://example.com/schema-ordered', 'A reason that is long enough.']);
220
+ assert.deepEqual(orderNewTestResponseSchema.parse(body), body);
221
+ });
@@ -48,6 +48,7 @@ beforeEach(async () => {
48
48
  });
49
49
 
50
50
  import {response} from './orderRetest.ts';
51
+ import {orderRetestResponseSchema} from './schemas.ts';
51
52
 
52
53
  after(async () => {
53
54
  await fs.unlink(retestFixturePath);
@@ -177,3 +178,11 @@ test('orderRetest response omits a web UI URL for this request but includes the
177
178
  assert.equal(similarWeb['this request'], null);
178
179
  assert.ok(similarWeb['closest ancestor request'].includes('listIssues.html'));
179
180
  });
181
+
182
+ test('orderRetest response conforms to its response schema', async () => {
183
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
184
+ // original catches extra keys that zod strips but that the strict JSON schema
185
+ // advertised to MCP clients (additionalProperties: false) rejects.
186
+ const body = await response(['260101T0004-ord', 'A reason that is long enough.']);
187
+ assert.deepEqual(orderRetestResponseSchema.parse(body), body);
188
+ });
@@ -42,6 +42,7 @@ beforeEach(async () => {
42
42
  });
43
43
 
44
44
  import {response} from './reportDisagreement.ts';
45
+ import {reportDisagreementResponseSchema} from './schemas.ts';
45
46
 
46
47
  after(async () => {
47
48
  console.log = originalLog;
@@ -206,3 +207,11 @@ test('reportDisagreement includes tool name and metadata', async () => {
206
207
  assert.equal(body['tool name'], 'reportDisagreement');
207
208
  assert.ok(body['response metadata'].identifier);
208
209
  });
210
+
211
+ test('reportDisagreement response conforms to its response schema', async () => {
212
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
213
+ // original catches extra keys that zod strips but that the strict JSON schema
214
+ // advertised to MCP clients (additionalProperties: false) rejects.
215
+ const body = await response(['linkNoText', '260101T0000-mix', '0', 'axe', 'r11', 'A reason submitted only for schema conformance.']);
216
+ assert.deepEqual(reportDisagreementResponseSchema.parse(body), body);
217
+ });
@@ -37,6 +37,7 @@ beforeEach(async () => {
37
37
  });
38
38
 
39
39
  import {response} from './requestFeature.ts';
40
+ import {requestFeatureResponseSchema} from './schemas.ts';
40
41
 
41
42
  after(async () => {
42
43
  console.log = originalLog;
@@ -186,3 +187,11 @@ test('requestFeature accepts a feature request that differs from an existing one
186
187
  const featureRequests = JSON.parse(await fs.readFile(process.env.FEATURE_REQUESTS_PATH!, 'utf8'));
187
188
  assert.equal(featureRequests.length, 2);
188
189
  });
190
+
191
+ test('requestFeature response conforms to its response schema', async () => {
192
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
193
+ // original catches extra keys that zod strips but that the strict JSON schema
194
+ // advertised to MCP clients (additionalProperties: false) rejects.
195
+ const body = await response(['A feature request submitted only for schema conformance.']);
196
+ assert.deepEqual(requestFeatureResponseSchema.parse(body), body);
197
+ });
@@ -42,6 +42,7 @@ beforeEach(async () => {
42
42
  });
43
43
 
44
44
  import {response} from './requestNewTest.ts';
45
+ import {requestNewTestResponseSchema} from './schemas.ts';
45
46
 
46
47
  after(() => {
47
48
  console.log = originalLog;
@@ -208,3 +209,11 @@ test('requestNewTest rejects a request with a fallback channel when the queue is
208
209
  assert.ok(disposition['what happens next'].includes('https://github.com/jrpool/kilotest/issues'));
209
210
  assert.ok(disposition['what happens next'].includes('info@kilotest.com'));
210
211
  });
212
+
213
+ test('requestNewTest response conforms to its response schema', async () => {
214
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
215
+ // original catches extra keys that zod strips but that the strict JSON schema
216
+ // advertised to MCP clients (additionalProperties: false) rejects.
217
+ const body = await response(['Schema Conformance Page', 'https://example.com/schema-conformance', 'A reason that is long enough.']);
218
+ assert.deepEqual(requestNewTestResponseSchema.parse(body), body);
219
+ });
@@ -56,6 +56,7 @@ beforeEach(async () => {
56
56
  });
57
57
 
58
58
  import {response} from './requestRetest.ts';
59
+ import {requestRetestResponseSchema} from './schemas.ts';
59
60
 
60
61
  after(async () => {
61
62
  console.log = originalLog;
@@ -193,3 +194,11 @@ test('requestRetest rejects a request with a fallback channel when the queue is
193
194
  assert.ok(disposition['what happens next'].includes('https://github.com/jrpool/kilotest/issues'));
194
195
  assert.ok(disposition['what happens next'].includes('info@kilotest.com'));
195
196
  });
197
+
198
+ test('requestRetest response conforms to its response schema', async () => {
199
+ // parse() catches missing or mistyped keys; comparing the parsed result to the
200
+ // original catches extra keys that zod strips but that the strict JSON schema
201
+ // advertised to MCP clients (additionalProperties: false) rejects.
202
+ const body = await response(['260101T0003-ret', 'A reason that is long enough.']);
203
+ assert.deepEqual(requestRetestResponseSchema.parse(body), body);
204
+ });
package/api/schemas.ts CHANGED
@@ -95,7 +95,7 @@ const ruleEngineFactsSchema = z.object({
95
95
  }).meta({id: 'RuleEngineFacts'});
96
96
 
97
97
  // Facts about the Kilotest tool collection, included in every response.
98
- const toolsFactsSchema = z.object({
98
+ export const toolsFactsSchema = z.object({
99
99
  name: z.string(),
100
100
  description: z.object({
101
101
  'what Kilotest does': z.string(),
@@ -108,8 +108,10 @@ const toolsFactsSchema = z.object({
108
108
  'how to generate more findings': z.object({
109
109
  'new testing': z.string(),
110
110
  'retesting': z.string(),
111
+ 'approval': z.string(),
111
112
  'latency': z.string(),
112
- 'confirmation': z.string()
113
+ 'request confirmation': z.string(),
114
+ 'order confirmation': z.string()
113
115
  })
114
116
  }).describe('Explanation of what Kilotest does and how to use its tools.'),
115
117
  URL: z.string().describe('URL of the Kilotest MCP server.'),
package/api/util.test.ts CHANGED
@@ -17,6 +17,7 @@ for (const key of ['MANAGER_EMAIL', 'ALERT_API_HOST', 'ALERT_API_PATH', 'ALERT_A
17
17
  }
18
18
 
19
19
  import {getReportBasics, getResponseMetadata, getRuleEngineFacts, getRuleEnginesFacts, getIssueSpec, getToolsFacts, getRequestFailureReason} from './util.ts';
20
+ import {toolsFactsSchema} from './schemas.ts';
20
21
  import {processTestRequest} from '../util.ts';
21
22
 
22
23
  // SETUP
@@ -137,6 +138,14 @@ test('getToolsFacts returns facts about the Kilotest tool collection', () => {
137
138
  assert.ok(facts.description['how to generate more findings']);
138
139
  });
139
140
 
141
+ test('getToolsFacts output conforms to toolsFactsSchema', () => {
142
+ // parse() catches missing or mistyped keys, and comparing the parsed result to
143
+ // the original catches extra keys that zod strips but that the strict JSON
144
+ // schema advertised to MCP clients (additionalProperties: false) rejects.
145
+ const facts = getToolsFacts();
146
+ assert.deepEqual(toolsFactsSchema.parse(facts), facts);
147
+ });
148
+
140
149
  test('getRequestFailureReason returns the URL-length reason for a urlLength result', () => {
141
150
  // processTestRequest can return urlLength for a new-test request whose URL is
142
151
  // outside the 12-to-300-character bound. No current caller reaches this
package/docs/AI-TOOLS.md CHANGED
@@ -49,6 +49,7 @@ The external features that support the use of Kilotest as a collection of AI too
49
49
  - Registration of Kilotest with [Awesome MCP Servers](https://www.tensorblock.co/mcp/servers/github-jrpool-kilotest-f518ba36).
50
50
  - Registration of Kilotest with the [RapidAPI](https://rapidapi.com/jrpool/api/kilotest/playground/apiendpoint_0f03577a-ff9a-472a-a0ed-533bd198981a) Hub.
51
51
  - Registration of Kilotest with the [LobeHub MCP Market](https://lobehub.com/mcp) (identifier: `jrpool-kilotest`), enabled with an `lhm.plugin.json` file.
52
+ - Registration of Kilotest with [MCPTry](https://mcptry.com/servers/io.github.jrpool/kilotest).
52
53
  - Deployment of an MCP server in HTTP mode on the Kilotest service host.
53
54
  - Submission of the MCP server to the [Claude directory of plugins and connectors](https://claude.ai/directory/manage).
54
55
  - Configuration of Claude Desktop on the local development host and the `claude.ai` web application to connect Claude Desktop models to the Kilotest MCP server. The configuration was performed in the UI of each platform with the addition of Kilotest as a _connector_. The user used the `Customize/Connectors/Add connector/Add custom connector` interface, providing these data before activating the `Add` button:
package/index.test.ts CHANGED
@@ -2334,6 +2334,50 @@ test('startServer starts an HTTPS server when protocol is https', {timeout: 2000
2334
2334
  }
2335
2335
  });
2336
2336
 
2337
+ test('startServer throws when THIS_KILOTEST_HOST is unset', async () => {
2338
+ const savedHost = process.env.THIS_KILOTEST_HOST;
2339
+ const savedProtocol = process.env.PROTOCOL;
2340
+ delete process.env.THIS_KILOTEST_HOST;
2341
+ delete process.env.PROTOCOL;
2342
+ try {
2343
+ await assert.rejects(startServer(), /THIS_KILOTEST_HOST/);
2344
+ }
2345
+ finally {
2346
+ if (savedHost !== undefined) {
2347
+ process.env.THIS_KILOTEST_HOST = savedHost;
2348
+ }
2349
+ if (savedProtocol !== undefined) {
2350
+ process.env.PROTOCOL = savedProtocol;
2351
+ }
2352
+ }
2353
+ });
2354
+
2355
+ test('startServer throws when PROTOCOL is https and KEY and CERT are unset', async () => {
2356
+ const savedProtocol = process.env.PROTOCOL;
2357
+ const savedKey = process.env.KEY;
2358
+ const savedCert = process.env.CERT;
2359
+ process.env.PROTOCOL = 'https';
2360
+ delete process.env.KEY;
2361
+ delete process.env.CERT;
2362
+ try {
2363
+ await assert.rejects(startServer(), /KEY.*CERT|CERT.*KEY/);
2364
+ }
2365
+ finally {
2366
+ if (savedProtocol !== undefined) {
2367
+ process.env.PROTOCOL = savedProtocol;
2368
+ }
2369
+ else {
2370
+ delete process.env.PROTOCOL;
2371
+ }
2372
+ if (savedKey !== undefined) {
2373
+ process.env.KEY = savedKey;
2374
+ }
2375
+ if (savedCert !== undefined) {
2376
+ process.env.CERT = savedCert;
2377
+ }
2378
+ }
2379
+ });
2380
+
2337
2381
  test('runIfMain calls startServer when mainModule matches currentModule', async () => {
2338
2382
  let called = false;
2339
2383
  const starter = async () => {
@@ -2358,3 +2402,25 @@ test('runIfMain does not call startServer when mainModule differs from currentMo
2358
2402
  await new Promise<void>(resolve => setImmediate(resolve));
2359
2403
  assert.equal(called, false);
2360
2404
  });
2405
+
2406
+ test('runIfMain logs the error and sets a failing exit code when the starter rejects', async () => {
2407
+ const savedExitCode = process.exitCode;
2408
+ const logged: string[] = [];
2409
+ const originalLog = console.log;
2410
+ console.log = (line: string) => logged.push(line);
2411
+ const starter = async () => {
2412
+ throw new Error('injected startup failure');
2413
+ };
2414
+ const fakeModule = {};
2415
+ try {
2416
+ runIfMain(fakeModule, fakeModule, starter);
2417
+ // Wait for the rejection handler's microtasks to run.
2418
+ await new Promise<void>(resolve => setImmediate(resolve));
2419
+ assert.ok(logged.some(line => line.includes('injected startup failure')));
2420
+ assert.equal(process.exitCode, 1);
2421
+ }
2422
+ finally {
2423
+ console.log = originalLog;
2424
+ process.exitCode = savedExitCode;
2425
+ }
2426
+ });
package/index.ts CHANGED
@@ -1529,7 +1529,19 @@ export {serve};
1529
1529
 
1530
1530
  // Starts the server using the configured protocol and credentials.
1531
1531
  export const startServer = async () => {
1532
+ // Fail fast on missing required environment variables. THIS_KILOTEST_HOST is read raw
1533
+ // by getThisHost and interpolated into URLs in every response; KEY and CERT are
1534
+ // required only for HTTPS. Other env vars have documented defaults or designed
1535
+ // degradation (see env.example and docs/SERVICE.md) and are deliberately not required.
1536
+ const requiredEnvVars = ['THIS_KILOTEST_HOST'];
1532
1537
  const startProtocol = process.env.PROTOCOL || 'http';
1538
+ if (startProtocol === 'https') {
1539
+ requiredEnvVars.push('KEY', 'CERT');
1540
+ }
1541
+ const missingEnvVars = requiredEnvVars.filter(name => !process.env[name]);
1542
+ if (missingEnvVars.length) {
1543
+ throw new Error(`Missing required environment variable(s): ${missingEnvVars.join(', ')}`);
1544
+ }
1533
1545
  if (startProtocol === 'http') {
1534
1546
  console.log('Starting HTTP server');
1535
1547
  return serve(http, {});
@@ -1545,7 +1557,10 @@ export const startServer = async () => {
1545
1557
  // Runs the server if the module was loaded directly (not required by a test). The starter is a parameter so tests can inject a spy, since ESM module exports cannot be monkey-patched.
1546
1558
  export const runIfMain = (mainModule: unknown, currentModule: unknown, starter: () => Promise<unknown> = startServer) => {
1547
1559
  if (mainModule === currentModule) {
1548
- starter().catch(error => console.log(errorMessage(error)));
1560
+ starter().catch(error => {
1561
+ console.log(errorMessage(error));
1562
+ process.exitCode = 1;
1563
+ });
1549
1564
  }
1550
1565
  };
1551
1566
 
package/lhm.plugin.json CHANGED
@@ -263,18 +263,26 @@
263
263
  "retesting": {
264
264
  "type": "string"
265
265
  },
266
+ "approval": {
267
+ "type": "string"
268
+ },
266
269
  "latency": {
267
270
  "type": "string"
268
271
  },
269
- "confirmation": {
272
+ "request confirmation": {
273
+ "type": "string"
274
+ },
275
+ "order confirmation": {
270
276
  "type": "string"
271
277
  }
272
278
  },
273
279
  "required": [
274
280
  "new testing",
275
281
  "retesting",
282
+ "approval",
276
283
  "latency",
277
- "confirmation"
284
+ "request confirmation",
285
+ "order confirmation"
278
286
  ],
279
287
  "additionalProperties": false
280
288
  }
@@ -907,18 +915,26 @@
907
915
  "retesting": {
908
916
  "type": "string"
909
917
  },
918
+ "approval": {
919
+ "type": "string"
920
+ },
910
921
  "latency": {
911
922
  "type": "string"
912
923
  },
913
- "confirmation": {
924
+ "request confirmation": {
925
+ "type": "string"
926
+ },
927
+ "order confirmation": {
914
928
  "type": "string"
915
929
  }
916
930
  },
917
931
  "required": [
918
932
  "new testing",
919
933
  "retesting",
934
+ "approval",
920
935
  "latency",
921
- "confirmation"
936
+ "request confirmation",
937
+ "order confirmation"
922
938
  ],
923
939
  "additionalProperties": false
924
940
  }
@@ -1341,18 +1357,26 @@
1341
1357
  "retesting": {
1342
1358
  "type": "string"
1343
1359
  },
1360
+ "approval": {
1361
+ "type": "string"
1362
+ },
1344
1363
  "latency": {
1345
1364
  "type": "string"
1346
1365
  },
1347
- "confirmation": {
1366
+ "request confirmation": {
1367
+ "type": "string"
1368
+ },
1369
+ "order confirmation": {
1348
1370
  "type": "string"
1349
1371
  }
1350
1372
  },
1351
1373
  "required": [
1352
1374
  "new testing",
1353
1375
  "retesting",
1376
+ "approval",
1354
1377
  "latency",
1355
- "confirmation"
1378
+ "request confirmation",
1379
+ "order confirmation"
1356
1380
  ],
1357
1381
  "additionalProperties": false
1358
1382
  }
@@ -1886,18 +1910,26 @@
1886
1910
  "retesting": {
1887
1911
  "type": "string"
1888
1912
  },
1913
+ "approval": {
1914
+ "type": "string"
1915
+ },
1889
1916
  "latency": {
1890
1917
  "type": "string"
1891
1918
  },
1892
- "confirmation": {
1919
+ "request confirmation": {
1920
+ "type": "string"
1921
+ },
1922
+ "order confirmation": {
1893
1923
  "type": "string"
1894
1924
  }
1895
1925
  },
1896
1926
  "required": [
1897
1927
  "new testing",
1898
1928
  "retesting",
1929
+ "approval",
1899
1930
  "latency",
1900
- "confirmation"
1931
+ "request confirmation",
1932
+ "order confirmation"
1901
1933
  ],
1902
1934
  "additionalProperties": false
1903
1935
  }
@@ -2275,18 +2307,26 @@
2275
2307
  "retesting": {
2276
2308
  "type": "string"
2277
2309
  },
2310
+ "approval": {
2311
+ "type": "string"
2312
+ },
2278
2313
  "latency": {
2279
2314
  "type": "string"
2280
2315
  },
2281
- "confirmation": {
2316
+ "request confirmation": {
2317
+ "type": "string"
2318
+ },
2319
+ "order confirmation": {
2282
2320
  "type": "string"
2283
2321
  }
2284
2322
  },
2285
2323
  "required": [
2286
2324
  "new testing",
2287
2325
  "retesting",
2326
+ "approval",
2288
2327
  "latency",
2289
- "confirmation"
2328
+ "request confirmation",
2329
+ "order confirmation"
2290
2330
  ],
2291
2331
  "additionalProperties": false
2292
2332
  }
@@ -2634,18 +2674,26 @@
2634
2674
  "retesting": {
2635
2675
  "type": "string"
2636
2676
  },
2677
+ "approval": {
2678
+ "type": "string"
2679
+ },
2637
2680
  "latency": {
2638
2681
  "type": "string"
2639
2682
  },
2640
- "confirmation": {
2683
+ "request confirmation": {
2684
+ "type": "string"
2685
+ },
2686
+ "order confirmation": {
2641
2687
  "type": "string"
2642
2688
  }
2643
2689
  },
2644
2690
  "required": [
2645
2691
  "new testing",
2646
2692
  "retesting",
2693
+ "approval",
2647
2694
  "latency",
2648
- "confirmation"
2695
+ "request confirmation",
2696
+ "order confirmation"
2649
2697
  ],
2650
2698
  "additionalProperties": false
2651
2699
  }
@@ -2978,18 +3026,26 @@
2978
3026
  "retesting": {
2979
3027
  "type": "string"
2980
3028
  },
3029
+ "approval": {
3030
+ "type": "string"
3031
+ },
2981
3032
  "latency": {
2982
3033
  "type": "string"
2983
3034
  },
2984
- "confirmation": {
3035
+ "request confirmation": {
3036
+ "type": "string"
3037
+ },
3038
+ "order confirmation": {
2985
3039
  "type": "string"
2986
3040
  }
2987
3041
  },
2988
3042
  "required": [
2989
3043
  "new testing",
2990
3044
  "retesting",
3045
+ "approval",
2991
3046
  "latency",
2992
- "confirmation"
3047
+ "request confirmation",
3048
+ "order confirmation"
2993
3049
  ],
2994
3050
  "additionalProperties": false
2995
3051
  }
@@ -3272,18 +3328,26 @@
3272
3328
  "retesting": {
3273
3329
  "type": "string"
3274
3330
  },
3331
+ "approval": {
3332
+ "type": "string"
3333
+ },
3275
3334
  "latency": {
3276
3335
  "type": "string"
3277
3336
  },
3278
- "confirmation": {
3337
+ "request confirmation": {
3338
+ "type": "string"
3339
+ },
3340
+ "order confirmation": {
3279
3341
  "type": "string"
3280
3342
  }
3281
3343
  },
3282
3344
  "required": [
3283
3345
  "new testing",
3284
3346
  "retesting",
3347
+ "approval",
3285
3348
  "latency",
3286
- "confirmation"
3349
+ "request confirmation",
3350
+ "order confirmation"
3287
3351
  ],
3288
3352
  "additionalProperties": false
3289
3353
  }
@@ -3602,18 +3666,26 @@
3602
3666
  "retesting": {
3603
3667
  "type": "string"
3604
3668
  },
3669
+ "approval": {
3670
+ "type": "string"
3671
+ },
3605
3672
  "latency": {
3606
3673
  "type": "string"
3607
3674
  },
3608
- "confirmation": {
3675
+ "request confirmation": {
3676
+ "type": "string"
3677
+ },
3678
+ "order confirmation": {
3609
3679
  "type": "string"
3610
3680
  }
3611
3681
  },
3612
3682
  "required": [
3613
3683
  "new testing",
3614
3684
  "retesting",
3685
+ "approval",
3615
3686
  "latency",
3616
- "confirmation"
3687
+ "request confirmation",
3688
+ "order confirmation"
3617
3689
  ],
3618
3690
  "additionalProperties": false
3619
3691
  }
@@ -3969,18 +4041,26 @@
3969
4041
  "retesting": {
3970
4042
  "type": "string"
3971
4043
  },
4044
+ "approval": {
4045
+ "type": "string"
4046
+ },
3972
4047
  "latency": {
3973
4048
  "type": "string"
3974
4049
  },
3975
- "confirmation": {
4050
+ "request confirmation": {
4051
+ "type": "string"
4052
+ },
4053
+ "order confirmation": {
3976
4054
  "type": "string"
3977
4055
  }
3978
4056
  },
3979
4057
  "required": [
3980
4058
  "new testing",
3981
4059
  "retesting",
4060
+ "approval",
3982
4061
  "latency",
3983
- "confirmation"
4062
+ "request confirmation",
4063
+ "order confirmation"
3984
4064
  ],
3985
4065
  "additionalProperties": false
3986
4066
  }
@@ -4321,18 +4401,26 @@
4321
4401
  "retesting": {
4322
4402
  "type": "string"
4323
4403
  },
4404
+ "approval": {
4405
+ "type": "string"
4406
+ },
4324
4407
  "latency": {
4325
4408
  "type": "string"
4326
4409
  },
4327
- "confirmation": {
4410
+ "request confirmation": {
4411
+ "type": "string"
4412
+ },
4413
+ "order confirmation": {
4328
4414
  "type": "string"
4329
4415
  }
4330
4416
  },
4331
4417
  "required": [
4332
4418
  "new testing",
4333
4419
  "retesting",
4420
+ "approval",
4334
4421
  "latency",
4335
- "confirmation"
4422
+ "request confirmation",
4423
+ "order confirmation"
4336
4424
  ],
4337
4425
  "additionalProperties": false
4338
4426
  }
@@ -4622,18 +4710,26 @@
4622
4710
  "retesting": {
4623
4711
  "type": "string"
4624
4712
  },
4713
+ "approval": {
4714
+ "type": "string"
4715
+ },
4625
4716
  "latency": {
4626
4717
  "type": "string"
4627
4718
  },
4628
- "confirmation": {
4719
+ "request confirmation": {
4720
+ "type": "string"
4721
+ },
4722
+ "order confirmation": {
4629
4723
  "type": "string"
4630
4724
  }
4631
4725
  },
4632
4726
  "required": [
4633
4727
  "new testing",
4634
4728
  "retesting",
4729
+ "approval",
4635
4730
  "latency",
4636
- "confirmation"
4731
+ "request confirmation",
4732
+ "order confirmation"
4637
4733
  ],
4638
4734
  "additionalProperties": false
4639
4735
  }
@@ -4722,5 +4818,5 @@
4722
4818
  }
4723
4819
  }
4724
4820
  ],
4725
- "version": "62.0.0"
4821
+ "version": "63.1.0"
4726
4822
  }
package/openapi-3.0.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.0.3
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 62.0.0
5
+ version: 63.1.0
6
6
  license:
7
7
  name: MIT
8
8
  url: https://github.com/jrpool/kilotest/blob/main/LICENSE
@@ -1832,15 +1832,21 @@ components:
1832
1832
  type: string
1833
1833
  retesting:
1834
1834
  type: string
1835
+ approval:
1836
+ type: string
1835
1837
  latency:
1836
1838
  type: string
1837
- confirmation:
1839
+ request confirmation:
1840
+ type: string
1841
+ order confirmation:
1838
1842
  type: string
1839
1843
  required:
1840
1844
  - new testing
1841
1845
  - retesting
1846
+ - approval
1842
1847
  - latency
1843
- - confirmation
1848
+ - request confirmation
1849
+ - order confirmation
1844
1850
  additionalProperties: false
1845
1851
  required:
1846
1852
  - what Kilotest does
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 62.0.0
5
+ version: 63.1.0
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
@@ -1814,15 +1814,21 @@ components:
1814
1814
  type: string
1815
1815
  retesting:
1816
1816
  type: string
1817
+ approval:
1818
+ type: string
1817
1819
  latency:
1818
1820
  type: string
1819
- confirmation:
1821
+ request confirmation:
1822
+ type: string
1823
+ order confirmation:
1820
1824
  type: string
1821
1825
  required:
1822
1826
  - new testing
1823
1827
  - retesting
1828
+ - approval
1824
1829
  - latency
1825
- - confirmation
1830
+ - request confirmation
1831
+ - order confirmation
1826
1832
  additionalProperties: false
1827
1833
  required:
1828
1834
  - what Kilotest does
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "62.0.0",
3
+ "version": "63.1.0",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "62.0.0",
9
+ "version": "63.1.0",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",
@@ -14,5 +14,14 @@
14
14
  }
15
15
  ],
16
16
  "title": "Kilotest",
17
- "websiteUrl": "https://kilotest.com"
17
+ "websiteUrl": "https://kilotest.com",
18
+ "icons": [
19
+ {
20
+ "src": "https://kilotest.com/favicon.svg",
21
+ "mimeType": "image/svg+xml",
22
+ "sizes": [
23
+ "any"
24
+ ]
25
+ }
26
+ ]
18
27
  }
package/test/setup.ts CHANGED
@@ -17,3 +17,7 @@ for (const key of ['MANAGER_EMAIL', 'ALERT_API_HOST', 'ALERT_API_PATH', 'ALERT_A
17
17
  // to pass the resolution check that isAllowedTarget performs. A test of that check
18
18
  // itself sets this env var back to unset (or false) around the specific call being tested.
19
19
  process.env.ALLOW_INTERNAL_TARGETS = 'true';
20
+ // Give every response a deterministic host. Without this, getThisHost returns
21
+ // undefined, so URL-valued response fields fail schema validation; setting it here
22
+ // rather than relying on a developer's .env keeps tests independent of the environment.
23
+ process.env.THIS_KILOTEST_HOST = 'https://kilotest.example.com';
@@ -58,6 +58,19 @@
58
58
  </ol>
59
59
  <p>Once you add Kilotest, it also becomes available in the Gemini app on your mobile device. To make Gemini use Kilotest for a prompt, enter <q>@</q> and select Kilotest.</p>
60
60
  </details>
61
+ <details id="github-copilot">
62
+ <summary>GitHub Copilot</summary>
63
+ <p>These instructions apply to <a href="https://github.com/features/copilot">GitHub Copilot</a> in the <a href="https://code.visualstudio.com">Visual Studio Code</a> application, version 1.99 or later, on any Copilot plan, including the Copilot Free plan. As prerequisites, you must have <a href="https://code.visualstudio.com/docs/setup/copilot">enabled Copilot</a> in the application, and you must be signed in to your GitHub account.</p>
64
+ <ol>
65
+ <li>Open the Command Palette and run <q>MCP: Add Server</q>.</li>
66
+ <li>Select <q>HTTP (HTTP or Server-Sent Events)</q>.</li>
67
+ <li>Enter <q>https://kilotest.com/mcp</q> as the server URL.</li>
68
+ <li>Enter <q>kilotest</q> as the server ID.</li>
69
+ <li>Select <q>Global</q> (and <q>Copilot Global</q>, if prompted) to make Kilotest available in all your workspaces.</li>
70
+ <li>Accept the prompt to trust and start the server.</li>
71
+ </ol>
72
+ <p>In the Copilot Chat view, select the <q>Agent</q> mode; the Kilotest tools then become available to Copilot. In Visual Studio 2022 version 17.14 or later, you can instead open the chat tool picker, select the <q>+</q> button, and choose <q>Add custom MCP server</q> to enter the same name and URL.</p>
73
+ </details>
61
74
  <details id="grok">
62
75
  <summary>Grok</summary>
63
76
  <p>These instructions apply to <a href="https://grok.com">Grok</a> on any plan, including the free plan.</p>
@@ -82,6 +95,21 @@
82
95
  <li>Save the connector.</li>
83
96
  </ol>
84
97
  </details>
98
+ <details id="microsoft-foundry">
99
+ <summary>Microsoft Foundry</summary>
100
+ <p>These instructions apply to <a href="https://ai.azure.com">Microsoft Foundry</a> (formerly Azure AI Foundry), where agents are billed pay-as-you-go through an Azure subscription. A new Azure account currently includes a US$200 credit. Before connecting Kilotest, you need a Foundry project with a deployed model and an agent.</p>
101
+ <ol>
102
+ <li>In the Foundry portal, select the <q>Build</q> tab and select your agent.</li>
103
+ <li>In the <q>Playground</q> tab, expand the <q>Tools</q> dropdown and select <q>Add</q>.</li>
104
+ <li>In the <q>Custom</q> tab, select <q>Model Context Protocol (MCP)</q>, then <q>Create</q>.</li>
105
+ <li>Enter <q>Kilotest</q> as the <q>Name</q>.</li>
106
+ <li>Enter <q>https://kilotest.com/mcp</q> as the <q>Remote MCP Server endpoint</q>.</li>
107
+ <li>Set the <q>Authentication</q> to <q>Unauthenticated</q>.</li>
108
+ <li>Select <q>Connect</q>.</li>
109
+ <li>Select <q>Save</q> to save the tool configuration in your agent.</li>
110
+ </ol>
111
+ <p>When your agent wants to call a Kilotest tool, review the tool name and arguments and select <q>Approve</q> to allow the call.</p>
112
+ </details>
85
113
  <details id="perplexity">
86
114
  <summary>Perplexity</summary>
87
115
  <p>These instructions apply to <a href="https://www.perplexity.ai/">Perplexity</a> Pro in a browser.</p>
@@ -114,7 +142,7 @@
114
142
  <h2>Demonstration</h2>
115
143
  <p>A <a href="/demo-chatgpt.html">step-through demonstration</a> shows ChatGPT using Kilotest to discover tested pages, inspect reported issues, and request tests and retests.</p>
116
144
  <h2 id="other">Other AI platforms</h2>
117
- <p>You can connect Kilotest to some other AI platforms, too. The instructions above are limited to consumer platforms that are either free or relatively inexpensive (US$20 per month or less). Business platforms such as ChatGPT Business, and developer platforms such as Devin Desktop, also permit you to enable Kilotest.</p>
145
+ <p>You can connect Kilotest to some other AI platforms, too. The instructions above are limited to platforms available to individuals that are free, relatively inexpensive (US$20 per month or less), or billed pay-as-you-go. Business platforms such as ChatGPT Business and Microsoft Copilot Studio, and developer platforms such as Devin Desktop, also permit you to enable Kilotest.</p>
118
146
  <p>Feel free to submit a comment below about empowering any AI platform to use Kilotest.</p>
119
147
  <p>You can also submit an issue on the <a href="https://github.com/jrpool/kilotest/issues">Kilotest GitHub repository</a>.</p>
120
148
  <section id="suggest-improvement">