common-qa-mcp 0.8.0 → 0.8.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/dist/adapters/remoteMcp/adapter.js +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +24 -0
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +360 -25
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +2 -2
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +257 -64
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/domain/types.d.ts +31 -4
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/domain/validate.d.ts.map +1 -1
- package/dist/domain/validate.js +5 -1
- package/dist/domain/validate.js.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +18 -11
- package/dist/initAgents.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +6 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/run.tools.d.ts +4 -0
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +200 -105
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/schemas.d.ts +41 -49
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +19 -1
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/tcEditPublisher.d.ts +20 -0
- package/dist/tools/tcEditPublisher.d.ts.map +1 -0
- package/dist/tools/tcEditPublisher.js +46 -0
- package/dist/tools/tcEditPublisher.js.map +1 -0
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +299 -3
- package/dist/tools/testcase.tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,10 @@ import type { ProviderAdapter } from '../adapters/types.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export type RunToolsDeps = {
|
|
10
10
|
getAdapter: () => ProviderAdapter | null;
|
|
11
|
+
/** Current project id, used to key the in-session "reuse last run config?" prompt. When absent
|
|
12
|
+
* or returning null, the reuse prompt is skipped entirely (tests that don't wire it get the raw
|
|
13
|
+
* cascade). */
|
|
14
|
+
getProjectId?: () => string | null;
|
|
11
15
|
};
|
|
12
16
|
export declare function registerRunTools(server: McpServer, deps: RunToolsDeps): void;
|
|
13
17
|
//# sourceMappingURL=run.tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.tools.d.ts","sourceRoot":"","sources":["../../src/tools/run.tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"run.tools.d.ts","sourceRoot":"","sources":["../../src/tools/run.tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,UAAU,EAAE,MAAM,eAAe,GAAG,IAAI,CAAC;IACzC;;oBAEgB;IAChB,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACpC,CAAC;AAyKF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAq6B5E"}
|
package/dist/tools/run.tools.js
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { ok, err } from '../mcpResult.js';
|
|
3
3
|
const NOT_CONNECTED = 'Not connected to a product. Use qa_connect({ url, token, projectId }) first — running a test needs a live backend connection.';
|
|
4
|
+
const lastRun = new Map();
|
|
5
|
+
function summarizeLastRun(c) {
|
|
6
|
+
const parts = [];
|
|
7
|
+
if (c.when)
|
|
8
|
+
parts.push(`when=${c.when}`);
|
|
9
|
+
if (c.mode)
|
|
10
|
+
parts.push(`mode=${c.mode}`);
|
|
11
|
+
if (c.releaseId !== undefined)
|
|
12
|
+
parts.push(`release=${c.releaseId || '(none)'}`);
|
|
13
|
+
if (c.sprintId !== undefined)
|
|
14
|
+
parts.push(`sprint=${c.sprintId || '(none)'}`);
|
|
15
|
+
if (c.environment)
|
|
16
|
+
parts.push(`env=${c.environment}`);
|
|
17
|
+
if (c.browser)
|
|
18
|
+
parts.push(`browser=${c.browser}`);
|
|
19
|
+
if (c.agentId)
|
|
20
|
+
parts.push(`agent=${c.agentId}`);
|
|
21
|
+
if (c.cloudRunnerId)
|
|
22
|
+
parts.push(`runner=${c.cloudRunnerId}`);
|
|
23
|
+
return parts.join(', ');
|
|
24
|
+
}
|
|
4
25
|
function describeError(e) {
|
|
5
26
|
return e instanceof Error ? e.message : String(e);
|
|
6
27
|
}
|
|
@@ -266,18 +287,25 @@ export function registerRunTools(server, deps) {
|
|
|
266
287
|
});
|
|
267
288
|
server.registerTool('tc_run', {
|
|
268
289
|
title: 'Run a test case (local / remote / cloud; now or scheduled)',
|
|
269
|
-
description: "
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
'
|
|
274
|
-
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
'
|
|
290
|
+
description: "★ ORDER OF QUESTIONS (STRICT — applies whether you ask one at a time OR bundle several into a " +
|
|
291
|
+
"single message): when → mode → release → sprint → version → environment → browser/device → " +
|
|
292
|
+
"schedule fields → agent. Ask in THIS order, always — never schema-declaration order, never " +
|
|
293
|
+
"refusal-arrival order, never a 'natural UI' order (release first is WRONG). Skip a step only " +
|
|
294
|
+
"when the flag below says it's skippable (sprint when release='', version when TC has one " +
|
|
295
|
+
"version, browser vs. device by TC type). ★\n\n" +
|
|
296
|
+
"Execute ONE existing test case in the connected product. ASK the user for EVERY field the " +
|
|
297
|
+
"Execute dialog asks — never assume — in this DEPENDENCY ORDER: (0) if a previous run in this " +
|
|
298
|
+
"session left a config for the project, the tool refuses first asking `reuseConfig` (true=merge " +
|
|
299
|
+
"remembered mode/release/sprint/env/browser/agent, false=start fresh); (1) `when` " +
|
|
300
|
+
"immediate/schedule, (2) `mode` local/remote/cloud, (3) `releaseId` (from list_releases; pass " +
|
|
301
|
+
"'' to skip — no release), (4) `sprintId` (from list_sprints; skipped entirely when " +
|
|
302
|
+
"releaseId=''), (5) `versionId` (from tc_get's `versions[]`; pass '' for current), (6) " +
|
|
303
|
+
"`environment` (from tc_list_environments), (7) FOR WEB TCs: `browser`; FOR ANDROID/IOS TCs: " +
|
|
304
|
+
"`deviceType` + `deviceId` (via `mobile_list_devices`), (8) for a schedule the start time + " +
|
|
305
|
+
"recurrence + timezone, (9) THEN offer the eligible agent/runner and pass the pick. IF YOU " +
|
|
306
|
+
"BATCH SEVERAL REFUSED FIELDS INTO ONE AskUserQuestion, ORDER the questions EXACTLY as above " +
|
|
307
|
+
"(when → mode → release → sprint → version → environment → browser/device → schedule fields " +
|
|
308
|
+
"→ agent) — never in schema order or refusal order. `mode` decides WHERE it runs:\n" +
|
|
281
309
|
" • 'local' — THIS machine's desktop agent (its own browser). Call `local_agents` (0 or 1 row per box). " +
|
|
282
310
|
"For when:'immediate' the tool first probes the desktop agent (:4040) and refuses cleanly if it's not up. " +
|
|
283
311
|
"Omit `agentId` to auto-pick this machine's agent (or pass it explicitly — must be this machine's row). " +
|
|
@@ -287,54 +315,91 @@ export function registerRunTools(server, deps) {
|
|
|
287
315
|
"remote agents are eligible; for when:'schedule' ANY remote agent works (it runs when it fires).\n" +
|
|
288
316
|
" • 'cloud' — an online cloud runner VM (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
289
317
|
'from cloud_runners; for a schedule omit it to let the backend auto-pick a live VM at fire time.\n' +
|
|
290
|
-
'
|
|
291
|
-
'if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
292
|
-
'`when` decides WHEN it runs (this tool REFUSES when `when` is omitted — ASK the user):\n' +
|
|
293
|
-
" • 'immediate' — dispatch now; returns the run id — poll run_status for the result.\n" +
|
|
294
|
-
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
295
|
-
'end date and timezone BEFORE offering an agent. Returns the schedule id.\n' +
|
|
318
|
+
'For cloud, if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
296
319
|
'NOTE: MANUAL test cases cannot be run or scheduled by an agent — the backend rejects them.',
|
|
297
320
|
inputSchema: {
|
|
298
321
|
tcId: z.string().describe('The test case id (or business key) to run.'),
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
322
|
+
reuseConfig: z.boolean().optional().describe("Set only when the tool refused asking about REUSE. true = merge the previous run's mode/release/sprint/env/browser/agent/runner into any unset field; false = ignore prior config and walk the full cascade. Omit on a first run."),
|
|
323
|
+
when: z.enum(['immediate', 'schedule']).optional().describe("STEP 1 — when to run. REQUIRED, ASK the user if unstated (the tool refuses when omitted). 'immediate' dispatches now; 'schedule' creates a schedule (needs scheduledFor)."),
|
|
324
|
+
mode: z.enum(['local', 'remote', 'cloud']).optional().describe("STEP 2 — where to run. REQUIRED, ASK the user if unstated."),
|
|
325
|
+
releaseId: z.string().optional().describe("STEP 3 — tag the run with a RELEASE (from list_releases). Optional in the UI; pass '' after asking to skip. The tool refuses when undefined so the user is asked."),
|
|
326
|
+
sprintId: z.string().optional().describe("STEP 4 — tag the run with a SPRINT (from list_sprints, scoped to release). Only asked when releaseId is non-empty; skipped entirely when releaseId=''. Pass '' after asking to skip."),
|
|
327
|
+
versionId: z.string().optional().describe("STEP 5 — override the TC's currently-selected VERSION for this run (an id or name from the TC's `versions[]`). Pass '' to accept the TC's current version."),
|
|
328
|
+
environment: z.string().optional().describe('STEP 6 — environment key (from tc_list_environments). ASK the user; the UI marks this required.'),
|
|
329
|
+
browser: z.string().optional().describe("STEP 7 (WEB TCs) — browser, default 'chromium' (bundled + isolated). Others: 'chrome'/'msedge' (installed channel), 'firefox', 'webkit'. NOTE: a schedule accepts only chromium/msedge/firefox/webkit."),
|
|
330
|
+
deviceId: z.string().optional().describe("STEP 7 (ANDROID/IOS TCs) — the target device id. LOCAL: adb serial or emulator name. CLOUD: the marketplace device name. Enumerate with `mobile_list_devices`."),
|
|
331
|
+
deviceType: z.enum(['real_device', 'emulator', 'simulator', 'cloud_device', 'cloud_emulator', 'cloud_simulator']).optional().describe("STEP 7 (ANDROID/IOS TCs) — the KIND of device the picked `deviceId` is (read off the matching row from `mobile_list_devices`)."),
|
|
332
|
+
cloudDeviceType: z.enum(['lambda_test', 'browser_stack', 'sauce_labs', 'simplify_qa_device_form']).optional().describe("REQUIRED when `deviceType` is cloud_* — which cloud farm the device came from (read off the matching row's `cloudDeviceType`)."),
|
|
333
|
+
osVersion: z.string().optional().describe("Optional OS version to stamp on the wire platform object for ANDROID/IOS — read off the matching row from `mobile_list_devices`."),
|
|
334
|
+
agentId: z.string().optional().describe("STEP 9 — for local: a credential_id from `local_agents` (must be THIS machine's row; omit to auto-pick). For remote: REQUIRED — a credential_id from `remote_agents`. when:'immediate' → the picked agent must be ONLINE; when:'schedule' → offline OK and `agentId` is REQUIRED for both local and remote."),
|
|
335
|
+
cloudRunnerId: z.string().optional().describe("STEP 9 (cloud) — a specific ONLINE runner id (from cloud_runners). For when:'schedule' omit to auto-pick a live VM at fire time."),
|
|
306
336
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
307
|
-
|
|
308
|
-
deviceId: z.string().optional().describe("REQUIRED for ANDROID/IOS test cases — the target device id. LOCAL: adb serial or emulator name. CLOUD: the marketplace device name. Enumerate with `mobile_list_devices`."),
|
|
309
|
-
deviceType: z.enum(['real_device', 'emulator', 'simulator', 'cloud_device', 'cloud_emulator', 'cloud_simulator']).optional().describe("REQUIRED for ANDROID/IOS test cases — the KIND of device the picked `deviceId` is (read off the matching row from `mobile_list_devices`). Drives the wire execution platform's `type` field."),
|
|
310
|
-
cloudDeviceType: z.enum(['lambda_test', 'browser_stack', 'sauce_labs', 'simplify_qa_device_form']).optional().describe("REQUIRED when `deviceType` is cloud_* — which cloud farm the device came from (read off the matching row's `cloudDeviceType` from `mobile_list_devices`). Note SIMPLIFY_QA_DEVICE_FORM's legacy 'FORM' spelling — mirrors the SQA wire schema exactly."),
|
|
311
|
-
osVersion: z.string().optional().describe("Optional OS version to stamp on the wire platform object for ANDROID/IOS — read off the matching row from `mobile_list_devices`. Ignored for non-mobile tcTypes."),
|
|
312
|
-
when: z.enum(['immediate', 'schedule']).optional().describe("When to run — REQUIRED, ASK the user if unstated (the tool refuses when omitted). 'immediate' dispatches now; 'schedule' creates a schedule (needs scheduledFor)."),
|
|
313
|
-
scheduledFor: z.string().optional().describe("REQUIRED for when:'schedule' — ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
337
|
+
scheduledFor: z.string().optional().describe("STEP 8 — REQUIRED for when:'schedule'; ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
314
338
|
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
315
339
|
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
316
340
|
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
317
341
|
},
|
|
318
342
|
annotations: { readOnlyHint: false },
|
|
319
|
-
}, async ({ tcId, mode, agentId, environment, browser, headless, cloudRunnerId, deviceId, deviceType, cloudDeviceType, osVersion,
|
|
343
|
+
}, async ({ tcId, reuseConfig, when, mode, agentId, environment, browser, headless, cloudRunnerId, deviceId, deviceType, cloudDeviceType, osVersion, scheduledFor, scheduleType, endDate, timezone, releaseId, sprintId, versionId }) => {
|
|
320
344
|
const adapter = deps.getAdapter();
|
|
321
345
|
if (!adapter)
|
|
322
346
|
return err(NOT_CONNECTED);
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
347
|
+
// STEP 0 — reuse-last-config prompt. Fires only when the caller wired getProjectId AND there
|
|
348
|
+
// is a remembered config for that project AND the caller has not answered yet (undefined).
|
|
349
|
+
// Merging when `reuseConfig: true` fills only fields left unset by this call — the user can
|
|
350
|
+
// still override any single field explicitly. TC-specific fields (versionId, deviceId,
|
|
351
|
+
// schedule cadence) are never remembered — they belong to the run, not the environment.
|
|
352
|
+
const projectId = deps.getProjectId?.() ?? null;
|
|
353
|
+
const remembered = projectId ? lastRun.get(projectId) : undefined;
|
|
354
|
+
if (remembered && reuseConfig === undefined) {
|
|
355
|
+
return err(`Previous run in this project used: ${summarizeLastRun(remembered)}. ` +
|
|
356
|
+
'ASK the user whether to reuse it — call tc_run again with `reuseConfig: true` to merge ' +
|
|
357
|
+
'those defaults, or `reuseConfig: false` to start fresh and be asked each field.');
|
|
358
|
+
}
|
|
359
|
+
if (reuseConfig === true && remembered) {
|
|
360
|
+
if (when === undefined)
|
|
361
|
+
when = remembered.when;
|
|
362
|
+
if (mode === undefined)
|
|
363
|
+
mode = remembered.mode;
|
|
364
|
+
if (releaseId === undefined)
|
|
365
|
+
releaseId = remembered.releaseId;
|
|
366
|
+
if (sprintId === undefined)
|
|
367
|
+
sprintId = remembered.sprintId;
|
|
368
|
+
if (environment === undefined)
|
|
369
|
+
environment = remembered.environment;
|
|
370
|
+
if (browser === undefined)
|
|
371
|
+
browser = remembered.browser;
|
|
372
|
+
if (agentId === undefined)
|
|
373
|
+
agentId = remembered.agentId;
|
|
374
|
+
if (cloudRunnerId === undefined)
|
|
375
|
+
cloudRunnerId = remembered.cloudRunnerId;
|
|
376
|
+
}
|
|
377
|
+
// STEP 1 — when. Decides schedule vs immediate; gates the schedule fields and the agent
|
|
378
|
+
// online-check downstream, so ask before anything else.
|
|
379
|
+
if (!when) {
|
|
380
|
+
return err("Specify WHEN to run: call tc_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
381
|
+
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
382
|
+
}
|
|
383
|
+
// STEP 2 — mode. Decides the agent/runner pool and constrains the browser set for a schedule.
|
|
384
|
+
if (!mode) {
|
|
385
|
+
return err("Specify where to run: call tc_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
386
|
+
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
387
|
+
"remote runs on a teammate's registered agent, cloud runs on an online cloud runner VM.");
|
|
388
|
+
}
|
|
389
|
+
// STEP 3 — release. Optional in the UI; the tool asks anyway (pass '' to skip).
|
|
326
390
|
if (releaseId === undefined) {
|
|
327
391
|
return err("ASK the user which RELEASE this run belongs to (from list_releases), then call tc_run " +
|
|
328
392
|
"again with releaseId. If the user wants no release, pass releaseId: '' (empty string).");
|
|
329
393
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
394
|
+
// STEP 4 — sprint. Only asked when a release was picked; when release is skipped, sprint is
|
|
395
|
+
// skipped entirely (sprints scope to a release in the UI).
|
|
396
|
+
if (releaseId !== '' && sprintId === undefined) {
|
|
397
|
+
return err("ASK the user which SPRINT this run belongs to (from list_sprints, filtered by releaseId), " +
|
|
398
|
+
"then call tc_run again with sprintId. If the user wants no sprint, pass sprintId: '' (empty string).");
|
|
333
399
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
// discovery.tools.ts:226; without this the model guesses a version NAME and the backend 500s.
|
|
400
|
+
if (releaseId === '')
|
|
401
|
+
sprintId = '';
|
|
402
|
+
// STEP 5 — version. Needs the TC's tcType and version list, so pre-fetch once here.
|
|
338
403
|
let preview;
|
|
339
404
|
try {
|
|
340
405
|
preview = await adapter.getTestCase(tcId);
|
|
@@ -357,30 +422,19 @@ export function registerRunTools(server, deps) {
|
|
|
357
422
|
}
|
|
358
423
|
versionId = '';
|
|
359
424
|
}
|
|
425
|
+
// STEP 6 — environment.
|
|
360
426
|
if (!environment) {
|
|
361
427
|
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
362
428
|
'tc_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
363
429
|
'field with a red asterisk.');
|
|
364
430
|
}
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// buildRunPlatform will ignore any browser passed alongside a mobile deviceId.
|
|
431
|
+
// STEP 7 (web only) — browser. Mobile branch handled after mode-dependent cloud/agent
|
|
432
|
+
// resolution, since the device roster is filtered by where it will run.
|
|
368
433
|
if (!isMobile && !browser) {
|
|
369
434
|
return err("ASK the user which BROWSER to run in (chromium / chrome / msedge / firefox / webkit), " +
|
|
370
435
|
'then call tc_run again with browser set. Required — the UI marks this field with a ' +
|
|
371
436
|
'red asterisk.');
|
|
372
437
|
}
|
|
373
|
-
if (!mode) {
|
|
374
|
-
return err("Specify where to run: call tc_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
375
|
-
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
376
|
-
"remote runs on a teammate's registered agent, cloud runs on an online cloud runner VM.");
|
|
377
|
-
}
|
|
378
|
-
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
379
|
-
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
380
|
-
if (!when) {
|
|
381
|
-
return err("Specify WHEN to run: call tc_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
382
|
-
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
383
|
-
}
|
|
384
438
|
let resolvedRunner;
|
|
385
439
|
if (mode === 'cloud') {
|
|
386
440
|
if (!adapter.listCloudRunners)
|
|
@@ -484,6 +538,8 @@ export function registerRunTools(server, deps) {
|
|
|
484
538
|
const where = mode === 'cloud'
|
|
485
539
|
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
486
540
|
: `agent \`${agentId}\``;
|
|
541
|
+
if (projectId)
|
|
542
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId });
|
|
487
543
|
return ok(`Scheduled ${tcId} (${scheduleType ?? 'once'}, ${mode}) for ${scheduledFor}${next} → ${where}. Schedule id \`${sched.id}\`.`, { schedule: sched, mode, when: 'schedule', ...(pinnedRunner ? { runner: pinnedRunner } : {}) });
|
|
488
544
|
}
|
|
489
545
|
catch (e) {
|
|
@@ -512,6 +568,8 @@ export function registerRunTools(server, deps) {
|
|
|
512
568
|
: mode === 'remote'
|
|
513
569
|
? `on remote agent \`${agentId}\``
|
|
514
570
|
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
571
|
+
if (projectId)
|
|
572
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId: resolvedRunner?.id ?? cloudRunnerId });
|
|
515
573
|
return ok(`Started ${tcId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
516
574
|
}
|
|
517
575
|
catch (e) {
|
|
@@ -520,15 +578,24 @@ export function registerRunTools(server, deps) {
|
|
|
520
578
|
});
|
|
521
579
|
server.registerTool('suite_run', {
|
|
522
580
|
title: 'Run a test suite (local / remote / cloud; now or scheduled)',
|
|
523
|
-
description:
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
'
|
|
527
|
-
|
|
528
|
-
'
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
"
|
|
581
|
+
description: "★ ORDER OF QUESTIONS (STRICT — applies whether you ask one at a time OR bundle several into a " +
|
|
582
|
+
"single message): when → mode → release → sprint → environment → schedule fields → agent. Ask " +
|
|
583
|
+
"in THIS order, always — never schema-declaration order, never refusal-arrival order, never " +
|
|
584
|
+
"a 'natural UI' order (release first is WRONG). Skip sprint when release=''. Suites do NOT " +
|
|
585
|
+
"ask version / browser / device — each TC in the suite carries its own. ★\n\n" +
|
|
586
|
+
'Execute an existing test SUITE — all its member test cases, in order — in the connected product. ' +
|
|
587
|
+
"ASK the user for EVERY field the UI's Execute-suite dialog asks — never assume — in this " +
|
|
588
|
+
"DEPENDENCY ORDER: (0) if a previous run in this session left a config for the project, the tool " +
|
|
589
|
+
"refuses first asking `reuseConfig` (true=merge remembered mode/release/sprint/env/agent, " +
|
|
590
|
+
"false=start fresh); (1) `when` immediate/schedule, (2) `mode` local/remote/cloud, (3) " +
|
|
591
|
+
"`releaseId` (from list_releases; pass '' to skip), (4) `sprintId` (from list_sprints; skipped " +
|
|
592
|
+
"entirely when releaseId=''), (5) `environment` (from tc_list_environments), (6) for a schedule " +
|
|
593
|
+
"the start time + recurrence + timezone, (7) THEN offer the eligible agent/runner. IF YOU " +
|
|
594
|
+
"BATCH SEVERAL REFUSED FIELDS INTO ONE AskUserQuestion, ORDER the questions EXACTLY as above " +
|
|
595
|
+
"(when → mode → release → sprint → environment → schedule fields → agent) — never in schema " +
|
|
596
|
+
"order or refusal order. UNLIKE " +
|
|
597
|
+
"`tc_run`, a suite does NOT ask for `versionId` or `browser` — each TC in the suite carries its " +
|
|
598
|
+
"own pinned version + execution platform (that's what the suite editor picks per row). " +
|
|
532
599
|
'`mode` decides WHERE it runs:\n' +
|
|
533
600
|
" • 'local' — THIS machine's desktop agent, always serial. Call `local_agents` (0 or 1 row). For " +
|
|
534
601
|
"when:'immediate' the tool probes :4040 and refuses if the desktop agent is down. Omit `agentId` to " +
|
|
@@ -538,66 +605,90 @@ export function registerRunTools(server, deps) {
|
|
|
538
605
|
" • 'cloud' — online cloud runner VM(s) (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
539
606
|
'from cloud_runners; for a schedule omit it to auto-pick a live VM at fire time.\n' +
|
|
540
607
|
'For cloud, if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
541
|
-
'
|
|
542
|
-
" • 'immediate' — dispatch now; returns the suite-run id — poll run_status for progress " +
|
|
543
|
-
'(per-test-case results under `children`) and run_stop to cancel.\n' +
|
|
544
|
-
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
545
|
-
'end date and timezone BEFORE offering an agent. Returns the schedule id.\n' +
|
|
546
|
-
'NOTE: a suite of only MANUAL test cases cannot be run or scheduled by an agent — the backend rejects it.',
|
|
608
|
+
'NOTE: manual suites go through the same dispatch — the backend stores them as an execution (no agent picks them up until steps are marked from the UI).',
|
|
547
609
|
inputSchema: {
|
|
548
610
|
suiteId: z.string().describe('The suite id to run (from suite_list / suite_get).'),
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
mode: z.enum(['local', 'remote', 'cloud']).optional().describe('
|
|
552
|
-
|
|
553
|
-
|
|
611
|
+
reuseConfig: z.boolean().optional().describe("Set only when the tool refused asking about REUSE. true = merge the previous run's mode/release/sprint/env/browser/agent/runner into any unset field; false = ignore prior config. Omit on a first run."),
|
|
612
|
+
when: z.enum(['immediate', 'schedule']).optional().describe("STEP 1 — when to run. REQUIRED, ASK the user if unstated."),
|
|
613
|
+
mode: z.enum(['local', 'remote', 'cloud']).optional().describe('STEP 2 — where to run. REQUIRED, ASK the user if unstated.'),
|
|
614
|
+
releaseId: z.string().optional().describe("STEP 3 — release from list_releases. ASK the user; pass '' to skip (no release / no sprint asked)."),
|
|
615
|
+
sprintId: z.string().optional().describe("STEP 4 — sprint from list_sprints (scoped to release). Only asked when releaseId is non-empty; skipped when releaseId=''. Pass '' to skip."),
|
|
616
|
+
environment: z.string().optional().describe('STEP 5 — environment key (from tc_list_environments). ASK the user; the UI marks this required for a suite.'),
|
|
554
617
|
browser: z.string().optional().describe("Optional at the SUITE level — each TC in the suite has its own execution platform (that's the suite-editor pick per row). Provide only to OVERRIDE for every WEB TC. Values: 'chromium' (default), 'chrome'/'msedge', 'firefox', 'webkit'. NOTE: a schedule accepts only chromium/msedge/firefox/webkit."),
|
|
555
618
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
scheduledFor: z.string().optional().describe("REQUIRED for when:'schedule'
|
|
619
|
+
agentId: z.string().optional().describe("STEP 7 — for local: a credential_id from `local_agents` (this machine only; omit to auto-pick). For remote: REQUIRED — a credential_id from `remote_agents`. when:'immediate' → picked agent must be ONLINE; when:'schedule' → offline OK, agentId REQUIRED for both local and remote."),
|
|
620
|
+
cloudRunnerId: z.string().optional().describe("STEP 7 (cloud) — a specific ONLINE runner id (from cloud_runners). For when:'schedule' omit to auto-pick a live VM at fire time."),
|
|
621
|
+
strategy: z.enum(['serial', 'parallel']).optional().describe("Dispatch order across the suite's test cases. Wire only accepts SERIAL / PARALLEL (backend validator). Default 'serial'; LOCAL is always serial regardless."),
|
|
622
|
+
maxConcurrent: z.number().int().positive().optional().describe('Cloud parallel only — maps to globalConfiguration.MAXIMUM_CONTAINER_COUNT on the wire.'),
|
|
623
|
+
terminateOnFailure: z.boolean().optional().describe('Stop dispatching remaining test cases after the first failure. Maps to globalConfiguration.TERMINATE_ON_FAIL on the wire.'),
|
|
624
|
+
scheduledFor: z.string().optional().describe("STEP 6 — REQUIRED for when:'schedule'; ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
562
625
|
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
563
626
|
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
564
627
|
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
565
628
|
},
|
|
566
629
|
annotations: { readOnlyHint: false },
|
|
567
|
-
}, async ({ suiteId, releaseId, sprintId, mode, agentId, environment, browser, headless, cloudRunnerId, strategy, maxConcurrent, terminateOnFailure, when, scheduledFor, scheduleType, endDate, timezone }) => {
|
|
630
|
+
}, async ({ suiteId, reuseConfig, releaseId, sprintId, mode, agentId, environment, browser, headless, cloudRunnerId, strategy, maxConcurrent, terminateOnFailure, when, scheduledFor, scheduleType, endDate, timezone }) => {
|
|
568
631
|
const adapter = deps.getAdapter();
|
|
569
632
|
if (!adapter)
|
|
570
633
|
return err(NOT_CONNECTED);
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (
|
|
634
|
+
// STEP 0 — reuse-last-config prompt. Same shape as tc_run: fires only when the caller wired
|
|
635
|
+
// getProjectId AND there is a remembered config AND the user hasn't answered yet. Merges the
|
|
636
|
+
// environment-level fields (mode/release/sprint/env/browser/agent/runner) into any left unset.
|
|
637
|
+
const projectId = deps.getProjectId?.() ?? null;
|
|
638
|
+
const remembered = projectId ? lastRun.get(projectId) : undefined;
|
|
639
|
+
if (remembered && reuseConfig === undefined) {
|
|
640
|
+
return err(`Previous run in this project used: ${summarizeLastRun(remembered)}. ` +
|
|
641
|
+
'ASK the user whether to reuse it — call suite_run again with `reuseConfig: true` to merge ' +
|
|
642
|
+
'those defaults, or `reuseConfig: false` to start fresh.');
|
|
643
|
+
}
|
|
644
|
+
if (reuseConfig === true && remembered) {
|
|
645
|
+
if (when === undefined)
|
|
646
|
+
when = remembered.when;
|
|
647
|
+
if (mode === undefined)
|
|
648
|
+
mode = remembered.mode;
|
|
649
|
+
if (releaseId === undefined)
|
|
650
|
+
releaseId = remembered.releaseId;
|
|
651
|
+
if (sprintId === undefined)
|
|
652
|
+
sprintId = remembered.sprintId;
|
|
653
|
+
if (environment === undefined)
|
|
654
|
+
environment = remembered.environment;
|
|
655
|
+
if (browser === undefined)
|
|
656
|
+
browser = remembered.browser;
|
|
657
|
+
if (agentId === undefined)
|
|
658
|
+
agentId = remembered.agentId;
|
|
659
|
+
if (cloudRunnerId === undefined)
|
|
660
|
+
cloudRunnerId = remembered.cloudRunnerId;
|
|
661
|
+
}
|
|
662
|
+
// STEP 1 — when.
|
|
663
|
+
if (!when) {
|
|
664
|
+
return err("Specify WHEN to run: call suite_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
665
|
+
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
666
|
+
}
|
|
667
|
+
// STEP 2 — mode.
|
|
668
|
+
if (!mode) {
|
|
669
|
+
return err("Specify where to run: call suite_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
670
|
+
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
671
|
+
"remote runs on a teammate's registered agent, cloud runs on online cloud runner VM(s).");
|
|
672
|
+
}
|
|
673
|
+
// STEP 3 — release. Unified with tc_run: pass '' to skip (matches the UI's optional field).
|
|
674
|
+
if (releaseId === undefined) {
|
|
577
675
|
return err("ASK the user which RELEASE this suite run belongs to (from list_releases), then call " +
|
|
578
|
-
|
|
676
|
+
"suite_run again with releaseId. If the user wants no release, pass releaseId: '' (empty string).");
|
|
579
677
|
}
|
|
580
|
-
|
|
678
|
+
// STEP 4 — sprint. Only asked when release is non-empty.
|
|
679
|
+
if (releaseId !== '' && sprintId === undefined) {
|
|
581
680
|
return err("ASK the user which SPRINT this suite run belongs to (from list_sprints, filtered by " +
|
|
582
|
-
|
|
583
|
-
'
|
|
681
|
+
"releaseId), then call suite_run again with sprintId. If the user wants no sprint, pass " +
|
|
682
|
+
"sprintId: '' (empty string).");
|
|
584
683
|
}
|
|
684
|
+
if (releaseId === '')
|
|
685
|
+
sprintId = '';
|
|
686
|
+
// STEP 5 — environment.
|
|
585
687
|
if (!environment) {
|
|
586
688
|
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
587
689
|
'suite_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
588
690
|
'field with a red asterisk for a suite.');
|
|
589
691
|
}
|
|
590
|
-
if (!mode) {
|
|
591
|
-
return err("Specify where to run: call suite_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
592
|
-
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
593
|
-
"remote runs on a teammate's registered agent, cloud runs on online cloud runner VM(s).");
|
|
594
|
-
}
|
|
595
|
-
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
596
|
-
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
597
|
-
if (!when) {
|
|
598
|
-
return err("Specify WHEN to run: call suite_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
599
|
-
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
600
|
-
}
|
|
601
692
|
let resolvedRunner;
|
|
602
693
|
if (mode === 'cloud') {
|
|
603
694
|
if (!adapter.listCloudRunners)
|
|
@@ -671,6 +762,8 @@ export function registerRunTools(server, deps) {
|
|
|
671
762
|
const where = mode === 'cloud'
|
|
672
763
|
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
673
764
|
: `agent \`${agentId}\``;
|
|
765
|
+
if (projectId)
|
|
766
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId });
|
|
674
767
|
return ok(`Scheduled suite ${suiteId} (${scheduleType ?? 'once'}, ${mode}) for ${scheduledFor}${next} → ${where}. Schedule id \`${sched.id}\`.`, { schedule: sched, mode, when: 'schedule', ...(pinnedRunner ? { runner: pinnedRunner } : {}) });
|
|
675
768
|
}
|
|
676
769
|
catch (e) {
|
|
@@ -697,6 +790,8 @@ export function registerRunTools(server, deps) {
|
|
|
697
790
|
: mode === 'remote'
|
|
698
791
|
? `on remote agent \`${agentId}\``
|
|
699
792
|
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
793
|
+
if (projectId)
|
|
794
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId: resolvedRunner?.id ?? cloudRunnerId });
|
|
700
795
|
return ok(`Started suite ${suiteId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
701
796
|
}
|
|
702
797
|
catch (e) {
|