mcp-maestro-mobile-ai 1.1.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.
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Run Tools
3
+ * Execute Maestro tests and capture results
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+ import { logger } from '../utils/logger.js';
11
+ import { runMaestroFlow, checkDeviceConnection, checkAppInstalled, getConfig } from '../utils/maestro.js';
12
+ import { validateMaestroYaml } from './validateTools.js';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ const PROJECT_ROOT = join(__dirname, '../../..');
17
+ const OUTPUT_DIR = join(PROJECT_ROOT, 'output');
18
+ const RESULTS_DIR = join(OUTPUT_DIR, 'results');
19
+
20
+ // Store for test results
21
+ let lastRunResults = null;
22
+ let runCounter = 0;
23
+
24
+ /**
25
+ * Run a single Maestro test with pre-flight checks
26
+ */
27
+ export async function runTest(yamlContent, testName, options = {}) {
28
+ try {
29
+ logger.info(`Running test: ${testName}`);
30
+
31
+ // Pre-flight check: Device connection
32
+ const deviceStatus = await checkDeviceConnection();
33
+ if (!deviceStatus.connected) {
34
+ return {
35
+ content: [
36
+ {
37
+ type: 'text',
38
+ text: JSON.stringify({
39
+ success: false,
40
+ name: testName,
41
+ error: 'No device connected',
42
+ details: deviceStatus.error,
43
+ hint: 'Start an Android emulator before running tests. Use check_device tool to verify.',
44
+ }),
45
+ },
46
+ ],
47
+ };
48
+ }
49
+
50
+ // Validate YAML first
51
+ const validation = await validateMaestroYaml(yamlContent);
52
+ const validationResult = JSON.parse(validation.content[0].text);
53
+
54
+ if (!validationResult.valid) {
55
+ return {
56
+ content: [
57
+ {
58
+ type: 'text',
59
+ text: JSON.stringify({
60
+ success: false,
61
+ name: testName,
62
+ error: 'YAML validation failed',
63
+ validationErrors: validationResult.errors,
64
+ hint: 'Fix the YAML errors and try again. Common issues: missing appId, invalid syntax.',
65
+ }),
66
+ },
67
+ ],
68
+ };
69
+ }
70
+
71
+ // Pre-flight check: App installed
72
+ const appId = validationResult.config?.appId;
73
+ if (appId) {
74
+ const appStatus = await checkAppInstalled(appId);
75
+ if (!appStatus.installed) {
76
+ return {
77
+ content: [
78
+ {
79
+ type: 'text',
80
+ text: JSON.stringify({
81
+ success: false,
82
+ name: testName,
83
+ error: `App not installed: ${appId}`,
84
+ hint: 'Install the app on the emulator before running tests.',
85
+ }),
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ }
91
+
92
+ // Get retry count from options or environment
93
+ const config = getConfig();
94
+ const retries = options.retries ?? config.defaultRetries;
95
+
96
+ // Run the test with retry support
97
+ const result = await runMaestroFlow(yamlContent, testName, { retries });
98
+
99
+ // Store result
100
+ const runId = `run-${++runCounter}-${Date.now()}`;
101
+ lastRunResults = {
102
+ runId,
103
+ timestamp: new Date().toISOString(),
104
+ tests: [result],
105
+ summary: {
106
+ total: 1,
107
+ passed: result.success ? 1 : 0,
108
+ failed: result.success ? 0 : 1,
109
+ },
110
+ };
111
+
112
+ // Save results to file
113
+ await saveResults(lastRunResults);
114
+
115
+ // Auto-cleanup old results
116
+ await autoCleanupResults();
117
+
118
+ logger.info(`Test ${result.success ? 'passed' : 'failed'}: ${testName}`);
119
+
120
+ return {
121
+ content: [
122
+ {
123
+ type: 'text',
124
+ text: JSON.stringify({
125
+ success: result.success,
126
+ name: testName,
127
+ duration: result.duration,
128
+ attempts: result.attempts || 1,
129
+ error: result.error || null,
130
+ screenshot: result.screenshot || null,
131
+ output: result.output ? result.output.substring(0, 500) : null,
132
+ runId,
133
+ }),
134
+ },
135
+ ],
136
+ };
137
+ } catch (error) {
138
+ logger.error(`Test execution error: ${testName}`, { error: error.message });
139
+ return {
140
+ content: [
141
+ {
142
+ type: 'text',
143
+ text: JSON.stringify({
144
+ success: false,
145
+ name: testName,
146
+ error: error.message,
147
+ hint: 'Check the logs for more details.',
148
+ }),
149
+ },
150
+ ],
151
+ };
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Run multiple Maestro tests with pre-flight checks
157
+ */
158
+ export async function runTestSuite(tests, options = {}) {
159
+ try {
160
+ if (!Array.isArray(tests) || tests.length === 0) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: 'text',
165
+ text: JSON.stringify({
166
+ success: false,
167
+ error: 'No tests provided. Provide an array of test objects with "yaml" and "name" properties.',
168
+ }),
169
+ },
170
+ ],
171
+ };
172
+ }
173
+
174
+ logger.info(`Running test suite with ${tests.length} tests`);
175
+
176
+ // Pre-flight check: Device connection
177
+ const deviceStatus = await checkDeviceConnection();
178
+ if (!deviceStatus.connected) {
179
+ return {
180
+ content: [
181
+ {
182
+ type: 'text',
183
+ text: JSON.stringify({
184
+ success: false,
185
+ error: 'No device connected',
186
+ details: deviceStatus.error,
187
+ hint: 'Start an Android emulator before running tests.',
188
+ }),
189
+ },
190
+ ],
191
+ };
192
+ }
193
+
194
+ const config = getConfig();
195
+ const retries = options.retries ?? config.defaultRetries;
196
+ const results = [];
197
+ const startTime = Date.now();
198
+
199
+ for (let i = 0; i < tests.length; i++) {
200
+ const test = tests[i];
201
+ logger.info(`Running test ${i + 1}/${tests.length}: ${test.name}`);
202
+
203
+ // Validate YAML
204
+ const validation = await validateMaestroYaml(test.yaml);
205
+ const validationResult = JSON.parse(validation.content[0].text);
206
+
207
+ if (!validationResult.valid) {
208
+ results.push({
209
+ success: false,
210
+ name: test.name,
211
+ error: 'YAML validation failed',
212
+ validationErrors: validationResult.errors,
213
+ });
214
+ continue;
215
+ }
216
+
217
+ // Run the test with retry support
218
+ const result = await runMaestroFlow(test.yaml, test.name, { retries });
219
+ results.push(result);
220
+ }
221
+
222
+ const totalDuration = ((Date.now() - startTime) / 1000).toFixed(2);
223
+ const passed = results.filter(r => r.success).length;
224
+ const failed = results.filter(r => !r.success).length;
225
+
226
+ // Store results
227
+ const runId = `suite-${++runCounter}-${Date.now()}`;
228
+ lastRunResults = {
229
+ runId,
230
+ timestamp: new Date().toISOString(),
231
+ duration: totalDuration,
232
+ tests: results,
233
+ summary: {
234
+ total: results.length,
235
+ passed,
236
+ failed,
237
+ },
238
+ };
239
+
240
+ // Save results to file
241
+ await saveResults(lastRunResults);
242
+
243
+ // Auto-cleanup old results
244
+ await autoCleanupResults();
245
+
246
+ logger.info(`Test suite completed: ${passed}/${results.length} passed`);
247
+
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: JSON.stringify({
253
+ success: failed === 0,
254
+ runId,
255
+ duration: totalDuration,
256
+ summary: {
257
+ total: results.length,
258
+ passed,
259
+ failed,
260
+ },
261
+ tests: results.map(r => ({
262
+ name: r.name,
263
+ success: r.success,
264
+ duration: r.duration,
265
+ attempts: r.attempts || 1,
266
+ error: r.error || null,
267
+ })),
268
+ }),
269
+ },
270
+ ],
271
+ };
272
+ } catch (error) {
273
+ logger.error('Test suite execution error', { error: error.message });
274
+ return {
275
+ content: [
276
+ {
277
+ type: 'text',
278
+ text: JSON.stringify({
279
+ success: false,
280
+ error: error.message,
281
+ }),
282
+ },
283
+ ],
284
+ };
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Save test results to file
290
+ */
291
+ async function saveResults(results) {
292
+ try {
293
+ await fs.mkdir(RESULTS_DIR, { recursive: true });
294
+
295
+ const filename = `${results.runId}.json`;
296
+ const filepath = join(RESULTS_DIR, filename);
297
+
298
+ await fs.writeFile(filepath, JSON.stringify(results, null, 2), 'utf8');
299
+ logger.info(`Results saved: ${filepath}`);
300
+
301
+ // Also save as latest
302
+ const latestPath = join(RESULTS_DIR, 'latest.json');
303
+ await fs.writeFile(latestPath, JSON.stringify(results, null, 2), 'utf8');
304
+ } catch (error) {
305
+ logger.error('Failed to save results', { error: error.message });
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Auto-cleanup old results based on MAX_RESULTS setting
311
+ */
312
+ async function autoCleanupResults() {
313
+ try {
314
+ const config = getConfig();
315
+ const maxResults = config.maxResults;
316
+
317
+ // Get all result files
318
+ const files = await fs.readdir(RESULTS_DIR);
319
+ const resultFiles = files.filter(f => f.endsWith('.json') && f !== 'latest.json');
320
+
321
+ // If under limit, no cleanup needed
322
+ if (resultFiles.length <= maxResults) {
323
+ return;
324
+ }
325
+
326
+ // Get file stats and sort by modification time
327
+ const filesWithStats = await Promise.all(
328
+ resultFiles.map(async (f) => {
329
+ const filepath = join(RESULTS_DIR, f);
330
+ const stats = await fs.stat(filepath);
331
+ return { name: f, path: filepath, mtime: stats.mtime };
332
+ })
333
+ );
334
+
335
+ filesWithStats.sort((a, b) => b.mtime - a.mtime);
336
+
337
+ // Delete files beyond the limit
338
+ const filesToDelete = filesWithStats.slice(maxResults);
339
+
340
+ for (const file of filesToDelete) {
341
+ try {
342
+ await fs.unlink(file.path);
343
+ logger.info(`Auto-cleanup: deleted old result ${file.name}`);
344
+ } catch (e) {
345
+ // Ignore errors
346
+ }
347
+ }
348
+ } catch (error) {
349
+ // Ignore cleanup errors - don't fail test runs
350
+ logger.warn('Auto-cleanup warning', { error: error.message });
351
+ }
352
+ }
353
+
354
+ export default {
355
+ runTest,
356
+ runTestSuite,
357
+ };