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.
- package/CHANGELOG.md +114 -0
- package/CONTRIBUTING.md +417 -0
- package/LICENSE +22 -0
- package/README.md +719 -0
- package/ROADMAP.md +239 -0
- package/docs/ENTERPRISE_READINESS.md +545 -0
- package/docs/MCP_SETUP.md +180 -0
- package/docs/PRIVACY.md +198 -0
- package/docs/REACT_NATIVE_AUTOMATION_GUIDELINES.md +584 -0
- package/docs/SECURITY.md +573 -0
- package/package.json +69 -0
- package/prompts/example-login-tests.txt +9 -0
- package/prompts/example-youtube-tests.txt +8 -0
- package/src/mcp-server/index.js +625 -0
- package/src/mcp-server/tools/contextTools.js +194 -0
- package/src/mcp-server/tools/promptTools.js +191 -0
- package/src/mcp-server/tools/runTools.js +357 -0
- package/src/mcp-server/tools/utilityTools.js +721 -0
- package/src/mcp-server/tools/validateTools.js +220 -0
- package/src/mcp-server/utils/appContext.js +295 -0
- package/src/mcp-server/utils/logger.js +52 -0
- package/src/mcp-server/utils/maestro.js +508 -0
- package/templates/mcp-config-claude-desktop.json +15 -0
- package/templates/mcp-config-cursor.json +15 -0
- package/templates/mcp-config-vscode.json +13 -0
|
@@ -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
|
+
};
|