@jrpool/kilotest 52.0.0 → 57.0.1
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/.devin/config.local.json +4 -1
- package/.github/workflows/ci.yml +3 -0
- 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/requestFeature.test.ts +36 -0
- package/api/requestFeature.ts +26 -4
- package/api/{requestTest.test.ts → requestNewTest.test.ts} +57 -23
- package/api/{requestTest.ts → requestNewTest.ts} +31 -36
- package/api/requestRetest.test.ts +24 -6
- package/api/requestRetest.ts +10 -29
- 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 +9 -6
- package/docs/AI-TOOLS.md +3 -3
- package/docs/SERVICE.md +19 -8
- package/env.example +39 -0
- package/index.test.ts +146 -24
- package/index.ts +129 -36
- package/lhm.plugin.json +1 -1
- package/linkValidation.test.ts +2 -2
- package/llms-full.txt +35 -4
- package/llms.txt +6 -3
- package/mcp.test.ts +109 -12
- package/mcp.ts +146 -26
- package/memory/MEMORY.md +1 -0
- package/memory/plan_issue_111_instant_test.md +198 -0
- package/openapi.yaml +456 -9
- package/package.json +2 -1
- package/routes.test.ts +1 -1
- package/routing.test.ts +2 -2
- package/scripts/generate-openapi.ts +1 -1
- package/server.json +1 -1
- package/sitemap.xml +1 -1
- package/smokeTest.ts +2 -2
- package/util.test.ts +486 -2
- package/util.ts +276 -45
- package/web/listReports/index.html +1 -1
- package/web/listReports/listReports.test.ts +1 -1
- package/web/{requestTest → requestNewTest}/index.ts +10 -1
- package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +18 -2
- package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
- package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
- package/web/requestRetest/index.ts +9 -0
- package/web/requestRetest/requestRetest.test.ts +14 -0
- package/web/tutorialAI/index.ts +25 -3
- package/web/tutorialAI/tutorialAI.test.ts +32 -0
- package/web/tutorialWeb/index.ts +25 -3
- package/web/tutorialWeb/tutorialWeb.test.ts +32 -0
- /package/web/{requestTest → requestNewTest}/index.html +0 -0
- /package/web/{requestTestForm → requestNewTestForm}/index.ts +0 -0
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,25 +27,52 @@ 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
|
|
41
50
|
|
|
42
51
|
export const mcpPath = '/mcp';
|
|
43
52
|
|
|
53
|
+
// Shared statement of the Kilotest domain and data hierarchy, repeated in every tool
|
|
54
|
+
// description, in the server instructions, and in the getKilotestOverview tool description,
|
|
55
|
+
// because some MCP clients do not propagate server-level instructions or the server
|
|
56
|
+
// description to their models.
|
|
57
|
+
const sharedContext = 'Kilotest tests web pages for front-end quality (accessibility, usability, and standards conformity); results are organized as report, then issue, then violator element, then diagnosis.';
|
|
58
|
+
|
|
59
|
+
// Detailed overview of the Kilotest domain, data hierarchy, and tool workflow, returned by
|
|
60
|
+
// the getKilotestOverview tool and served as the docs://kilotest/overview resource.
|
|
61
|
+
const kilotestOverview = [
|
|
62
|
+
'Kilotest tests web pages for front-end quality: accessibility, usability, and standards conformity.',
|
|
63
|
+
'Kilotest integrates an ensemble of twelve independent rule engines to test a web page and stores the results as a structured report.',
|
|
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.',
|
|
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.',
|
|
67
|
+
'More documentation is available at https://kilotest.com/llms.txt and https://kilotest.com/llms-full.txt.'
|
|
68
|
+
].join('\n\n');
|
|
69
|
+
|
|
44
70
|
// FUNCTIONS
|
|
45
71
|
|
|
72
|
+
// Returns a tool description consisting of the tool-specific summary followed by the shared
|
|
73
|
+
// Kilotest domain context.
|
|
74
|
+
const toolDoc = (summary: string): string => `${summary} ${sharedContext}`;
|
|
75
|
+
|
|
46
76
|
// Creates and returns an McpServer with Kilotest tools registered.
|
|
47
77
|
export const createMCPServer = (): McpServer => {
|
|
48
78
|
const server = new McpServer({
|
|
@@ -51,16 +81,34 @@ export const createMCPServer = (): McpServer => {
|
|
|
51
81
|
description: 'Tools that test web pages for front-end quality (accessibility, usability, and standards conformity) and make test results available'
|
|
52
82
|
},
|
|
53
83
|
{
|
|
54
|
-
instructions:
|
|
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.`
|
|
55
85
|
});
|
|
86
|
+
server.registerTool(
|
|
87
|
+
'getKilotestOverview',
|
|
88
|
+
{
|
|
89
|
+
description: toolDoc('Explain what Kilotest is, what its tools do, and how its results are structured.'),
|
|
90
|
+
inputSchema: {},
|
|
91
|
+
annotations: {
|
|
92
|
+
title: toolDoc('Explain what Kilotest is, what its tools do, and how its results are structured.'),
|
|
93
|
+
readOnlyHint: true,
|
|
94
|
+
idempotentHint: true,
|
|
95
|
+
destructiveHint: false,
|
|
96
|
+
openWorldHint: false
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async () => {
|
|
100
|
+
await recordMetric('mcpToolCalls', 'getKilotestOverview');
|
|
101
|
+
return {content: [{type: 'text', text: kilotestOverview}]};
|
|
102
|
+
}
|
|
103
|
+
);
|
|
56
104
|
server.registerTool(
|
|
57
105
|
'listReports',
|
|
58
106
|
{
|
|
59
|
-
description: 'Provide basics about all available reports.',
|
|
107
|
+
description: toolDoc('Provide basics about all available reports.'),
|
|
60
108
|
inputSchema: {},
|
|
61
109
|
outputSchema: listReportsResponseSchema,
|
|
62
110
|
annotations: {
|
|
63
|
-
title: 'Provide basics about all available reports.',
|
|
111
|
+
title: toolDoc('Provide basics about all available reports.'),
|
|
64
112
|
readOnlyHint: true,
|
|
65
113
|
idempotentHint: true,
|
|
66
114
|
destructiveHint: false,
|
|
@@ -76,11 +124,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
76
124
|
server.registerTool(
|
|
77
125
|
'listIssues',
|
|
78
126
|
{
|
|
79
|
-
description: 'Provide details about one report, including basics about the issues reported in it.',
|
|
127
|
+
description: toolDoc('Provide details about one report, including basics about the issues reported in it.'),
|
|
80
128
|
inputSchema: listIssuesSchema,
|
|
81
129
|
outputSchema: listIssuesResponseSchema,
|
|
82
130
|
annotations: {
|
|
83
|
-
title: 'Provide details about one report, including basics about the issues reported in it.',
|
|
131
|
+
title: toolDoc('Provide details about one report, including basics about the issues reported in it.'),
|
|
84
132
|
readOnlyHint: true,
|
|
85
133
|
idempotentHint: true,
|
|
86
134
|
destructiveHint: false,
|
|
@@ -96,11 +144,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
96
144
|
server.registerTool(
|
|
97
145
|
'listViolators',
|
|
98
146
|
{
|
|
99
|
-
description: 'Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.',
|
|
147
|
+
description: toolDoc('Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.'),
|
|
100
148
|
inputSchema: listViolatorsSchema,
|
|
101
149
|
outputSchema: listViolatorsResponseSchema,
|
|
102
150
|
annotations: {
|
|
103
|
-
title: 'Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.',
|
|
151
|
+
title: toolDoc('Provide details about one issue in one report, including basics about the elements of the tested page that were reported as exhibiting the issue.'),
|
|
104
152
|
readOnlyHint: true,
|
|
105
153
|
idempotentHint: true,
|
|
106
154
|
destructiveHint: false,
|
|
@@ -116,11 +164,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
116
164
|
server.registerTool(
|
|
117
165
|
'listDiagnoses',
|
|
118
166
|
{
|
|
119
|
-
description: 'Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue.',
|
|
167
|
+
description: toolDoc('Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue.'),
|
|
120
168
|
inputSchema: listDiagnosesSchema,
|
|
121
169
|
outputSchema: listDiagnosesResponseSchema,
|
|
122
170
|
annotations: {
|
|
123
|
-
title: 'Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue.',
|
|
171
|
+
title: toolDoc('Provide details about one element reported as exhibiting one issue in one report, including the diagnoses provided by rule engines about how the element exhibited the issue.'),
|
|
124
172
|
readOnlyHint: true,
|
|
125
173
|
idempotentHint: true,
|
|
126
174
|
destructiveHint: false,
|
|
@@ -136,11 +184,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
136
184
|
server.registerTool(
|
|
137
185
|
'getReport',
|
|
138
186
|
{
|
|
139
|
-
description: 'Get one full report in JSON.',
|
|
187
|
+
description: toolDoc('Get one full report in JSON.'),
|
|
140
188
|
inputSchema: getReportSchema,
|
|
141
189
|
outputSchema: getReportResponseSchema,
|
|
142
190
|
annotations: {
|
|
143
|
-
title: 'Get one full report in JSON.',
|
|
191
|
+
title: toolDoc('Get one full report in JSON.'),
|
|
144
192
|
readOnlyHint: true,
|
|
145
193
|
idempotentHint: true,
|
|
146
194
|
destructiveHint: false,
|
|
@@ -154,13 +202,13 @@ export const createMCPServer = (): McpServer => {
|
|
|
154
202
|
}
|
|
155
203
|
);
|
|
156
204
|
server.registerTool(
|
|
157
|
-
'
|
|
205
|
+
'requestNewTest',
|
|
158
206
|
{
|
|
159
|
-
description: 'Process my request to test a page about which no report is available yet.',
|
|
160
|
-
inputSchema:
|
|
161
|
-
outputSchema:
|
|
207
|
+
description: toolDoc('Process my request to test a page about which no report is available yet.'),
|
|
208
|
+
inputSchema: requestNewTestSchema,
|
|
209
|
+
outputSchema: requestNewTestResponseSchema,
|
|
162
210
|
annotations: {
|
|
163
|
-
title: 'Process my request to test a page about which no report is available yet.',
|
|
211
|
+
title: toolDoc('Process my request to test a page about which no report is available yet.'),
|
|
164
212
|
readOnlyHint: false,
|
|
165
213
|
idempotentHint: false,
|
|
166
214
|
destructiveHint: false,
|
|
@@ -168,19 +216,19 @@ export const createMCPServer = (): McpServer => {
|
|
|
168
216
|
}
|
|
169
217
|
},
|
|
170
218
|
async ({description, URL, reason}) => {
|
|
171
|
-
const result = await
|
|
172
|
-
await recordMetric('mcpToolCalls', '
|
|
219
|
+
const result = await requestNewTestAPI.response([description, URL, reason]);
|
|
220
|
+
await recordMetric('mcpToolCalls', 'requestNewTest');
|
|
173
221
|
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
174
222
|
}
|
|
175
223
|
);
|
|
176
224
|
server.registerTool(
|
|
177
225
|
'requestRetest',
|
|
178
226
|
{
|
|
179
|
-
description: 'Process my request to retest a page about which a report is available.',
|
|
227
|
+
description: toolDoc('Process my request to retest a page about which a report is available.'),
|
|
180
228
|
inputSchema: requestRetestSchema,
|
|
181
229
|
outputSchema: requestRetestResponseSchema,
|
|
182
230
|
annotations: {
|
|
183
|
-
title: 'Process my request to retest a page about which a report is available.',
|
|
231
|
+
title: toolDoc('Process my request to retest a page about which a report is available.'),
|
|
184
232
|
readOnlyHint: false,
|
|
185
233
|
idempotentHint: false,
|
|
186
234
|
destructiveHint: false,
|
|
@@ -196,11 +244,11 @@ export const createMCPServer = (): McpServer => {
|
|
|
196
244
|
server.registerTool(
|
|
197
245
|
'requestFeature',
|
|
198
246
|
{
|
|
199
|
-
description: 'Process my request to add or improve a feature.',
|
|
247
|
+
description: toolDoc('Process my request to add or improve a feature.'),
|
|
200
248
|
inputSchema: requestFeatureSchema,
|
|
201
249
|
outputSchema: requestFeatureResponseSchema,
|
|
202
250
|
annotations: {
|
|
203
|
-
title: 'Process my request to add or improve a feature.',
|
|
251
|
+
title: toolDoc('Process my request to add or improve a feature.'),
|
|
204
252
|
readOnlyHint: false,
|
|
205
253
|
idempotentHint: false,
|
|
206
254
|
destructiveHint: false,
|
|
@@ -213,6 +261,78 @@ export const createMCPServer = (): McpServer => {
|
|
|
213
261
|
return {content: [{type: 'text', text: JSON.stringify(result)}], structuredContent: result};
|
|
214
262
|
}
|
|
215
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
|
+
);
|
|
324
|
+
server.registerResource(
|
|
325
|
+
'kilotest-overview',
|
|
326
|
+
'docs://kilotest/overview',
|
|
327
|
+
{
|
|
328
|
+
title: 'Kilotest overview',
|
|
329
|
+
description: `What Kilotest is and how its tools and results are organized. ${sharedContext}`,
|
|
330
|
+
mimeType: 'text/plain'
|
|
331
|
+
},
|
|
332
|
+
async () => ({
|
|
333
|
+
contents: [{uri: 'docs://kilotest/overview', mimeType: 'text/plain', text: kilotestOverview}]
|
|
334
|
+
})
|
|
335
|
+
);
|
|
216
336
|
return server;
|
|
217
337
|
};
|
|
218
338
|
// Handles an MCP request.
|
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).
|