common-qa-mcp 0.4.1 โ 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/remoteMcp/adapter.d.ts +4 -1
- package/dist/adapters/remoteMcp/adapter.d.ts.map +1 -1
- package/dist/adapters/remoteMcp/adapter.js +8 -3
- package/dist/adapters/remoteMcp/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/adapter.d.ts +130 -36
- package/dist/adapters/simplifyqa/adapter.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/adapter.js +833 -265
- package/dist/adapters/simplifyqa/adapter.js.map +1 -1
- package/dist/adapters/simplifyqa/mapper.d.ts +5 -1
- package/dist/adapters/simplifyqa/mapper.d.ts.map +1 -1
- package/dist/adapters/simplifyqa/mapper.js +35 -1
- package/dist/adapters/simplifyqa/mapper.js.map +1 -1
- package/dist/adapters/types.d.ts +47 -10
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/adapters/walnut/actionTypes.d.ts +1 -1
- package/dist/adapters/walnut/actionTypes.d.ts.map +1 -1
- package/dist/adapters/walnut/actionTypes.js +37 -0
- package/dist/adapters/walnut/actionTypes.js.map +1 -1
- package/dist/adapters/walnut/mapper.d.ts.map +1 -1
- package/dist/adapters/walnut/mapper.js +12 -1
- package/dist/adapters/walnut/mapper.js.map +1 -1
- package/dist/domain/types.d.ts +98 -17
- package/dist/domain/types.d.ts.map +1 -1
- package/dist/initAgents.d.ts.map +1 -1
- package/dist/initAgents.js +31 -11
- package/dist/initAgents.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/tools/backlog.tools.d.ts.map +1 -1
- package/dist/tools/backlog.tools.js +154 -26
- package/dist/tools/backlog.tools.js.map +1 -1
- package/dist/tools/connection.tools.d.ts.map +1 -1
- package/dist/tools/connection.tools.js +15 -3
- package/dist/tools/connection.tools.js.map +1 -1
- package/dist/tools/defects.tools.d.ts.map +1 -1
- package/dist/tools/defects.tools.js +29 -3
- package/dist/tools/defects.tools.js.map +1 -1
- package/dist/tools/discovery.tools.d.ts.map +1 -1
- package/dist/tools/discovery.tools.js +304 -137
- package/dist/tools/discovery.tools.js.map +1 -1
- package/dist/tools/markdown.d.ts.map +1 -1
- package/dist/tools/markdown.js +9 -5
- package/dist/tools/markdown.js.map +1 -1
- package/dist/tools/objects.tools.d.ts.map +1 -1
- package/dist/tools/objects.tools.js +111 -6
- package/dist/tools/objects.tools.js.map +1 -1
- package/dist/tools/recording.tools.d.ts.map +1 -1
- package/dist/tools/recording.tools.js +108 -10
- package/dist/tools/recording.tools.js.map +1 -1
- package/dist/tools/run.tools.d.ts.map +1 -1
- package/dist/tools/run.tools.js +263 -63
- package/dist/tools/run.tools.js.map +1 -1
- package/dist/tools/tasks.tools.d.ts.map +1 -1
- package/dist/tools/tasks.tools.js +29 -3
- package/dist/tools/tasks.tools.js.map +1 -1
- package/dist/tools/testcase.tools.d.ts.map +1 -1
- package/dist/tools/testcase.tools.js +5 -2
- package/dist/tools/testcase.tools.js.map +1 -1
- package/package.json +1 -1
package/dist/tools/run.tools.js
CHANGED
|
@@ -14,6 +14,14 @@ function localAgentLine(a) {
|
|
|
14
14
|
const host = a.host && a.host !== a.name ? ` ยท ${a.host}` : '';
|
|
15
15
|
return `- ${a.online ? '๐ข' : 'โช'} ${a.name ?? a.id}${host} \`${a.id}\`${a.status ? ` (${a.status})` : ''}`;
|
|
16
16
|
}
|
|
17
|
+
function mobileDeviceLine(d) {
|
|
18
|
+
// `active` here is the last-known connection flag from the SQA DB (stale by design). Show it as
|
|
19
|
+
// a hint, not a gate โ a device the user just plugged in may still read `active:false` here.
|
|
20
|
+
const dot = d.active ? '๐ข' : 'โช';
|
|
21
|
+
const os = d.osVersion ? ` ยท ${d.platform.toLowerCase()} ${d.osVersion}` : ` ยท ${d.platform.toLowerCase()}`;
|
|
22
|
+
const status = d.status ? ` (${d.status})` : '';
|
|
23
|
+
return `- ${dot} ${d.name}${os} \`${d.id}\`${status}`;
|
|
24
|
+
}
|
|
17
25
|
/** One line per schedule for management listings โ what it runs, cadence, where + pinned agent,
|
|
18
26
|
* browser, next run, status; the credential-free id in backticks is what the management tools take. */
|
|
19
27
|
function scheduleLine(s) {
|
|
@@ -52,6 +60,12 @@ async function localAgentOfflineForNow(adapter, agentId) {
|
|
|
52
60
|
try {
|
|
53
61
|
const agents = await adapter.listLocalAgents();
|
|
54
62
|
const picked = agents.find((a) => a.id === agentId);
|
|
63
|
+
// If the picked agent physically belongs to this machine (hostname / MAC / IP matched), the
|
|
64
|
+
// adapter's DB-flag override in listLocalAgents already set `online: true`. Never refuse a
|
|
65
|
+
// run-now for our OWN machine โ the DB flag is stale by design (7s cron in the executions
|
|
66
|
+
// service that we cannot touch).
|
|
67
|
+
if (picked?.isThisMachine)
|
|
68
|
+
return null;
|
|
55
69
|
if (picked && !picked.online) {
|
|
56
70
|
const online = agents.filter((a) => a.online);
|
|
57
71
|
return (`Agent \`${agentId}\`${picked.name ? ` (${picked.name})` : ''} is offline โ a run-now needs an ONLINE agent. ` +
|
|
@@ -82,8 +96,12 @@ function statusText(s) {
|
|
|
82
96
|
return parts.length ? ` ยท ${parts.join(', ')}` : '';
|
|
83
97
|
})()
|
|
84
98
|
: '';
|
|
85
|
-
const
|
|
86
|
-
|
|
99
|
+
const CHILD_CAP = 100;
|
|
100
|
+
const total = s.children?.length ?? 0;
|
|
101
|
+
const shown = s.children?.slice(0, CHILD_CAP) ?? [];
|
|
102
|
+
const kids = shown.length
|
|
103
|
+
? '\n\n' + shown.map((c) => `- ${icon(c.state)} ${c.id}${c.title ? ` โ ${c.title}` : ''} (${c.state})`).join('\n') +
|
|
104
|
+
(total > CHILD_CAP ? `\nโฆ and ${total - CHILD_CAP} more (${total} total)` : '')
|
|
87
105
|
: '';
|
|
88
106
|
return `Run ${s.runId} ยท **${s.state}**${prog}${tally}${link}${kids}`;
|
|
89
107
|
}
|
|
@@ -114,13 +132,12 @@ export function registerRunTools(server, deps) {
|
|
|
114
132
|
}
|
|
115
133
|
});
|
|
116
134
|
server.registerTool('local_agents', {
|
|
117
|
-
title:
|
|
118
|
-
description:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
'registration + a stale cron, so it lags behind). Other agents\' status still comes from the DB.',
|
|
135
|
+
title: "List THIS machine's local agent(s)",
|
|
136
|
+
description: "List the LOCAL agent(s) registered from THIS machine โ the desktop agent running on the box " +
|
|
137
|
+
"this MCP is on. Use for a LOCAL tc_run / suite_run. There is exactly one desktop agent per " +
|
|
138
|
+
"machine, so this is 0 or 1 row: empty means no agent is registered from this box (start the " +
|
|
139
|
+
"SimplifyQA desktop agent, or use mode:'remote' to dispatch to a teammate's). Use `remote_agents` " +
|
|
140
|
+
"for the other registered machines. Requires a live connection.",
|
|
124
141
|
inputSchema: {},
|
|
125
142
|
annotations: { readOnlyHint: true },
|
|
126
143
|
}, async () => {
|
|
@@ -130,11 +147,43 @@ export function registerRunTools(server, deps) {
|
|
|
130
147
|
if (!adapter.listLocalAgents)
|
|
131
148
|
return err('This product has no local agents to list.');
|
|
132
149
|
try {
|
|
133
|
-
const
|
|
150
|
+
const all = await adapter.listLocalAgents();
|
|
151
|
+
const agents = all.filter((a) => a.isThisMachine);
|
|
152
|
+
if (!agents.length) {
|
|
153
|
+
return ok("No local agent is registered from this machine. Start the SimplifyQA desktop agent on " +
|
|
154
|
+
"this box, or use remote_agents + mode:'remote' to run on a teammate's machine.", { agents: [] });
|
|
155
|
+
}
|
|
134
156
|
const online = agents.filter((a) => a.online);
|
|
157
|
+
const text = `${agents.length} local agent${agents.length === 1 ? '' : 's'} on this machine ยท ${online.length} online:\n\n` +
|
|
158
|
+
agents.map(localAgentLine).join('\n');
|
|
159
|
+
return ok(text, { agents, online: online.length });
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
return err(describeError(e));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
server.registerTool('remote_agents', {
|
|
166
|
+
title: 'List REMOTE agents (other registered machines)',
|
|
167
|
+
description: "List the LOCAL agents registered on OTHER machines (teammates' boxes) โ the pool a REMOTE " +
|
|
168
|
+
"run dispatches to. Use to pick an `agentId` for a REMOTE tc_run / suite_run. Excludes this " +
|
|
169
|
+
"machine's own agent (that's `local_agents`). REMOTE dispatches to a specific teammate's " +
|
|
170
|
+
"registered desktop agent through the SimplifyQA queue; the picked agent's browsers / adb " +
|
|
171
|
+
"devices are what actually execute. Requires a live connection.",
|
|
172
|
+
inputSchema: {},
|
|
173
|
+
annotations: { readOnlyHint: true },
|
|
174
|
+
}, async () => {
|
|
175
|
+
const adapter = deps.getAdapter();
|
|
176
|
+
if (!adapter)
|
|
177
|
+
return err(NOT_CONNECTED);
|
|
178
|
+
if (!adapter.listLocalAgents)
|
|
179
|
+
return err('This product has no local agents to list.');
|
|
180
|
+
try {
|
|
181
|
+
const all = await adapter.listLocalAgents();
|
|
182
|
+
const agents = all.filter((a) => !a.isThisMachine);
|
|
135
183
|
if (!agents.length)
|
|
136
|
-
return ok('No
|
|
137
|
-
const
|
|
184
|
+
return ok('No other machines are registered as agents for this customer.', { agents: [] });
|
|
185
|
+
const online = agents.filter((a) => a.online);
|
|
186
|
+
const text = `${agents.length} remote agent${agents.length === 1 ? '' : 's'} ยท ${online.length} online:\n\n` +
|
|
138
187
|
agents.map(localAgentLine).join('\n');
|
|
139
188
|
return ok(text, { agents, online: online.length });
|
|
140
189
|
}
|
|
@@ -142,24 +191,81 @@ export function registerRunTools(server, deps) {
|
|
|
142
191
|
return err(describeError(e));
|
|
143
192
|
}
|
|
144
193
|
});
|
|
194
|
+
server.registerTool('mobile_list_devices', {
|
|
195
|
+
title: 'List mobile devices (Android or iOS)',
|
|
196
|
+
description: "List MOBILE devices available to the project โ union of the LOCAL roster (real devices + " +
|
|
197
|
+
"emulators/simulators registered via the desktop agent, from `/tm/mobileDevices/search`) " +
|
|
198
|
+
"and the CLOUD roster (whichever farm the project has integrated: LambdaTest / " +
|
|
199
|
+
"BrowserStack / SauceLabs / SimplifyQA Device Farm โ the first non-empty one wins, " +
|
|
200
|
+
"matching the UI's single-provider-per-project model). Use before `tc_run` for an ANDROID " +
|
|
201
|
+
"or IOS test case to pick a `deviceId` (and read `deviceType` + `cloudDeviceType` off the " +
|
|
202
|
+
"same row). The `active` flag on local rows is last-known: stale by design (the SQA " +
|
|
203
|
+
"backend writes it when the desktop agent connects/disconnects, and a 7s-stale cron flips " +
|
|
204
|
+
"it back); a device the user just plugged in may still read `active:false` here. Cloud " +
|
|
205
|
+
"rows don't carry `active` โ cloud farms are always assumed reachable. Requires a live " +
|
|
206
|
+
"connection.",
|
|
207
|
+
inputSchema: {
|
|
208
|
+
platform: z.enum(['android', 'ios']).describe("Which mobile platform to list devices for."),
|
|
209
|
+
},
|
|
210
|
+
annotations: { readOnlyHint: true },
|
|
211
|
+
}, async ({ platform }) => {
|
|
212
|
+
const adapter = deps.getAdapter();
|
|
213
|
+
if (!adapter)
|
|
214
|
+
return err(NOT_CONNECTED);
|
|
215
|
+
if (!adapter.listMobileDevices)
|
|
216
|
+
return err('This product has no mobile device roster to list.');
|
|
217
|
+
try {
|
|
218
|
+
const devices = await adapter.listMobileDevices(platform.toUpperCase());
|
|
219
|
+
if (!devices.length) {
|
|
220
|
+
return ok(`No ${platform.toUpperCase()} devices are registered to this project. Connect a device ` +
|
|
221
|
+
`via the SimplifyQA desktop agent, then try again.`, { devices: [] });
|
|
222
|
+
}
|
|
223
|
+
// Group by deviceType so the client can present the same tabs the UI does (Real / Emulator
|
|
224
|
+
// / Simulator / Cloud Device). Cloud rows come from the configured farm (LT / BS / SL / SQA
|
|
225
|
+
// Device Farm); the row's `cloudDeviceType` names which one โ pass it back on `tc_run`.
|
|
226
|
+
const groups = {};
|
|
227
|
+
for (const d of devices) {
|
|
228
|
+
if (!groups[d.deviceType])
|
|
229
|
+
groups[d.deviceType] = [];
|
|
230
|
+
groups[d.deviceType].push(d);
|
|
231
|
+
}
|
|
232
|
+
const active = devices.filter((d) => d.active).length;
|
|
233
|
+
const sections = Object.entries(groups)
|
|
234
|
+
.map(([type, rows]) => `**${type}** (${rows.length})\n${rows.map(mobileDeviceLine).join('\n')}`)
|
|
235
|
+
.join('\n\n');
|
|
236
|
+
const text = `${devices.length} ${platform.toUpperCase()} device${devices.length === 1 ? '' : 's'} ยท ${active} active:\n\n` +
|
|
237
|
+
sections;
|
|
238
|
+
return ok(text, { devices, active });
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
return err(describeError(e));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
145
244
|
server.registerTool('tc_run', {
|
|
146
|
-
title: 'Run a test case (local
|
|
245
|
+
title: 'Run a test case (local / remote / cloud; now or scheduled)',
|
|
147
246
|
description: "Execute ONE existing test case in the connected product. ASK the user for EVERY field the " +
|
|
148
247
|
"Execute dialog asks โ never assume โ and in ORDER: (1) `releaseId` (from list_releases; " +
|
|
149
248
|
"pass '' if the user skips), (2) `sprintId` (from list_sprints; pass '' if the user skips), " +
|
|
150
249
|
"(3) `versionId` (from tc_get's `versions[]`; pass '' to accept the TC's current version), " +
|
|
151
|
-
'(4) `environment` (from tc_list_environments โ required), (5) `browser`
|
|
152
|
-
'(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
'
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
250
|
+
'(4) `environment` (from tc_list_environments โ required), (5) FOR WEB TCs: `browser` ' +
|
|
251
|
+
'(required); FOR ANDROID/IOS TCs: skip browser โ `deviceType` + `deviceId` are asked in step ' +
|
|
252
|
+
"(9) instead (call `mobile_list_devices` to enumerate), (6) `mode` local/remote/cloud, (7) " +
|
|
253
|
+
"`when` immediate/schedule, (8) for a schedule the start time + recurrence + timezone, (9) " +
|
|
254
|
+
'THEN offer the eligible agent/runner and (for mobile) the device โ pass the picks. The tool ' +
|
|
255
|
+
'REFUSES when any of (1)-(7) is omitted, and additionally when `deviceType`/`deviceId` are ' +
|
|
256
|
+
'omitted on a mobile TC โ that is deliberate; it forces the ask instead of silently ' +
|
|
257
|
+
'defaulting. `mode` decides WHERE it runs:\n' +
|
|
258
|
+
" โข 'local' โ THIS machine's desktop agent (its own browser). Call `local_agents` (0 or 1 row per box). " +
|
|
259
|
+
"For when:'immediate' the tool first probes the desktop agent (:4040) and refuses cleanly if it's not up. " +
|
|
260
|
+
"Omit `agentId` to auto-pick this machine's agent (or pass it explicitly โ must be this machine's row). " +
|
|
261
|
+
"For when:'schedule' `agentId` is REQUIRED.\n" +
|
|
262
|
+
" โข 'remote' โ ANOTHER registered machine's desktop agent (a teammate's box). Call `remote_agents` and " +
|
|
263
|
+
"OFFER the list; `agentId` is REQUIRED (no auto-pick across boxes). For when:'immediate' only ONLINE " +
|
|
264
|
+
"remote agents are eligible; for when:'schedule' ANY remote agent works (it runs when it fires).\n" +
|
|
159
265
|
" โข 'cloud' โ an online cloud runner VM (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
160
266
|
'from cloud_runners; for a schedule omit it to let the backend auto-pick a live VM at fire time.\n' +
|
|
161
|
-
'IF THE USER HAS NOT SAID local or cloud, ASK โ this tool refuses when `mode` is omitted. For cloud,
|
|
162
|
-
'no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
267
|
+
'IF THE USER HAS NOT SAID local, remote, or cloud, ASK โ this tool refuses when `mode` is omitted. For cloud, ' +
|
|
268
|
+
'if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
163
269
|
'`when` decides WHEN it runs (this tool REFUSES when `when` is omitted โ ASK the user):\n' +
|
|
164
270
|
" โข 'immediate' โ dispatch now; returns the run id โ poll run_status for the result.\n" +
|
|
165
271
|
" โข 'schedule' โ create a schedule instead (requires `scheduledFor`); ASK the start time, recurrence, " +
|
|
@@ -167,8 +273,8 @@ export function registerRunTools(server, deps) {
|
|
|
167
273
|
'NOTE: MANUAL test cases cannot be run or scheduled by an agent โ the backend rejects them.',
|
|
168
274
|
inputSchema: {
|
|
169
275
|
tcId: z.string().describe('The test case id (or business key) to run.'),
|
|
170
|
-
mode: z.enum(['local', 'cloud']).optional().describe("Where to run. REQUIRED โ ASK the user if unstated (the tool refuses when omitted)."),
|
|
171
|
-
agentId: z.string().optional().describe("For local: a specific agent credential_id
|
|
276
|
+
mode: z.enum(['local', 'remote', 'cloud']).optional().describe("Where to run. REQUIRED โ ASK the user if unstated (the tool refuses when omitted)."),
|
|
277
|
+
agentId: z.string().optional().describe("For local: a specific agent credential_id from `local_agents` (must be THIS machine's row; omit to auto-pick). For remote: REQUIRED โ a credential_id from `remote_agents` (a teammate's row). when:'immediate' โ the picked agent must be ONLINE; when:'schedule' โ offline agents are OK (they run when they fire) and `agentId` is REQUIRED for both local and remote."),
|
|
172
278
|
releaseId: z.string().optional().describe('Tag the run with a RELEASE (from list_releases). Optional in the UI; omit to leave untagged.'),
|
|
173
279
|
sprintId: z.string().optional().describe('Tag the run with a SPRINT (from list_sprints). Optional in the UI; omit to leave untagged.'),
|
|
174
280
|
versionId: z.string().optional().describe("Override the TC's currently-selected VERSION for this run (an id from the TC's `versions[]`). Omit to run the current version โ that's what the UI's Version chip defaults to."),
|
|
@@ -176,7 +282,10 @@ export function registerRunTools(server, deps) {
|
|
|
176
282
|
browser: z.string().optional().describe("Browser, default 'chromium' (bundled + isolated). Others: 'chrome'/'msedge' (installed channel), 'firefox', 'webkit'. NOTE: a schedule accepts only chromium/msedge/firefox/webkit."),
|
|
177
283
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
178
284
|
cloudRunnerId: z.string().optional().describe("For cloud: a specific ONLINE runner id (from cloud_runners). For when:'schedule' omit to auto-pick a live VM at fire time."),
|
|
179
|
-
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
|
|
285
|
+
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`."),
|
|
286
|
+
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."),
|
|
287
|
+
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."),
|
|
288
|
+
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."),
|
|
180
289
|
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)."),
|
|
181
290
|
scheduledFor: z.string().optional().describe("REQUIRED for when:'schedule' โ ISO datetime of the first/only run (e.g. '2026-08-20T09:00:00Z')."),
|
|
182
291
|
scheduleType: z.enum(['once', 'hourly', 'daily', 'weekdays', 'weekly', 'monthly']).optional().describe("Recurrence for a schedule. Default 'once'. hourly/daily/weekdays require an endDate."),
|
|
@@ -184,7 +293,7 @@ export function registerRunTools(server, deps) {
|
|
|
184
293
|
timezone: z.string().optional().describe("IANA timezone for a schedule (e.g. 'Asia/Kolkata'). Default 'UTC'."),
|
|
185
294
|
},
|
|
186
295
|
annotations: { readOnlyHint: false },
|
|
187
|
-
}, async ({ tcId, mode, agentId, environment, browser, headless, cloudRunnerId, deviceId, when, scheduledFor, scheduleType, endDate, timezone, releaseId, sprintId, versionId }) => {
|
|
296
|
+
}, async ({ tcId, mode, agentId, environment, browser, headless, cloudRunnerId, deviceId, deviceType, cloudDeviceType, osVersion, when, scheduledFor, scheduleType, endDate, timezone, releaseId, sprintId, versionId }) => {
|
|
188
297
|
const adapter = deps.getAdapter();
|
|
189
298
|
if (!adapter)
|
|
190
299
|
return err(NOT_CONNECTED);
|
|
@@ -199,25 +308,49 @@ export function registerRunTools(server, deps) {
|
|
|
199
308
|
return err("ASK the user which SPRINT this run belongs to (from list_sprints), then call tc_run " +
|
|
200
309
|
"again with sprintId. If the user wants no sprint, pass sprintId: '' (empty string).");
|
|
201
310
|
}
|
|
311
|
+
// Pre-flight the TC once โ needed both to inline the version list (below) and to know the
|
|
312
|
+
// tcType (WEB vs ANDROID/IOS/โฆ) so the refuse cascade asks the right platform-specific
|
|
313
|
+
// fields: browser for WEB, deviceType + deviceId for mobile. Mirrors tc_get's refusal at
|
|
314
|
+
// discovery.tools.ts:226; without this the model guesses a version NAME and the backend 500s.
|
|
315
|
+
let preview;
|
|
316
|
+
try {
|
|
317
|
+
preview = await adapter.getTestCase(tcId);
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
return err(describeError(e));
|
|
321
|
+
}
|
|
322
|
+
const tcType = (preview.technologyType ?? 'WEB').toUpperCase();
|
|
323
|
+
const isMobile = tcType === 'ANDROID' || tcType === 'IOS';
|
|
202
324
|
if (versionId === undefined) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
325
|
+
const vs = preview.versions ?? [];
|
|
326
|
+
if (vs.length > 1) {
|
|
327
|
+
const listing = vs
|
|
328
|
+
.map((v) => ` โข \`${v.id}\` โ **${v.name}**${v.isCurrent ? ' (current)' : ''}`)
|
|
329
|
+
.join('\n');
|
|
330
|
+
return err(`Test case ${preview.id ?? tcId} has ${vs.length} versions โ ASK the user which ` +
|
|
331
|
+
`one to run, then call tc_run again with versionId (pass the version id OR its ` +
|
|
332
|
+
`name โ either resolves). Pass versionId: '' to accept the TC's current ` +
|
|
333
|
+
`version.\n\n${listing}`);
|
|
334
|
+
}
|
|
335
|
+
versionId = '';
|
|
206
336
|
}
|
|
207
337
|
if (!environment) {
|
|
208
338
|
return err('ASK the user which ENVIRONMENT to run against (from tc_list_environments), then call ' +
|
|
209
339
|
'tc_run again with environment set to the chosen env key. Required โ the UI marks this ' +
|
|
210
340
|
'field with a red asterisk.');
|
|
211
341
|
}
|
|
212
|
-
|
|
342
|
+
// Browser is a WEB-only field โ the SQA Execute dialog doesn't render it for mobile TCs
|
|
343
|
+
// (they show a device picker instead). Skip the refuse for ANDROID/IOS; the adapter's
|
|
344
|
+
// buildRunPlatform will ignore any browser passed alongside a mobile deviceId.
|
|
345
|
+
if (!isMobile && !browser) {
|
|
213
346
|
return err("ASK the user which BROWSER to run in (chromium / chrome / msedge / firefox / webkit), " +
|
|
214
347
|
'then call tc_run again with browser set. Required โ the UI marks this field with a ' +
|
|
215
348
|
'red asterisk.');
|
|
216
349
|
}
|
|
217
350
|
if (!mode) {
|
|
218
|
-
return err("Specify where to run: call tc_run again with mode: 'local' or 'cloud'. " +
|
|
219
|
-
|
|
220
|
-
'cloud runs on an online cloud runner VM.
|
|
351
|
+
return err("Specify where to run: call tc_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
352
|
+
"If the user has not said which, ASK them first โ local runs on THIS machine's agent, " +
|
|
353
|
+
"remote runs on a teammate's registered agent, cloud runs on an online cloud runner VM.");
|
|
221
354
|
}
|
|
222
355
|
// Never assume now-vs-scheduled โ it decides which agents are eligible (online-only for now,
|
|
223
356
|
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
@@ -252,13 +385,48 @@ export function registerRunTools(server, deps) {
|
|
|
252
385
|
}
|
|
253
386
|
// when:'schedule' with no cloudRunnerId โ leave unpinned; the backend auto-picks a live VM at fire time.
|
|
254
387
|
}
|
|
255
|
-
// LOCAL run-NOW to a specific agent must be online (can't dispatch to an offline agent).
|
|
256
|
-
// SCHEDULE
|
|
257
|
-
|
|
388
|
+
// LOCAL/REMOTE run-NOW to a specific agent must be online (can't dispatch to an offline agent).
|
|
389
|
+
// The SCHEDULE branch below allows an offline agent since it runs when it fires. REMOTE also
|
|
390
|
+
// REQUIRES an explicit agentId at all times โ auto-pick across teammate boxes is unsafe.
|
|
391
|
+
if (mode === 'remote' && !agentId) {
|
|
392
|
+
return err("REMOTE runs need an explicit agentId. Call remote_agents to see the teammate machines, " +
|
|
393
|
+
"then pass one as agentId (must be an ONLINE row for when:'immediate').");
|
|
394
|
+
}
|
|
395
|
+
if ((mode === 'local' || mode === 'remote') && when === 'immediate' && agentId) {
|
|
258
396
|
const offlineMsg = await localAgentOfflineForNow(adapter, agentId);
|
|
259
397
|
if (offlineMsg)
|
|
260
398
|
return err(offlineMsg);
|
|
261
399
|
}
|
|
400
|
+
// Mobile refuse cascade โ asked AFTER mode/when so the client can filter the device list by
|
|
401
|
+
// where it's going to run. Order mirrors the UI: pick the KIND of device (Real / Emulator /
|
|
402
|
+
// Cloud โฆ), then the specific device off the matching tab. Cloud tabs show even in LOCAL
|
|
403
|
+
// mode (see device-dropdown.component.html:104-111) โ LOCAL orchestrator can drive a cloud
|
|
404
|
+
// device via Appium.
|
|
405
|
+
if (isMobile) {
|
|
406
|
+
if (!deviceType) {
|
|
407
|
+
const platform = tcType.toLowerCase();
|
|
408
|
+
const localKinds = platform === 'ios' ? "'real_device', 'simulator'" : "'real_device', 'emulator'";
|
|
409
|
+
return err(`ASK the user which DEVICE TYPE this ${tcType} run should use: ${localKinds}, or ` +
|
|
410
|
+
`'cloud_device' / 'cloud_emulator' / 'cloud_simulator' for a farm device. Then call ` +
|
|
411
|
+
`tc_run again with deviceType set. (Also call mobile_list_devices({ platform: '${platform}' }) ` +
|
|
412
|
+
`to show the available devices grouped by kind โ the user picks a device and passes ` +
|
|
413
|
+
`\`id\` as deviceId, its \`deviceType\`, and โ for cloud rows โ its \`cloudDeviceType\`.)`);
|
|
414
|
+
}
|
|
415
|
+
const isCloudPick = deviceType.startsWith('cloud_');
|
|
416
|
+
if (isCloudPick && !cloudDeviceType) {
|
|
417
|
+
const platform = tcType.toLowerCase();
|
|
418
|
+
return err(`Cloud device picks need \`cloudDeviceType\` โ which farm the device came from. Call ` +
|
|
419
|
+
`mobile_list_devices({ platform: '${platform}' }); the matching row's \`cloudDeviceType\` ` +
|
|
420
|
+
`is one of 'lambda_test' / 'browser_stack' / 'sauce_labs' / 'simplify_qa_device_form'. ` +
|
|
421
|
+
`Pass it alongside deviceType and deviceId.`);
|
|
422
|
+
}
|
|
423
|
+
if (!deviceId) {
|
|
424
|
+
const platform = tcType.toLowerCase();
|
|
425
|
+
return err(`ASK the user which DEVICE to run on. Call mobile_list_devices({ platform: '${platform}' }) ` +
|
|
426
|
+
`and offer the rows whose deviceType matches '${deviceType}' โ the user picks one; pass its ` +
|
|
427
|
+
`\`id\` as deviceId (and optionally its \`osVersion\`) to tc_run.`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
262
430
|
if (when === 'schedule') {
|
|
263
431
|
if (!adapter.createSchedule)
|
|
264
432
|
return err('This product does not support scheduling runs.');
|
|
@@ -267,7 +435,8 @@ export function registerRunTools(server, deps) {
|
|
|
267
435
|
"ISO datetime (e.g. '2026-08-20T09:00:00Z'). Ask the user when the run should start (and the " +
|
|
268
436
|
'recurrence, end date and timezone if any).');
|
|
269
437
|
}
|
|
270
|
-
// A schedule fires detached from any live session โ a LOCAL schedule must pin a
|
|
438
|
+
// A schedule fires detached from any live session โ a LOCAL or REMOTE schedule must pin a
|
|
439
|
+
// specific agent (mode:'remote' already fenced above, so this catches LOCAL).
|
|
271
440
|
if (mode === 'local' && !agentId)
|
|
272
441
|
return err(await localScheduleNeedsAgent(adapter));
|
|
273
442
|
// Cloud: pin the runner's CREDENTIAL only when the user explicitly picked one; otherwise leave
|
|
@@ -276,7 +445,7 @@ export function registerRunTools(server, deps) {
|
|
|
276
445
|
const schedOpts = {
|
|
277
446
|
mode,
|
|
278
447
|
scheduledFor,
|
|
279
|
-
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
448
|
+
...((mode === 'local' || mode === 'remote') && agentId ? { agentId } : {}),
|
|
280
449
|
...(pinnedRunner?.credentialId ? { cloudRunnerCredentialId: pinnedRunner.credentialId } : {}),
|
|
281
450
|
...(browser !== undefined ? { browser } : {}),
|
|
282
451
|
...(environment !== undefined ? { environmentId: environment } : {}),
|
|
@@ -300,12 +469,15 @@ export function registerRunTools(server, deps) {
|
|
|
300
469
|
}
|
|
301
470
|
const opts = {
|
|
302
471
|
mode,
|
|
303
|
-
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
472
|
+
...((mode === 'local' || mode === 'remote') && agentId ? { agentId } : {}),
|
|
304
473
|
...(environment !== undefined ? { environment } : {}),
|
|
305
474
|
...(browser !== undefined ? { browser } : {}),
|
|
306
475
|
...(headless !== undefined ? { headless } : {}),
|
|
307
476
|
...(resolvedRunner ? { cloudRunnerId: resolvedRunner.id } : {}),
|
|
308
477
|
...(deviceId !== undefined ? { deviceId } : {}),
|
|
478
|
+
...(deviceType !== undefined ? { deviceType: deviceType.toUpperCase() } : {}),
|
|
479
|
+
...(cloudDeviceType !== undefined ? { cloudDeviceType: cloudDeviceType.toUpperCase() } : {}),
|
|
480
|
+
...(osVersion !== undefined ? { osVersion } : {}),
|
|
309
481
|
...(releaseId ? { releaseId } : {}),
|
|
310
482
|
...(sprintId ? { sprintId } : {}),
|
|
311
483
|
...(versionId ? { versionId } : {}),
|
|
@@ -314,7 +486,9 @@ export function registerRunTools(server, deps) {
|
|
|
314
486
|
const run = await adapter.startRun({ tcId }, opts);
|
|
315
487
|
const where = mode === 'cloud'
|
|
316
488
|
? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
|
|
317
|
-
:
|
|
489
|
+
: mode === 'remote'
|
|
490
|
+
? `on remote agent \`${agentId}\``
|
|
491
|
+
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
318
492
|
return ok(`Started ${tcId} ${where}. Run id \`${run.runId}\` โ poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
319
493
|
}
|
|
320
494
|
catch (e) {
|
|
@@ -322,20 +496,22 @@ export function registerRunTools(server, deps) {
|
|
|
322
496
|
}
|
|
323
497
|
});
|
|
324
498
|
server.registerTool('suite_run', {
|
|
325
|
-
title: 'Run a test suite (local
|
|
499
|
+
title: 'Run a test suite (local / remote / cloud; now or scheduled)',
|
|
326
500
|
description: 'Execute an existing test SUITE โ all its member test cases, in order โ in the connected product. ' +
|
|
327
501
|
'ASK the user for EVERY field the UI\'s Execute-suite dialog asks โ never assume โ and in ORDER ' +
|
|
328
502
|
'(mirrors the product UI, `_parentComponent==="SU"`): (1) `releaseId` (from list_releases โ the UI ' +
|
|
329
503
|
'marks it required with `*` for a suite), (2) `sprintId` (from list_sprints โ also `*` for a suite), ' +
|
|
330
|
-
'(3) `environment` (from tc_list_environments โ `*`), (4) `mode` local/cloud, (5) `when` immediate/schedule, ' +
|
|
504
|
+
'(3) `environment` (from tc_list_environments โ `*`), (4) `mode` local/remote/cloud, (5) `when` immediate/schedule, ' +
|
|
331
505
|
'(6) for a schedule the start time + recurrence + timezone, (7) THEN offer the eligible agent/runner. ' +
|
|
332
506
|
'The tool REFUSES when any of (1)โ(5) is omitted โ that is deliberate; it forces the ask instead of ' +
|
|
333
507
|
'silently defaulting. UNLIKE `tc_run`, a suite does NOT ask for `versionId` or `browser` โ each TC ' +
|
|
334
508
|
"in the suite carries its own pinned version + execution platform (that's what the suite editor picks per row). " +
|
|
335
509
|
'`mode` decides WHERE it runs:\n' +
|
|
336
|
-
" โข 'local' โ
|
|
337
|
-
"when:'immediate'
|
|
338
|
-
"when:'schedule'
|
|
510
|
+
" โข 'local' โ THIS machine's desktop agent, always serial. Call `local_agents` (0 or 1 row). For " +
|
|
511
|
+
"when:'immediate' the tool probes :4040 and refuses if the desktop agent is down. Omit `agentId` to " +
|
|
512
|
+
"auto-pick this machine's agent; for when:'schedule' `agentId` is REQUIRED.\n" +
|
|
513
|
+
" โข 'remote' โ ANOTHER registered machine's desktop agent, always serial. Call `remote_agents`; " +
|
|
514
|
+
"`agentId` is REQUIRED (no auto-pick). when:'immediate' โ ONLINE only; when:'schedule' โ offline OK.\n" +
|
|
339
515
|
" โข 'cloud' โ online cloud runner VM(s) (ONLINE-only whether now or scheduled). Pass `cloudRunnerId` " +
|
|
340
516
|
'from cloud_runners; for a schedule omit it to auto-pick a live VM at fire time.\n' +
|
|
341
517
|
'For cloud, if no runner is online it reports that there are no active cloud runners (nothing is run).\n' +
|
|
@@ -349,8 +525,8 @@ export function registerRunTools(server, deps) {
|
|
|
349
525
|
suiteId: z.string().describe('The suite id to run (from suite_list / suite_get).'),
|
|
350
526
|
releaseId: z.string().optional().describe('REQUIRED for a suite (UI marks it `*` for _parentComponent==="SU"). A release id from list_releases. Ask the user.'),
|
|
351
527
|
sprintId: z.string().optional().describe('REQUIRED for a suite (UI marks it `*` for _parentComponent==="SU"). A sprint id from list_sprints (scoped to the picked release). Ask the user.'),
|
|
352
|
-
mode: z.enum(['local', 'cloud']).optional().describe('Where to run. REQUIRED โ ASK the user if unstated (the tool refuses when omitted).'),
|
|
353
|
-
agentId: z.string().optional().describe("For local: a
|
|
528
|
+
mode: z.enum(['local', 'remote', 'cloud']).optional().describe('Where to run. REQUIRED โ ASK the user if unstated (the tool refuses when omitted).'),
|
|
529
|
+
agentId: z.string().optional().describe("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."),
|
|
354
530
|
environment: z.string().optional().describe('Environment key (from tc_list_environments). REQUIRED โ the UI marks this with a red asterisk for a suite.'),
|
|
355
531
|
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."),
|
|
356
532
|
headless: z.boolean().optional().describe('Headless. Defaults: cloud=true, local=false. (Immediate runs only.)'),
|
|
@@ -389,9 +565,9 @@ export function registerRunTools(server, deps) {
|
|
|
389
565
|
'field with a red asterisk for a suite.');
|
|
390
566
|
}
|
|
391
567
|
if (!mode) {
|
|
392
|
-
return err("Specify where to run: call suite_run again with mode: 'local' or 'cloud'. " +
|
|
393
|
-
|
|
394
|
-
'cloud runs on online cloud runner VM(s).
|
|
568
|
+
return err("Specify where to run: call suite_run again with mode: 'local', 'remote', or 'cloud'. " +
|
|
569
|
+
"If the user has not said which, ASK them first โ local runs on THIS machine's agent, " +
|
|
570
|
+
"remote runs on a teammate's registered agent, cloud runs on online cloud runner VM(s).");
|
|
395
571
|
}
|
|
396
572
|
// Never assume now-vs-scheduled โ it decides which agents are eligible (online-only for now,
|
|
397
573
|
// online+offline for a local schedule) and, for a schedule, the start time asked next.
|
|
@@ -426,9 +602,13 @@ export function registerRunTools(server, deps) {
|
|
|
426
602
|
}
|
|
427
603
|
// when:'schedule' with no cloudRunnerId โ leave unpinned; the backend auto-picks a live VM at fire time.
|
|
428
604
|
}
|
|
429
|
-
// LOCAL run-NOW to a specific agent must be online
|
|
430
|
-
//
|
|
431
|
-
if (mode === '
|
|
605
|
+
// LOCAL/REMOTE run-NOW to a specific agent must be online. SCHEDULE branch below allows offline.
|
|
606
|
+
// REMOTE always requires an explicit agentId (auto-pick across teammate boxes is unsafe).
|
|
607
|
+
if (mode === 'remote' && !agentId) {
|
|
608
|
+
return err("REMOTE runs need an explicit agentId. Call remote_agents to see the teammate machines, " +
|
|
609
|
+
"then pass one as agentId (must be an ONLINE row for when:'immediate').");
|
|
610
|
+
}
|
|
611
|
+
if ((mode === 'local' || mode === 'remote') && when === 'immediate' && agentId) {
|
|
432
612
|
const offlineMsg = await localAgentOfflineForNow(adapter, agentId);
|
|
433
613
|
if (offlineMsg)
|
|
434
614
|
return err(offlineMsg);
|
|
@@ -442,7 +622,8 @@ export function registerRunTools(server, deps) {
|
|
|
442
622
|
'recurrence, end date and timezone if any). NOTE: strategy/maxConcurrent/terminateOnFailure are ' +
|
|
443
623
|
"immediate-run only โ a schedule uses the suite's own settings.");
|
|
444
624
|
}
|
|
445
|
-
// A schedule fires detached from any live session โ
|
|
625
|
+
// A schedule fires detached from any live session โ LOCAL and REMOTE schedules must pin a
|
|
626
|
+
// specific agent. REMOTE was already fenced above; this catches LOCAL.
|
|
446
627
|
if (mode === 'local' && !agentId)
|
|
447
628
|
return err(await localScheduleNeedsAgent(adapter));
|
|
448
629
|
// Cloud: pin the runner's CREDENTIAL only when the user explicitly picked one; otherwise leave
|
|
@@ -451,7 +632,7 @@ export function registerRunTools(server, deps) {
|
|
|
451
632
|
const schedOpts = {
|
|
452
633
|
mode,
|
|
453
634
|
scheduledFor,
|
|
454
|
-
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
635
|
+
...((mode === 'local' || mode === 'remote') && agentId ? { agentId } : {}),
|
|
455
636
|
...(pinnedRunner?.credentialId ? { cloudRunnerCredentialId: pinnedRunner.credentialId } : {}),
|
|
456
637
|
...(browser !== undefined ? { browser } : {}),
|
|
457
638
|
...(environment !== undefined ? { environmentId: environment } : {}),
|
|
@@ -475,7 +656,7 @@ export function registerRunTools(server, deps) {
|
|
|
475
656
|
}
|
|
476
657
|
const opts = {
|
|
477
658
|
mode,
|
|
478
|
-
...(mode === 'local' && agentId ? { agentId } : {}),
|
|
659
|
+
...((mode === 'local' || mode === 'remote') && agentId ? { agentId } : {}),
|
|
479
660
|
...(environment !== undefined ? { environment } : {}),
|
|
480
661
|
...(browser !== undefined ? { browser } : {}),
|
|
481
662
|
...(headless !== undefined ? { headless } : {}),
|
|
@@ -490,7 +671,9 @@ export function registerRunTools(server, deps) {
|
|
|
490
671
|
const run = await adapter.startRun({ suiteId }, opts);
|
|
491
672
|
const where = mode === 'cloud'
|
|
492
673
|
? `on cloud runner ${resolvedRunner ? `**${resolvedRunner.name}**` : ''}`
|
|
493
|
-
:
|
|
674
|
+
: mode === 'remote'
|
|
675
|
+
? `on remote agent \`${agentId}\``
|
|
676
|
+
: `locally${agentId ? ` on agent \`${agentId}\`` : ' on your online agent'}`;
|
|
494
677
|
return ok(`Started suite ${suiteId} ${where}. Run id \`${run.runId}\` โ poll run_status({ runId: "${run.runId}" }) for progress.`, { run, mode, ...(resolvedRunner ? { runner: resolvedRunner } : {}) });
|
|
495
678
|
}
|
|
496
679
|
catch (e) {
|
|
@@ -499,16 +682,33 @@ export function registerRunTools(server, deps) {
|
|
|
499
682
|
});
|
|
500
683
|
server.registerTool('run_status', {
|
|
501
684
|
title: 'Get a run status',
|
|
502
|
-
description: 'Read the status of a run
|
|
503
|
-
'
|
|
504
|
-
|
|
685
|
+
description: 'Read the status of a run โ state (queued/running/passed/failed/stopped/error), progress, ' +
|
|
686
|
+
'per-test-case rollup for a SUITE run (capped at 100 shown), and a link. Pass EITHER `runId` ' +
|
|
687
|
+
'(from tc_run / suite_run) or `suiteId` (from suite_list) to read that suite\'s LATEST ' +
|
|
688
|
+
'execution. Requires a live connection.',
|
|
689
|
+
inputSchema: {
|
|
690
|
+
runId: z.string().optional().describe('The run id returned by tc_run or suite_run.'),
|
|
691
|
+
suiteId: z.string().optional().describe('A suite id (from suite_list) โ resolves to the suite\'s latest execution.'),
|
|
692
|
+
},
|
|
505
693
|
annotations: { readOnlyHint: true },
|
|
506
|
-
}, async ({ runId }) => {
|
|
694
|
+
}, async ({ runId, suiteId }) => {
|
|
507
695
|
const adapter = deps.getAdapter();
|
|
508
696
|
if (!adapter)
|
|
509
697
|
return err(NOT_CONNECTED);
|
|
698
|
+
if ((runId && suiteId) || (!runId && !suiteId)) {
|
|
699
|
+
return err('Pass exactly one of `runId` or `suiteId`.');
|
|
700
|
+
}
|
|
510
701
|
try {
|
|
511
|
-
|
|
702
|
+
let effectiveRunId = runId;
|
|
703
|
+
if (suiteId) {
|
|
704
|
+
if (!adapter.getLatestSuiteRun)
|
|
705
|
+
return err('This product does not support latest-suite-run lookup โ pass `runId` instead.');
|
|
706
|
+
const latest = await adapter.getLatestSuiteRun(suiteId);
|
|
707
|
+
if (!latest)
|
|
708
|
+
return ok(`Suite ${suiteId} has no executions yet.`);
|
|
709
|
+
effectiveRunId = latest;
|
|
710
|
+
}
|
|
711
|
+
const status = await adapter.getRun(effectiveRunId);
|
|
512
712
|
return ok(statusText(status), { run: status });
|
|
513
713
|
}
|
|
514
714
|
catch (e) {
|