common-qa-mcp 0.7.1 → 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 +370 -28
- 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 +36 -6
- 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/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +12 -1
- package/dist/tools/recording.tools.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 +228 -116
- 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 +314 -12
- package/dist/tools/testcase.tools.js.map +1 -1
- package/package.json +1 -1
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
|
}
|
|
@@ -82,18 +103,32 @@ async function localAgentOfflineForNow(adapter, agentId) {
|
|
|
82
103
|
function statusText(s) {
|
|
83
104
|
const unit = s.children ? 'test cases' : 'steps';
|
|
84
105
|
const prog = s.progress ? ` · ${s.progress.done}/${s.progress.total} ${unit}` : '';
|
|
106
|
+
const when = s.startedAt ? ` · started ${s.startedAt}${s.endedAt ? ` · ended ${s.endedAt}` : ''}` : '';
|
|
85
107
|
const link = s.reportUrl ? `\n${s.reportUrl}` : '';
|
|
86
|
-
const icon = (state) =>
|
|
87
|
-
|
|
88
|
-
|
|
108
|
+
const icon = (state) => state === 'passed' ? '✅'
|
|
109
|
+
: state === 'failed' ? '❌'
|
|
110
|
+
: state === 'running' ? '⏳'
|
|
111
|
+
: state === 'skipped' ? '⏭️'
|
|
112
|
+
: state === 'notExecuted' ? '➖'
|
|
113
|
+
: state === 'stopped' ? '⏹️'
|
|
114
|
+
: state === 'error' ? '⚠️'
|
|
115
|
+
: '•';
|
|
116
|
+
// Per-state tally across children (suite runs only) — EVERY state SQA actually reports gets its
|
|
117
|
+
// own bucket (skipped/notExecuted/error included, not folded into another), so the sum below is
|
|
118
|
+
// guaranteed to equal `total` by construction (every child has exactly one state). Kept concise —
|
|
119
|
+
// only non-zero buckets, in a stable order so the header reads the same every poll.
|
|
120
|
+
const isProvisional = s.state === 'running' || s.state === 'queued';
|
|
89
121
|
const tally = s.children?.length
|
|
90
122
|
? (() => {
|
|
91
|
-
const order = ['passed', 'failed', 'running', 'queued', 'stopped', 'error'];
|
|
123
|
+
const order = ['passed', 'failed', 'skipped', 'notExecuted', 'running', 'queued', 'stopped', 'error'];
|
|
92
124
|
const counts = new Map();
|
|
93
125
|
for (const c of s.children)
|
|
94
126
|
counts.set(c.state, (counts.get(c.state) ?? 0) + 1);
|
|
95
127
|
const parts = order.filter((k) => counts.get(k)).map((k) => `${counts.get(k)} ${k}`);
|
|
96
|
-
|
|
128
|
+
// One row per SUITE MEMBER (test case), never per iteration — counts below are per test
|
|
129
|
+
// case, stated explicitly since a member with multiple testdata iterations still counts once.
|
|
130
|
+
const label = isProvisional ? ' (provisional — run still in progress)' : '';
|
|
131
|
+
return parts.length ? ` · ${parts.join(', ')} [per test case]${label}` : '';
|
|
97
132
|
})()
|
|
98
133
|
: '';
|
|
99
134
|
const CHILD_CAP = 100;
|
|
@@ -112,7 +147,7 @@ function statusText(s) {
|
|
|
112
147
|
return `${stIcon} step ${st.n}: ${st.description} (${st.status})${errLine}`;
|
|
113
148
|
}).join('\n')
|
|
114
149
|
: '';
|
|
115
|
-
return `Run ${s.runId} · **${s.state}**${prog}${tally}${link}${kids}${steps}`;
|
|
150
|
+
return `Run ${s.runId} · **${s.state}**${prog}${when}${tally}${link}${kids}${steps}`;
|
|
116
151
|
}
|
|
117
152
|
export function registerRunTools(server, deps) {
|
|
118
153
|
server.registerTool('cloud_runners', {
|
|
@@ -252,18 +287,25 @@ export function registerRunTools(server, deps) {
|
|
|
252
287
|
});
|
|
253
288
|
server.registerTool('tc_run', {
|
|
254
289
|
title: 'Run a test case (local / remote / cloud; now or scheduled)',
|
|
255
|
-
description: "
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
'
|
|
260
|
-
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
'
|
|
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" +
|
|
267
309
|
" • 'local' — THIS machine's desktop agent (its own browser). Call `local_agents` (0 or 1 row per box). " +
|
|
268
310
|
"For when:'immediate' the tool first probes the desktop agent (:4040) and refuses cleanly if it's not up. " +
|
|
269
311
|
"Omit `agentId` to auto-pick this machine's agent (or pass it explicitly — must be this machine's row). " +
|
|
@@ -273,54 +315,91 @@ export function registerRunTools(server, deps) {
|
|
|
273
315
|
"remote agents are eligible; for when:'schedule' ANY remote agent works (it runs when it fires).\n" +
|
|
274
316
|
" • 'cloud' — an online cloud runner VM (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
275
317
|
'from cloud_runners; for a schedule omit it to let the backend auto-pick a live VM at fire time.\n' +
|
|
276
|
-
'
|
|
277
|
-
'if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
278
|
-
'`when` decides WHEN it runs (this tool REFUSES when `when` is omitted — ASK the user):\n' +
|
|
279
|
-
" • 'immediate' — dispatch now; returns the run id — poll run_status for the result.\n" +
|
|
280
|
-
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
281
|
-
'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' +
|
|
282
319
|
'NOTE: MANUAL test cases cannot be run or scheduled by an agent — the backend rejects them.',
|
|
283
320
|
inputSchema: {
|
|
284
321
|
tcId: z.string().describe('The test case id (or business key) to run.'),
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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."),
|
|
292
336
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
293
|
-
|
|
294
|
-
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`."),
|
|
295
|
-
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."),
|
|
296
|
-
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."),
|
|
297
|
-
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."),
|
|
298
|
-
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)."),
|
|
299
|
-
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')."),
|
|
300
338
|
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
301
339
|
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
302
340
|
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
303
341
|
},
|
|
304
342
|
annotations: { readOnlyHint: false },
|
|
305
|
-
}, 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 }) => {
|
|
306
344
|
const adapter = deps.getAdapter();
|
|
307
345
|
if (!adapter)
|
|
308
346
|
return err(NOT_CONNECTED);
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
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).
|
|
312
390
|
if (releaseId === undefined) {
|
|
313
391
|
return err("ASK the user which RELEASE this run belongs to (from list_releases), then call tc_run " +
|
|
314
392
|
"again with releaseId. If the user wants no release, pass releaseId: '' (empty string).");
|
|
315
393
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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).");
|
|
319
399
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
//
|
|
323
|
-
// 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.
|
|
324
403
|
let preview;
|
|
325
404
|
try {
|
|
326
405
|
preview = await adapter.getTestCase(tcId);
|
|
@@ -343,30 +422,19 @@ export function registerRunTools(server, deps) {
|
|
|
343
422
|
}
|
|
344
423
|
versionId = '';
|
|
345
424
|
}
|
|
425
|
+
// STEP 6 — environment.
|
|
346
426
|
if (!environment) {
|
|
347
427
|
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
348
428
|
'tc_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
349
429
|
'field with a red asterisk.');
|
|
350
430
|
}
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// 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.
|
|
354
433
|
if (!isMobile && !browser) {
|
|
355
434
|
return err("ASK the user which BROWSER to run in (chromium / chrome / msedge / firefox / webkit), " +
|
|
356
435
|
'then call tc_run again with browser set. Required — the UI marks this field with a ' +
|
|
357
436
|
'red asterisk.');
|
|
358
437
|
}
|
|
359
|
-
if (!mode) {
|
|
360
|
-
return err("Specify where to run: call tc_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
361
|
-
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
362
|
-
"remote runs on a teammate's registered agent, cloud runs on an online cloud runner VM.");
|
|
363
|
-
}
|
|
364
|
-
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
365
|
-
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
366
|
-
if (!when) {
|
|
367
|
-
return err("Specify WHEN to run: call tc_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
368
|
-
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
369
|
-
}
|
|
370
438
|
let resolvedRunner;
|
|
371
439
|
if (mode === 'cloud') {
|
|
372
440
|
if (!adapter.listCloudRunners)
|
|
@@ -470,6 +538,8 @@ export function registerRunTools(server, deps) {
|
|
|
470
538
|
const where = mode === 'cloud'
|
|
471
539
|
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
472
540
|
: `agent \`${agentId}\``;
|
|
541
|
+
if (projectId)
|
|
542
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId });
|
|
473
543
|
return ok(`Scheduled ${tcId} (${scheduleType ?? 'once'}, ${mode}) for ${scheduledFor}${next} → ${where}. Schedule id \`${sched.id}\`.`, { schedule: sched, mode, when: 'schedule', ...(pinnedRunner ? { runner: pinnedRunner } : {}) });
|
|
474
544
|
}
|
|
475
545
|
catch (e) {
|
|
@@ -498,6 +568,8 @@ export function registerRunTools(server, deps) {
|
|
|
498
568
|
: mode === 'remote'
|
|
499
569
|
? `on remote agent \`${agentId}\``
|
|
500
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 });
|
|
501
573
|
return ok(`Started ${tcId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
502
574
|
}
|
|
503
575
|
catch (e) {
|
|
@@ -506,15 +578,24 @@ export function registerRunTools(server, deps) {
|
|
|
506
578
|
});
|
|
507
579
|
server.registerTool('suite_run', {
|
|
508
580
|
title: 'Run a test suite (local / remote / cloud; now or scheduled)',
|
|
509
|
-
description:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
'
|
|
513
|
-
|
|
514
|
-
'
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
"
|
|
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). " +
|
|
518
599
|
'`mode` decides WHERE it runs:\n' +
|
|
519
600
|
" • 'local' — THIS machine's desktop agent, always serial. Call `local_agents` (0 or 1 row). For " +
|
|
520
601
|
"when:'immediate' the tool probes :4040 and refuses if the desktop agent is down. Omit `agentId` to " +
|
|
@@ -524,66 +605,90 @@ export function registerRunTools(server, deps) {
|
|
|
524
605
|
" • 'cloud' — online cloud runner VM(s) (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
525
606
|
'from cloud_runners; for a schedule omit it to auto-pick a live VM at fire time.\n' +
|
|
526
607
|
'For cloud, if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
527
|
-
'
|
|
528
|
-
" • 'immediate' — dispatch now; returns the suite-run id — poll run_status for progress " +
|
|
529
|
-
'(per-test-case results under `children`) and run_stop to cancel.\n' +
|
|
530
|
-
" • 'schedule' — create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
531
|
-
'end date and timezone BEFORE offering an agent. Returns the schedule id.\n' +
|
|
532
|
-
'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).',
|
|
533
609
|
inputSchema: {
|
|
534
610
|
suiteId: z.string().describe('The suite id to run (from suite_list / suite_get).'),
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
mode: z.enum(['local', 'remote', 'cloud']).optional().describe('
|
|
538
|
-
|
|
539
|
-
|
|
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.'),
|
|
540
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."),
|
|
541
618
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
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')."),
|
|
548
625
|
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
549
626
|
endDate: z.string().optional().describe('For a recurring schedule — ISO end date (required for hourly/daily/weekdays).'),
|
|
550
627
|
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
551
628
|
},
|
|
552
629
|
annotations: { readOnlyHint: false },
|
|
553
|
-
}, 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 }) => {
|
|
554
631
|
const adapter = deps.getAdapter();
|
|
555
632
|
if (!adapter)
|
|
556
633
|
return err(NOT_CONNECTED);
|
|
557
|
-
//
|
|
558
|
-
//
|
|
559
|
-
//
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
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) {
|
|
563
675
|
return err("ASK the user which RELEASE this suite run belongs to (from list_releases), then call " +
|
|
564
|
-
|
|
676
|
+
"suite_run again with releaseId. If the user wants no release, pass releaseId: '' (empty string).");
|
|
565
677
|
}
|
|
566
|
-
|
|
678
|
+
// STEP 4 — sprint. Only asked when release is non-empty.
|
|
679
|
+
if (releaseId !== '' && sprintId === undefined) {
|
|
567
680
|
return err("ASK the user which SPRINT this suite run belongs to (from list_sprints, filtered by " +
|
|
568
|
-
|
|
569
|
-
'
|
|
681
|
+
"releaseId), then call suite_run again with sprintId. If the user wants no sprint, pass " +
|
|
682
|
+
"sprintId: '' (empty string).");
|
|
570
683
|
}
|
|
684
|
+
if (releaseId === '')
|
|
685
|
+
sprintId = '';
|
|
686
|
+
// STEP 5 — environment.
|
|
571
687
|
if (!environment) {
|
|
572
688
|
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
573
689
|
'suite_run again with environment set to the chosen env key. Required — the UI marks this ' +
|
|
574
690
|
'field with a red asterisk for a suite.');
|
|
575
691
|
}
|
|
576
|
-
if (!mode) {
|
|
577
|
-
return err("Specify where to run: call suite_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
578
|
-
"If the user has not said which, ASK them first — local runs on THIS machine's agent, " +
|
|
579
|
-
"remote runs on a teammate's registered agent, cloud runs on online cloud runner VM(s).");
|
|
580
|
-
}
|
|
581
|
-
// Never assume now-vs-scheduled — it decides which agents are eligible (online-only for now,
|
|
582
|
-
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
583
|
-
if (!when) {
|
|
584
|
-
return err("Specify WHEN to run: call suite_run again with when: 'immediate' (run now) or when: 'schedule' " +
|
|
585
|
-
'(create a schedule). If the user has not said, ask them first — never assume.');
|
|
586
|
-
}
|
|
587
692
|
let resolvedRunner;
|
|
588
693
|
if (mode === 'cloud') {
|
|
589
694
|
if (!adapter.listCloudRunners)
|
|
@@ -657,6 +762,8 @@ export function registerRunTools(server, deps) {
|
|
|
657
762
|
const where = mode === 'cloud'
|
|
658
763
|
? (pinnedRunner ? `cloud runner **${pinnedRunner.name}**` : 'cloud (auto-pick a live VM at run time)')
|
|
659
764
|
: `agent \`${agentId}\``;
|
|
765
|
+
if (projectId)
|
|
766
|
+
lastRun.set(projectId, { when, mode, releaseId, sprintId, environment, browser, agentId, cloudRunnerId });
|
|
660
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 } : {}) });
|
|
661
768
|
}
|
|
662
769
|
catch (e) {
|
|
@@ -683,6 +790,8 @@ export function registerRunTools(server, deps) {
|
|
|
683
790
|
: mode === 'remote'
|
|
684
791
|
? `on remote agent \`${agentId}\``
|
|
685
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 });
|
|
686
795
|
return ok(`Started suite ${suiteId} ${where}. Run id \`${run.runId}\` — poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
687
796
|
}
|
|
688
797
|
catch (e) {
|
|
@@ -691,11 +800,14 @@ export function registerRunTools(server, deps) {
|
|
|
691
800
|
});
|
|
692
801
|
server.registerTool('run_status', {
|
|
693
802
|
title: 'Get a run status',
|
|
694
|
-
description: 'Read the status of a run — state (queued/running/passed/failed/stopped/
|
|
695
|
-
'per-test-case rollup for a SUITE run (capped at 100 shown
|
|
696
|
-
'
|
|
697
|
-
'
|
|
698
|
-
'
|
|
803
|
+
description: 'Read the status of a run — state (queued/running/passed/failed/skipped/notExecuted/stopped/' +
|
|
804
|
+
'error), progress, per-test-case rollup for a SUITE run (capped at 100 shown, ONE ROW PER ' +
|
|
805
|
+
'TEST CASE — not per iteration), and a link. Every state SQA reports is its own bucket in the ' +
|
|
806
|
+
'per-state tally (skipped and notExecuted are real, distinct outcomes — never counted as ' +
|
|
807
|
+
'passed or folded into queued). While the run is still queued/running, the tally is labelled ' +
|
|
808
|
+
'provisional. Pass EITHER `runId` (from tc_run / suite_run) or `suiteId` (from suite_list) to ' +
|
|
809
|
+
'read that suite\'s LATEST execution.\n\n' +
|
|
810
|
+
'The suite rollup only says pass/fail/etc per test case, never WHY — to get one child\'s own ' +
|
|
699
811
|
'step-level detail (which step failed, the error), call again with the SAME `runId`/`suiteId` ' +
|
|
700
812
|
'plus `tcId` set to one of the ids shown in the rollup. Requires a live connection.',
|
|
701
813
|
inputSchema: {
|