@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.
Files changed (59) hide show
  1. package/.devin/config.local.json +4 -1
  2. package/.github/workflows/ci.yml +3 -0
  3. package/README.md +1 -1
  4. package/api/awaitTest.test.ts +114 -0
  5. package/api/awaitTest.ts +108 -0
  6. package/api/listReports.ts +2 -2
  7. package/api/orderNewTest.test.ts +213 -0
  8. package/api/orderNewTest.ts +145 -0
  9. package/api/orderRetest.test.ts +179 -0
  10. package/api/orderRetest.ts +119 -0
  11. package/api/requestFeature.test.ts +36 -0
  12. package/api/requestFeature.ts +26 -4
  13. package/api/{requestTest.test.ts → requestNewTest.test.ts} +57 -23
  14. package/api/{requestTest.ts → requestNewTest.ts} +31 -36
  15. package/api/requestRetest.test.ts +24 -6
  16. package/api/requestRetest.ts +10 -29
  17. package/api/routes.test.ts +7 -4
  18. package/api/routes.ts +45 -7
  19. package/api/schemas.ts +120 -10
  20. package/api/util.ts +59 -2
  21. package/api/version.ts +6 -2
  22. package/capability.md +9 -6
  23. package/docs/AI-TOOLS.md +3 -3
  24. package/docs/SERVICE.md +19 -8
  25. package/env.example +39 -0
  26. package/index.test.ts +146 -24
  27. package/index.ts +129 -36
  28. package/lhm.plugin.json +1 -1
  29. package/linkValidation.test.ts +2 -2
  30. package/llms-full.txt +35 -4
  31. package/llms.txt +6 -3
  32. package/mcp.test.ts +109 -12
  33. package/mcp.ts +146 -26
  34. package/memory/MEMORY.md +1 -0
  35. package/memory/plan_issue_111_instant_test.md +198 -0
  36. package/openapi.yaml +456 -9
  37. package/package.json +2 -1
  38. package/routes.test.ts +1 -1
  39. package/routing.test.ts +2 -2
  40. package/scripts/generate-openapi.ts +1 -1
  41. package/server.json +1 -1
  42. package/sitemap.xml +1 -1
  43. package/smokeTest.ts +2 -2
  44. package/util.test.ts +486 -2
  45. package/util.ts +276 -45
  46. package/web/listReports/index.html +1 -1
  47. package/web/listReports/listReports.test.ts +1 -1
  48. package/web/{requestTest → requestNewTest}/index.ts +10 -1
  49. package/web/{requestTest/requestTest.test.ts → requestNewTest/requestNewTest.test.ts} +18 -2
  50. package/web/{requestTestForm → requestNewTestForm}/index.html +1 -1
  51. package/web/{requestTestForm/requestTestForm.test.ts → requestNewTestForm/requestNewTestForm.test.ts} +6 -6
  52. package/web/requestRetest/index.ts +9 -0
  53. package/web/requestRetest/requestRetest.test.ts +14 -0
  54. package/web/tutorialAI/index.ts +25 -3
  55. package/web/tutorialAI/tutorialAI.test.ts +32 -0
  56. package/web/tutorialWeb/index.ts +25 -3
  57. package/web/tutorialWeb/tutorialWeb.test.ts +32 -0
  58. /package/web/{requestTest → requestNewTest}/index.html +0 -0
  59. /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 requestTestAPI from './api/requestTest.ts';
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
- requestTestSchema,
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
- requestTestResponseSchema,
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: '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 requestTest tool to request that the page be tested. If the latest report about the page is obsolete, use the requestRetest tool to request that the page be retested.'
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
- 'requestTest',
205
+ 'requestNewTest',
158
206
  {
159
- description: 'Process my request to test a page about which no report is available yet.',
160
- inputSchema: requestTestSchema,
161
- outputSchema: requestTestResponseSchema,
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 requestTestAPI.response([description, URL, reason]);
172
- await recordMetric('mcpToolCalls', 'requestTest');
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).