@jrpool/kilotest 55.0.0 → 57.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 +1 -1
- package/api/awaitTest.test.ts +114 -0
- package/api/awaitTest.ts +108 -0
- package/api/listReports.ts +2 -2
- package/api/orderNewTest.test.ts +213 -0
- package/api/orderNewTest.ts +145 -0
- package/api/orderRetest.test.ts +179 -0
- package/api/orderRetest.ts +119 -0
- package/api/{requestTest.test.ts → requestNewTest.test.ts} +25 -25
- package/api/{requestTest.ts → requestNewTest.ts} +16 -39
- package/api/requestRetest.test.ts +6 -6
- package/api/requestRetest.ts +10 -33
- package/api/routes.test.ts +7 -4
- package/api/routes.ts +45 -7
- package/api/schemas.ts +120 -10
- package/api/util.ts +59 -2
- package/api/version.ts +6 -2
- package/capability.md +12 -5
- package/docs/SERVICE.md +6 -8
- package/env.example +15 -0
- package/index.html +1 -0
- package/index.test.ts +95 -24
- package/index.ts +83 -16
- package/lhm.plugin.json +1 -1
- package/linkValidation.test.ts +2 -2
- package/llms-full.txt +34 -3
- package/llms.txt +6 -2
- package/mcp.test.ts +72 -12
- package/mcp.ts +81 -11
- package/memory/MEMORY.md +1 -0
- package/memory/plan_issue_111_instant_test.md +198 -0
- package/openapi.yaml +451 -4
- package/package.json +1 -1
- package/routes.test.ts +1 -1
- package/routing.test.ts +2 -2
- package/server.json +1 -1
- package/sitemap.xml +2 -1
- package/smokeTest.ts +2 -2
- package/util.test.ts +214 -2
- package/util.ts +175 -7
- package/web/listReports/index.html +1 -1
- package/web/listReports/listReports.test.ts +1 -1
- package/web/privacy/index.html +37 -0
- package/web/privacy/index.ts +20 -0
- package/web/privacy/privacy.test.ts +18 -0
- package/web/{requestTest → requestNewTest}/index.ts +1 -1
- package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +2 -2
- package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
- package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
- package/web/tutorialAI/index.html +26 -39
- /package/web/{requestTest → requestNewTest}/index.html +0 -0
- /package/web/{requestTestForm → requestNewTestForm}/index.ts +0 -0
package/mcp.test.ts
CHANGED
|
@@ -37,10 +37,10 @@ test('mcpPath is /mcp', () => {
|
|
|
37
37
|
assert.equal(mcpPath, '/mcp');
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
-
test('createMCPServer registers all
|
|
40
|
+
test('createMCPServer registers all 12 tools', () => {
|
|
41
41
|
const server = createMCPServer();
|
|
42
42
|
const toolNames = Object.keys((server as any)._registeredTools);
|
|
43
|
-
assert.equal(toolNames.length,
|
|
43
|
+
assert.equal(toolNames.length, 12);
|
|
44
44
|
assert.deepEqual(toolNames, [
|
|
45
45
|
'getKilotestOverview',
|
|
46
46
|
'listReports',
|
|
@@ -48,9 +48,12 @@ test('createMCPServer registers all 9 tools', () => {
|
|
|
48
48
|
'listViolators',
|
|
49
49
|
'listDiagnoses',
|
|
50
50
|
'getReport',
|
|
51
|
-
'
|
|
51
|
+
'requestNewTest',
|
|
52
52
|
'requestRetest',
|
|
53
|
-
'requestFeature'
|
|
53
|
+
'requestFeature',
|
|
54
|
+
'orderNewTest',
|
|
55
|
+
'orderRetest',
|
|
56
|
+
'awaitTest'
|
|
54
57
|
]);
|
|
55
58
|
});
|
|
56
59
|
|
|
@@ -159,10 +162,10 @@ test('getReport handler returns content and structuredContent for a valid report
|
|
|
159
162
|
assert.equal(getToolCallCount('getReport'), countBefore + 1);
|
|
160
163
|
});
|
|
161
164
|
|
|
162
|
-
test('
|
|
165
|
+
test('requestNewTest handler returns content and structuredContent', async () => {
|
|
163
166
|
const server = createMCPServer();
|
|
164
|
-
const countBefore = getToolCallCount('
|
|
165
|
-
const result = await (server as any)._registeredTools.
|
|
167
|
+
const countBefore = getToolCallCount('requestNewTest');
|
|
168
|
+
const result = await (server as any)._registeredTools.requestNewTest.handler({
|
|
166
169
|
description: 'Test Page',
|
|
167
170
|
URL: 'https://example.com/test',
|
|
168
171
|
reason: 'Because accessibility matters'
|
|
@@ -170,7 +173,7 @@ test('requestTest handler returns content and structuredContent', async () => {
|
|
|
170
173
|
assert.ok(result.content);
|
|
171
174
|
assert.equal(result.content[0].type, 'text');
|
|
172
175
|
assert.ok(result.structuredContent);
|
|
173
|
-
assert.equal(getToolCallCount('
|
|
176
|
+
assert.equal(getToolCallCount('requestNewTest'), countBefore + 1);
|
|
174
177
|
});
|
|
175
178
|
|
|
176
179
|
test('requestRetest handler returns content and structuredContent for a valid report', async () => {
|
|
@@ -199,6 +202,60 @@ test('requestFeature handler returns content and structuredContent', async () =>
|
|
|
199
202
|
assert.equal(getToolCallCount('requestFeature'), countBefore + 1);
|
|
200
203
|
});
|
|
201
204
|
|
|
205
|
+
test('orderNewTest handler returns content and structuredContent, and enqueues a job', async () => {
|
|
206
|
+
const {jobsPath} = await import('./util.ts');
|
|
207
|
+
const server = createMCPServer();
|
|
208
|
+
const countBefore = getToolCallCount('orderNewTest');
|
|
209
|
+
const result = await (server as any)._registeredTools.orderNewTest.handler({
|
|
210
|
+
description: 'MCP Ordered Page',
|
|
211
|
+
URL: 'https://example.com/mcp-ordered',
|
|
212
|
+
reason: 'Because accessibility matters here'
|
|
213
|
+
});
|
|
214
|
+
assert.ok(result.content);
|
|
215
|
+
assert.equal(result.content[0].type, 'text');
|
|
216
|
+
assert.ok(result.structuredContent);
|
|
217
|
+
assert.equal(getToolCallCount('orderNewTest'), countBefore + 1);
|
|
218
|
+
const reportIdentifier = result.structuredContent['response content']['disposition of your order']['report identifier'];
|
|
219
|
+
assert.ok(reportIdentifier);
|
|
220
|
+
const queuedPath = `${jobsPath()}/queue/${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`;
|
|
221
|
+
fs.unlinkSync(queuedPath);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('orderRetest handler returns content and structuredContent, and enqueues a job', async () => {
|
|
225
|
+
const {jobsPath} = await import('./util.ts');
|
|
226
|
+
const server = createMCPServer();
|
|
227
|
+
const countBefore = getToolCallCount('orderRetest');
|
|
228
|
+
// 260202T0000-new is the latest report of "Mixed Outcomes Page" (260101T0000-mix is an
|
|
229
|
+
// earlier, superseded report of the same page), so this order is accepted.
|
|
230
|
+
const result = await (server as any)._registeredTools.orderRetest.handler({
|
|
231
|
+
timeStamp: '260202T0000',
|
|
232
|
+
jobID: 'new',
|
|
233
|
+
reason: 'Because the report is obsolete'
|
|
234
|
+
});
|
|
235
|
+
assert.ok(result.content);
|
|
236
|
+
assert.equal(result.content[0].type, 'text');
|
|
237
|
+
assert.ok(result.structuredContent);
|
|
238
|
+
assert.equal(getToolCallCount('orderRetest'), countBefore + 1);
|
|
239
|
+
const reportIdentifier = result.structuredContent['response content']['disposition of your order']['report identifier'];
|
|
240
|
+
assert.ok(reportIdentifier);
|
|
241
|
+
const queuedPath = `${jobsPath()}/queue/${reportIdentifier.timeStamp}-${reportIdentifier.jobID}.json`;
|
|
242
|
+
fs.unlinkSync(queuedPath);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('awaitTest handler returns content and structuredContent for an already-completed report', async () => {
|
|
246
|
+
const server = createMCPServer();
|
|
247
|
+
const countBefore = getToolCallCount('awaitTest');
|
|
248
|
+
const result = await (server as any)._registeredTools.awaitTest.handler({
|
|
249
|
+
timeStamp: '260101T0000',
|
|
250
|
+
jobID: 'mix'
|
|
251
|
+
});
|
|
252
|
+
assert.ok(result.content);
|
|
253
|
+
assert.equal(result.content[0].type, 'text');
|
|
254
|
+
assert.ok(result.structuredContent);
|
|
255
|
+
assert.equal(result.structuredContent['response content']['disposition of your wait'].outcome, 'completed');
|
|
256
|
+
assert.equal(getToolCallCount('awaitTest'), countBefore + 1);
|
|
257
|
+
});
|
|
258
|
+
|
|
202
259
|
test('listIssues handler returns an error for a nonexistent report', async () => {
|
|
203
260
|
const server = createMCPServer();
|
|
204
261
|
const result = await (server as any)._registeredTools.listIssues.handler({
|
|
@@ -301,7 +358,7 @@ test('handleMCP responds to initialize with server info', async () => {
|
|
|
301
358
|
}
|
|
302
359
|
});
|
|
303
360
|
|
|
304
|
-
test('handleMCP lists all
|
|
361
|
+
test('handleMCP lists all 12 tools via tools/list', async () => {
|
|
305
362
|
const server = await startMCPServer();
|
|
306
363
|
try {
|
|
307
364
|
const port = server.address().port;
|
|
@@ -309,7 +366,7 @@ test('handleMCP lists all 9 tools via tools/list', async () => {
|
|
|
309
366
|
assert.equal(res.statusCode, 200);
|
|
310
367
|
const result = parseSSEResult(res.body);
|
|
311
368
|
const toolNames = result.result.tools.map((t: any) => t.name);
|
|
312
|
-
assert.equal(toolNames.length,
|
|
369
|
+
assert.equal(toolNames.length, 12);
|
|
313
370
|
assert.deepEqual(toolNames, [
|
|
314
371
|
'getKilotestOverview',
|
|
315
372
|
'listReports',
|
|
@@ -317,9 +374,12 @@ test('handleMCP lists all 9 tools via tools/list', async () => {
|
|
|
317
374
|
'listViolators',
|
|
318
375
|
'listDiagnoses',
|
|
319
376
|
'getReport',
|
|
320
|
-
'
|
|
377
|
+
'requestNewTest',
|
|
321
378
|
'requestRetest',
|
|
322
|
-
'requestFeature'
|
|
379
|
+
'requestFeature',
|
|
380
|
+
'orderNewTest',
|
|
381
|
+
'orderRetest',
|
|
382
|
+
'awaitTest'
|
|
323
383
|
]);
|
|
324
384
|
}
|
|
325
385
|
finally {
|
package/mcp.ts
CHANGED
|
@@ -13,9 +13,12 @@ import * as listReportsAPI from './api/listReports.ts';
|
|
|
13
13
|
import * as listIssuesAPI from './api/listIssues.ts';
|
|
14
14
|
import * as listViolatorsAPI from './api/listViolators.ts';
|
|
15
15
|
import * as listDiagnosesAPI from './api/listDiagnoses.ts';
|
|
16
|
-
import * as
|
|
16
|
+
import * as requestNewTestAPI from './api/requestNewTest.ts';
|
|
17
17
|
import * as requestRetestAPI from './api/requestRetest.ts';
|
|
18
18
|
import * as requestFeatureAPI from './api/requestFeature.ts';
|
|
19
|
+
import * as orderNewTestAPI from './api/orderNewTest.ts';
|
|
20
|
+
import * as orderRetestAPI from './api/orderRetest.ts';
|
|
21
|
+
import * as awaitTestAPI from './api/awaitTest.ts';
|
|
19
22
|
import {version} from './api/version.ts';
|
|
20
23
|
import {recordMetric} from './util.ts';
|
|
21
24
|
|
|
@@ -24,17 +27,23 @@ import {
|
|
|
24
27
|
listIssuesSchema,
|
|
25
28
|
listViolatorsSchema,
|
|
26
29
|
listDiagnosesSchema,
|
|
27
|
-
|
|
30
|
+
requestNewTestSchema,
|
|
28
31
|
requestRetestSchema,
|
|
29
32
|
requestFeatureSchema,
|
|
33
|
+
orderNewTestSchema,
|
|
34
|
+
orderRetestSchema,
|
|
35
|
+
awaitTestSchema,
|
|
30
36
|
listReportsResponseSchema,
|
|
31
37
|
listIssuesResponseSchema,
|
|
32
38
|
listViolatorsResponseSchema,
|
|
33
39
|
listDiagnosesResponseSchema,
|
|
34
40
|
getReportResponseSchema,
|
|
35
|
-
|
|
41
|
+
requestNewTestResponseSchema,
|
|
36
42
|
requestRetestResponseSchema,
|
|
37
|
-
requestFeatureResponseSchema
|
|
43
|
+
requestFeatureResponseSchema,
|
|
44
|
+
orderNewTestResponseSchema,
|
|
45
|
+
orderRetestResponseSchema,
|
|
46
|
+
awaitTestResponseSchema
|
|
38
47
|
} from './api/schemas.ts';
|
|
39
48
|
|
|
40
49
|
// CONSTANTS
|
|
@@ -53,7 +62,8 @@ const kilotestOverview = [
|
|
|
53
62
|
'Kilotest tests web pages for front-end quality: accessibility, usability, and standards conformity.',
|
|
54
63
|
'Kilotest integrates an ensemble of twelve independent rule engines to test a web page and stores the results as a structured report.',
|
|
55
64
|
'Results are organized as a hierarchy. A report contains issues. An issue has violators: elements of the tested page reported as exhibiting the issue. A violator has diagnoses: explanations from rule engines of how the element exhibited the issue.',
|
|
56
|
-
'Typical workflow: call the listReports tool to learn whether a report about the page already exists. If it does, drill down with the listIssues, listViolators, and listDiagnoses tools, or retrieve the entire report with the getReport tool. If no report exists, request a test with the
|
|
65
|
+
'Typical workflow: call the listReports tool to learn whether a report about the page already exists. If it does, drill down with the listIssues, listViolators, and listDiagnoses tools, or retrieve the entire report with the getReport tool. If no report exists, request a test with the requestNewTest tool. If the latest report is obsolete, request a retest with the requestRetest tool.',
|
|
66
|
+
'For immediate, automatically approved testing, use the orderNewTest tool (for a new page) or the orderRetest tool (for a page with an obsolete report) instead of requestNewTest or requestRetest. Each enqueues the job right away, with no manual approval step, and returns a report identifier along with a question about whether to wait for completion. To wait, call the awaitTest tool with that identifier; it blocks until the report is ready, the job fails, or a maximum wait time elapses. To check later instead, call the listIssues tool with the same identifier.',
|
|
57
67
|
'More documentation is available at https://kilotest.com/llms.txt and https://kilotest.com/llms-full.txt.'
|
|
58
68
|
].join('\n\n');
|
|
59
69
|
|
|
@@ -71,7 +81,7 @@ export const createMCPServer = (): McpServer => {
|
|
|
71
81
|
description: 'Tools that test web pages for front-end quality (accessibility, usability, and standards conformity) and make test results available'
|
|
72
82
|
},
|
|
73
83
|
{
|
|
74
|
-
instructions: `${sharedContext} Use the listReports tool to start. If it shows that there is a report available about the page you want facts about, drill down with the listIssues, listViolators, and listDiagnoses tools. If not, use the
|
|
84
|
+
instructions: `${sharedContext} Use the listReports tool to start. If it shows that there is a report available about the page you want facts about, drill down with the listIssues, listViolators, and listDiagnoses tools. If not, use the requestNewTest tool to request that the page be tested, or the orderNewTest tool for immediate, automatically approved testing. If the latest report about the page is obsolete, use the requestRetest tool to request that the page be retested, or the orderRetest tool for immediate, automatically approved retesting. For detailed documentation, call the getKilotestOverview tool.`
|
|
75
85
|
});
|
|
76
86
|
server.registerTool(
|
|
77
87
|
'getKilotestOverview',
|
|
@@ -192,11 +202,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
192
202
|
}
|
|
193
203
|
);
|
|
194
204
|
server.registerTool(
|
|
195
|
-
'
|
|
205
|
+
'requestNewTest',
|
|
196
206
|
{
|
|
197
207
|
description: toolDoc('Process my request to test a page about which no report is available yet.'),
|
|
198
|
-
inputSchema:
|
|
199
|
-
outputSchema:
|
|
208
|
+
inputSchema: requestNewTestSchema,
|
|
209
|
+
outputSchema: requestNewTestResponseSchema,
|
|
200
210
|
annotations: {
|
|
201
211
|
title: toolDoc('Process my request to test a page about which no report is available yet.'),
|
|
202
212
|
readOnlyHint: false,
|
|
@@ -206,8 +216,8 @@ export const createMCPServer = (): McpServer => {
|
|
|
206
216
|
}
|
|
207
217
|
},
|
|
208
218
|
async ({description, URL, reason}) => {
|
|
209
|
-
const result = await
|
|
210
|
-
await recordMetric('mcpToolCalls', '
|
|
219
|
+
const result = await requestNewTestAPI.response([description, URL, reason]);
|
|
220
|
+
await recordMetric('mcpToolCalls', 'requestNewTest');
|
|
211
221
|
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
212
222
|
}
|
|
213
223
|
);
|
|
@@ -251,6 +261,66 @@ export const createMCPServer = (): McpServer => {
|
|
|
251
261
|
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
252
262
|
}
|
|
253
263
|
);
|
|
264
|
+
server.registerTool(
|
|
265
|
+
'orderNewTest',
|
|
266
|
+
{
|
|
267
|
+
description: toolDoc('Process my order to test a page about which no report is available yet, immediately and automatically approved, instead of waiting for manual approval. Testing typically takes about 2 minutes and occasionally up to 4 minutes.'),
|
|
268
|
+
inputSchema: orderNewTestSchema,
|
|
269
|
+
outputSchema: orderNewTestResponseSchema,
|
|
270
|
+
annotations: {
|
|
271
|
+
title: toolDoc('Process my order to test a page about which no report is available yet, immediately and automatically approved.'),
|
|
272
|
+
readOnlyHint: false,
|
|
273
|
+
idempotentHint: false,
|
|
274
|
+
destructiveHint: false,
|
|
275
|
+
openWorldHint: false
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async ({description, URL, reason}) => {
|
|
279
|
+
const result = await orderNewTestAPI.response([description, URL, reason]);
|
|
280
|
+
await recordMetric('mcpToolCalls', 'orderNewTest');
|
|
281
|
+
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
server.registerTool(
|
|
285
|
+
'orderRetest',
|
|
286
|
+
{
|
|
287
|
+
description: toolDoc('Process my order to retest a page immediately and automatically approved, instead of waiting for manual approval. Testing typically takes about 2 minutes and occasionally up to 4 minutes.'),
|
|
288
|
+
inputSchema: orderRetestSchema,
|
|
289
|
+
outputSchema: orderRetestResponseSchema,
|
|
290
|
+
annotations: {
|
|
291
|
+
title: toolDoc('Process my order to retest a page immediately and automatically approved.'),
|
|
292
|
+
readOnlyHint: false,
|
|
293
|
+
idempotentHint: false,
|
|
294
|
+
destructiveHint: false,
|
|
295
|
+
openWorldHint: false
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
async ({timeStamp, jobID, reason}) => {
|
|
299
|
+
const result = await orderRetestAPI.response([timeStamp, jobID, reason]);
|
|
300
|
+
await recordMetric('mcpToolCalls', 'orderRetest');
|
|
301
|
+
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
server.registerTool(
|
|
305
|
+
'awaitTest',
|
|
306
|
+
{
|
|
307
|
+
description: toolDoc('Wait for a job ordered via the orderNewTest or orderRetest tool to complete, using the report identifier either tool returned. Blocks until the report is ready, the job fails, or a maximum wait time elapses; sends no interim notices.'),
|
|
308
|
+
inputSchema: awaitTestSchema,
|
|
309
|
+
outputSchema: awaitTestResponseSchema,
|
|
310
|
+
annotations: {
|
|
311
|
+
title: toolDoc('Wait for a job ordered via the orderNewTest or orderRetest tool to complete.'),
|
|
312
|
+
readOnlyHint: true,
|
|
313
|
+
idempotentHint: false,
|
|
314
|
+
destructiveHint: false,
|
|
315
|
+
openWorldHint: false
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
async ({timeStamp, jobID}) => {
|
|
319
|
+
const result = await awaitTestAPI.response([timeStamp, jobID]);
|
|
320
|
+
await recordMetric('mcpToolCalls', 'awaitTest');
|
|
321
|
+
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
322
|
+
}
|
|
323
|
+
);
|
|
254
324
|
server.registerResource(
|
|
255
325
|
'kilotest-overview',
|
|
256
326
|
'docs://kilotest/overview',
|
package/memory/MEMORY.md
CHANGED
|
@@ -5,3 +5,4 @@ This directory holds persistent context files for the Kilotest project. Each fil
|
|
|
5
5
|
## Index
|
|
6
6
|
|
|
7
7
|
- [user_maintainer_context.md](user_maintainer_context.md) — user: Maintainer is 84 years old, sole contributor, prioritizes legibility for future maintainers over short-term velocity
|
|
8
|
+
- [plan_issue_111_instant_test.md](plan_issue_111_instant_test.md) — project: orderTest/orderRetest/awaitTest (issue #111), implemented on orderTest branch with full REST+MCP parity
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: project
|
|
3
|
+
name: Plan for issue #111 (instant testing)
|
|
4
|
+
description: Design and implementation of orderTest/orderRetest/awaitTest, the three-tool MCP addition (with REST parity) for immediate, auto-approved testing with an optional wait for completion. Implemented on the orderTest branch.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Issue #111: instant-testing MCP tools (implemented)
|
|
8
|
+
|
|
9
|
+
Issue: [jrpool/kilotest#111](https://github.com/jrpool/kilotest/issues/111). The issue's own comment already
|
|
10
|
+
narrowed scope to a minimal increment: keep the existing single-worker Testaro polling
|
|
11
|
+
model, add new MCP tools, skip cost recovery, add only a lightweight anti-abuse gate.
|
|
12
|
+
This was implemented on the `orderTest` branch as three tools, `orderTest`, `orderRetest`,
|
|
13
|
+
and `awaitTest`, each with REST (`/api/*`) and MCP exposure, at the user's request for
|
|
14
|
+
full parity with the existing `requestTest`/`requestRetest` tools.
|
|
15
|
+
|
|
16
|
+
## Why two tools, not one
|
|
17
|
+
|
|
18
|
+
MCP `tools/call` is one request, one response; a tool cannot pose a question mid-call
|
|
19
|
+
and get an answer in the same call. The natural interaction is: order the test (fast,
|
|
20
|
+
always returns quickly), then, as a genuinely separate second call, decide whether to
|
|
21
|
+
wait for the result. Splitting into two tools avoids needing any new MCP machinery
|
|
22
|
+
(progress tokens, progress notifications) to solve the timeout problem below.
|
|
23
|
+
|
|
24
|
+
### The problem that killed the single-tool, blocking-wait design
|
|
25
|
+
|
|
26
|
+
An earlier design considered one tool with a `waitForCompletion` flag, where a `true`
|
|
27
|
+
call would enqueue and then block up to ~4 minutes for the report. This breaks if the
|
|
28
|
+
MCP client times out client-side before Kilotest responds: the client has stopped
|
|
29
|
+
listening, so nothing the server does (a final response, a progress notification) can
|
|
30
|
+
reach it, unless the client supplied a `progressToken` up front and progress
|
|
31
|
+
notifications were emitted early enough to carry the job identifier before the
|
|
32
|
+
timeout hit. That makes the wait's safety depend on client capabilities the caller may
|
|
33
|
+
not have, with no way for the server to detect or work around the gap.
|
|
34
|
+
|
|
35
|
+
The fix is not "notify them anyway", it is: never let identifier delivery depend on a
|
|
36
|
+
wait completing. `orderTest` returns immediately, always, with the identifier already
|
|
37
|
+
in hand, before any wait is ever proposed. This makes the whole progress-token /
|
|
38
|
+
client-timeout question moot for the ordering step; the identifier is guaranteed
|
|
39
|
+
delivered before any risky wait begins.
|
|
40
|
+
|
|
41
|
+
### Why a caller could never misuse awaitTest on a manually-approved job
|
|
42
|
+
|
|
43
|
+
Considered and rejected: restricting `awaitTest` to jobs specifically created by
|
|
44
|
+
`orderTest`, versus letting it work generically on any pending job by `timeStamp`/
|
|
45
|
+
`jobID`. This turned out to be moot: `requestTest`/`requestRetest` never hand the
|
|
46
|
+
caller a `timeStamp`/`jobID` at all (nothing exists yet), only instructions to poll
|
|
47
|
+
`listReports` later by description/URL. An identifier for a report only becomes
|
|
48
|
+
visible once the report is complete. So no caller can ever construct an `awaitTest`
|
|
49
|
+
call naming a manually-approved, still-pending job; the scenario that would need a
|
|
50
|
+
restriction cannot arise. No state-check or restriction is needed in `awaitTest`.
|
|
51
|
+
`orderTest` is the only path that exposes a pre-completion identifier at all, because
|
|
52
|
+
it enqueues synchronously and returns the job's own identifier immediately.
|
|
53
|
+
|
|
54
|
+
## Implementation notes
|
|
55
|
+
|
|
56
|
+
- `orderJob(target, reason)` in `util.ts` is the shared enqueue function backing both
|
|
57
|
+
`orderTest` (target `{description, url}`) and `orderRetest` (target `{timeStamp,
|
|
58
|
+
jobID}`), mirroring `processTestRequest`'s own dual-target design. It shares
|
|
59
|
+
`testRequestsLock` with `processTestRequest` so the two can never race to enqueue
|
|
60
|
+
duplicate jobs for the same page.
|
|
61
|
+
- `awaitJob(timeStamp, jobID)` in `util.ts` is the shared poll function backing
|
|
62
|
+
`awaitTest`; it works for jobs from either `orderTest` or `orderRetest` (or, in
|
|
63
|
+
principle, any job in the queue/claimed pipeline), since it only inspects
|
|
64
|
+
`getJobNames()`/`getReportStats()`, not job origin. Poll interval and timeout are
|
|
65
|
+
injectable via `AWAIT_JOB_POLL_MS`/`AWAIT_JOB_TIMEOUT_MS` so tests do not need to
|
|
66
|
+
wait the real ~2-4 minutes.
|
|
67
|
+
- Full REST+MCP parity: `api/routes.ts`, `index.ts`'s `/api/*` POST dispatch, and
|
|
68
|
+
`openapi.yaml` (regenerated via `npm run generate:openapi`) all cover the three new
|
|
69
|
+
operations, alongside the MCP tool registrations in `mcp.ts`. `smokeTest.ts` needed
|
|
70
|
+
no change, since it already smoke-tests the `/api/*` wildcard generically via one
|
|
71
|
+
representative path, not every individual operation.
|
|
72
|
+
- Web-UI-equivalent URL fields (`'URLs of similar requests for web users'`) are `null`
|
|
73
|
+
for `orderTest`/`orderRetest`/`awaitTest`'s own request, per explicit instruction:
|
|
74
|
+
"web users will not be offered an order option." The *ancestor* request's web URL
|
|
75
|
+
(e.g. `listReports.html`, `listIssues.html`) is still populated where one exists.
|
|
76
|
+
- `requestReferenceSchema` (`api/schemas.ts`) had to be widened from `method:
|
|
77
|
+
z.literal('GET')` to `z.enum(['GET', 'POST'])`, since `awaitTest`'s ancestors,
|
|
78
|
+
`orderTest`/`orderRetest`, are POSTs, unlike every other tool's GET ancestor.
|
|
79
|
+
- **`awaitTest` has two possible ancestors, not one**: a bug in the first cut had
|
|
80
|
+
`awaitTest.ts` unconditionally naming `orderTest` as `'closest ancestor request'`,
|
|
81
|
+
wrong for any identifier that actually came from `orderRetest`, and `awaitTest`
|
|
82
|
+
cannot tell which one produced a given `timeStamp`/`jobID`, nor does it need to (its
|
|
83
|
+
poll logic is origin-agnostic, per the earlier finding above). Considered and
|
|
84
|
+
rejected: adding an `orderType: 'test' | 'retest'` parameter so `awaitTest` could
|
|
85
|
+
report a single correct ancestor; rejected because it would force every caller to
|
|
86
|
+
track and pass through a fact `awaitTest`'s own behavior never uses, reintroducing
|
|
87
|
+
exactly the origin-coupling the generic design deliberately avoided. Adopted instead:
|
|
88
|
+
`'closest ancestor request'` reports both `orderTest` and `orderRetest` as an array,
|
|
89
|
+
only for `awaitTest` (every other tool keeps a single nullable ancestor object).
|
|
90
|
+
Implemented by giving `thisRequestSchema`/`envelope` (`api/schemas.ts`) an optional
|
|
91
|
+
`ancestorSchema` override parameter, mirroring the existing `similarWebSchema`
|
|
92
|
+
override pattern, rather than widening the shared schema for every tool.
|
|
93
|
+
- The caller's stated `reason` is persisted onto the job itself (`job.sources.reason`),
|
|
94
|
+
surviving into the eventual report exactly as `sources.worker` already does. No
|
|
95
|
+
approval rule consults it yet; the user's rationale for keeping the field even though
|
|
96
|
+
there is no human review is to accumulate real callers' stated reasons as data to
|
|
97
|
+
inform more sophisticated, reason-aware approval rules in a later iteration.
|
|
98
|
+
- Shared response-shaping helpers landed in `api/util.ts`: `getRequestFailureReason`
|
|
99
|
+
and `buildRequestDisposition`, extracted from `requestTest.ts`/`requestRetest.ts`
|
|
100
|
+
(now refactored to use them) before `orderTest.ts`/`orderRetest.ts` were written
|
|
101
|
+
against the same helpers.
|
|
102
|
+
|
|
103
|
+
## Tool 1: orderTest
|
|
104
|
+
|
|
105
|
+
- Reuses `requestTest.ts`'s existing validation (length/URL/target-safety checks
|
|
106
|
+
already in top-level `util.ts`: `checkLength`, `isURL`, `isAllowedTarget`,
|
|
107
|
+
`isAllowedRedirectTarget`).
|
|
108
|
+
- On success, enqueues directly into `db/jobs/queue` (bypassing the manual-approval
|
|
109
|
+
inbox `db/jobs/testRequests.json` entirely), following the job-template population
|
|
110
|
+
logic already in `web/enqueue/index.ts` (`jobName = ${nowStamp}-${jobIDSuffix}`,
|
|
111
|
+
etc.).
|
|
112
|
+
- Approval gate (lightweight, per the issue's own comment, no human-in-the-loop):
|
|
113
|
+
- Queue-size cap: env var **`JOB_QUEUE_MAX`** (not "instant-test"-scoped; `db/jobs/queue`
|
|
114
|
+
is a shared resource fed by manual approvals, the web UI, and this tool, so the cap
|
|
115
|
+
name must not imply a per-source quota). Checked against the live count of
|
|
116
|
+
`db/jobs/queue` at call time via the existing `queuePath()` helper, not a
|
|
117
|
+
self-maintained counter. Parallels the existing `TEST_REQUEST_QUEUE_MAX` (which caps
|
|
118
|
+
the earlier-stage pending-approval inbox), as the sibling cap one stage further down
|
|
119
|
+
the same pipeline.
|
|
120
|
+
- Dedup against `queue`/`claimed`, reusing/extending `processTestRequest`'s existing
|
|
121
|
+
per-URL/description dedup logic.
|
|
122
|
+
- Response, in all cases (accepted or rejected), returns immediately:
|
|
123
|
+
- If rejected: says why (queue full, duplicate), no identifier.
|
|
124
|
+
- If accepted: includes the job's `timeStamp`/`jobID` (or equivalent identifier),
|
|
125
|
+
states that testing has begun and the report will be available via `listIssues`
|
|
126
|
+
with those parameters, and asks the literal question: "Do you want to wait
|
|
127
|
+
(typically about 2 minutes, occasionally up to 4 minutes) and be notified of
|
|
128
|
+
completion?" The calling model decides and, if yes, makes a separate `awaitTest`
|
|
129
|
+
call.
|
|
130
|
+
|
|
131
|
+
## Tool 2: awaitTest
|
|
132
|
+
|
|
133
|
+
- Takes the identifier (`timeStamp`/`jobID`) `orderTest` returned.
|
|
134
|
+
- Blocks, polling for report completion, until the report exists or a ~4-minute cap
|
|
135
|
+
elapses.
|
|
136
|
+
- No progress notifications during the wait ("no notices until job completion or job
|
|
137
|
+
timeout", per explicit instruction). This is a pure bounded poll.
|
|
138
|
+
- On completion or timeout, returns a response pointing at the same `listIssues`
|
|
139
|
+
parameters already given, so both outcomes converge on the same follow-up action.
|
|
140
|
+
- Needs zero Testaro changes and zero MCP progress-notification plumbing: it derives
|
|
141
|
+
status purely from Kilotest's own already-tracked state (`db/jobs/claimed/` vs. a
|
|
142
|
+
completed report existing), not from any per-rule-engine signal.
|
|
143
|
+
|
|
144
|
+
## Deferred: detailed (per-rule-engine) progress reporting
|
|
145
|
+
|
|
146
|
+
Explicitly postponed; "the work is substantial even with generic progress reporting."
|
|
147
|
+
Findings, kept for whenever this is revisited:
|
|
148
|
+
|
|
149
|
+
- Testaro's `doActs.js` already has the needed hook, `opts.onProgress`, firing
|
|
150
|
+
`actStart`/`actEnd` per act (including per rule-engine `test` act, with `which`
|
|
151
|
+
naming the tool and `actEnd` carrying `outcome`/`elapsedMs`), built for Testaro's own
|
|
152
|
+
issue #44.
|
|
153
|
+
- However, **no current Testaro entry point supplies `opts`**: `call.js`, `dirWatch.js`,
|
|
154
|
+
and `netWatch.js` (the one a Kilotest Testaro worker actually runs) all call
|
|
155
|
+
`doJob(job)` with a single argument. `opts.onProgress` is dead code from any shipped
|
|
156
|
+
Testaro CLI/watch surface today.
|
|
157
|
+
- Consuming detailed progress therefore requires a **Testaro-side revision** to
|
|
158
|
+
`netWatch.js` (to accept/forward progress, plausibly keyed off a job property rather
|
|
159
|
+
than a runtime `opts` argument, since `netWatch.js`'s call site has no second argument
|
|
160
|
+
to attach a callback to) plus a **new worker-to-Kilotest channel** (the worker
|
|
161
|
+
currently only talks to Kilotest at claim-time and report-time; no mid-job channel
|
|
162
|
+
exists). This is real, separate scope: a Testaro release plus a worker deployment
|
|
163
|
+
update, before Kilotest's side could even be attempted. File as its own Testaro issue
|
|
164
|
+
when picked up, not bundled into this work.
|
|
165
|
+
- Generic progress (this plan) needed none of this, since Kilotest already knows
|
|
166
|
+
claimed-vs-reported from its own state.
|
|
167
|
+
|
|
168
|
+
## Tool 3: orderRetest
|
|
169
|
+
|
|
170
|
+
Same shape as `orderTest`, but for retesting a page with an existing report: target is
|
|
171
|
+
`{timeStamp, jobID}` of the report to retest, matching `requestRetest`'s own parameters.
|
|
172
|
+
Added at the user's explicit request, alongside full REST/MCP parity, for symmetry with
|
|
173
|
+
`orderTest`. Reuses the same `orderJob` function (see Implementation notes above) and the
|
|
174
|
+
same `awaitTest` tool for waiting; no separate wait tool was needed.
|
|
175
|
+
|
|
176
|
+
## Out of scope for this increment
|
|
177
|
+
|
|
178
|
+
- Cost recovery (explicit in the issue's own comment).
|
|
179
|
+
- Any change to the Testaro worker or its single-worker polling model.
|
|
180
|
+
- Any human-approval step for `orderTest`/`orderRetest`-originated jobs.
|
|
181
|
+
|
|
182
|
+
## Likely shared-code extraction
|
|
183
|
+
|
|
184
|
+
`requestTest.ts` and `requestRetest.ts` already duplicate a failure-reason mapping
|
|
185
|
+
(`requestResult` → human-readable string) and a `requestDisposition` envelope shape
|
|
186
|
+
(`'what happens next'` / `'how you can check for completion'` / `'how a web user can
|
|
187
|
+
check for completion'`). A third near-identical need in `orderTest.ts` makes this real
|
|
188
|
+
duplication (three call sites), worth extracting into small helpers in `api/util.ts`
|
|
189
|
+
before or alongside writing `orderTest.ts`. The outer response-body envelope
|
|
190
|
+
(`'tool collection'`, `'this request'`, etc.) is NOT worth extracting further; it is
|
|
191
|
+
already assembled from `getToolsFacts()`/`getResponseMetadata()` and the remaining
|
|
192
|
+
per-tool differences (method, URL, body shape, ancestor request) are genuinely
|
|
193
|
+
tool-specific.
|
|
194
|
+
|
|
195
|
+
See also `user_maintainer_context` for the legibility-for-future-maintainers priority
|
|
196
|
+
this design tries to respect (explicit state checks over implicit invariants, e.g. why
|
|
197
|
+
`awaitTest` needed no special-casing once the actual constraint was understood, rather
|
|
198
|
+
than adding a defensive check for a scenario that cannot occur).
|