mcp-state-machine-test-framework 1.2.6 → 1.2.7
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/index.js +300 -727
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,727 +1,300 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
-
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import fs from "fs/promises";
|
|
8
|
-
import fsSync from "fs";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
import { exec } from "child_process";
|
|
12
|
-
import { promisify } from "util";
|
|
13
|
-
|
|
14
|
-
const execAsync = promisify(exec);
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const data = {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
throw e;
|
|
302
|
-
} finally {
|
|
303
|
-
await runActions(suite.afterStep, stepRes.actions, row);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
await runActions(suite.afterCase, caseRes.hooks.afterCase, row);
|
|
307
|
-
} catch (e) { caseRes.status = "failed"; caseRes.error = e.message; }
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
await runActions(suite.afterSuite, results.hooks.afterSuite);
|
|
311
|
-
} finally {
|
|
312
|
-
for (const { transport } of this.mcpClients.values()) await transport.close();
|
|
313
|
-
this.mcpClients.clear();
|
|
314
|
-
await this.finalizarReporte(reportDir, results);
|
|
315
|
-
}
|
|
316
|
-
return reportDir;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
async finalizarReporte(dir, data) {
|
|
320
|
-
const renderActions = (actions) => actions.map(a => {
|
|
321
|
-
const isAssert = a.action.startsWith("[ASSERT]");
|
|
322
|
-
const isIdentity = a.output && a.output.includes("Identidad confirmada");
|
|
323
|
-
|
|
324
|
-
return `
|
|
325
|
-
<div class="action-card" style="border-left: 4px solid ${a.status === 'passed' ? (isAssert ? '#10b981' : '#3b82f6') : '#ef4444'}; background: ${isAssert ? 'rgba(16, 185, 129, 0.05)' : 'rgba(15, 23, 42, 0.8)'}; padding: 20px; border-radius: 12px; margin-bottom: 15px; border: 1px solid #1e293b;">
|
|
326
|
-
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px;">
|
|
327
|
-
<span style="font-weight: 600; font-size: 0.9em; color: ${isAssert ? '#10b981' : '#94a3b8'};">
|
|
328
|
-
${isAssert ? '🛡️ VALIDACIÓN (ASSERT)' : '⚙️ ACCIÓN'}
|
|
329
|
-
</span>
|
|
330
|
-
<span class="status-badge" style="background:${a.status === 'passed' ? '#065f46' : '#991b1b'};">${a.status}</span>
|
|
331
|
-
</div>
|
|
332
|
-
<div style="font-family: 'JetBrains Mono', monospace; font-size: 0.95em; color: #f8fafc; margin-bottom: 10px;">
|
|
333
|
-
${isAssert ? a.action.replace("[ASSERT]", "").trim() : a.action}
|
|
334
|
-
</div>
|
|
335
|
-
${isIdentity ? `<div style="background: rgba(16, 185, 129, 0.2); color: #34d399; padding: 8px 12px; border-radius: 6px; font-size: 0.85em; margin-bottom: 10px; border: 1px solid rgba(16, 185, 129, 0.3);">🔍 <b>Identidad:</b> ${a.output.includes("Multivectorial") ? "Sincronización Multivectorial Exitosa" : "Confirmada"}</div>` : ""}
|
|
336
|
-
${a.image ? `<div style="margin-top:15px; text-align:center;"><img src="${a.image}" style="max-width:100%; border-radius:8px; border: 1px solid #334155; cursor: pointer;" onclick="window.open(this.src)"></div>` : ""}
|
|
337
|
-
${a.output && !isIdentity ? `
|
|
338
|
-
<details style="margin-top:10px;">
|
|
339
|
-
<summary style="cursor:pointer; color:#64748b; font-size:0.8em;">Ver logs técnicos</summary>
|
|
340
|
-
<pre style="background:#020617; padding:12px; border-radius:6px; font-size:0.8em; color:#10b981; margin-top:8px; border:1px solid #1e293b; overflow-x:auto;">${a.output}</pre>
|
|
341
|
-
</details>` : ""}
|
|
342
|
-
${a.error ? `<pre style="background:#450a0a; padding:12px; border-radius:6px; font-size:0.8em; color:#fca5a5; margin-top:8px;">${a.error}</pre>` : ""}
|
|
343
|
-
</div>
|
|
344
|
-
`}).join("");
|
|
345
|
-
|
|
346
|
-
const html = `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><title>SMS Dashboard V11.1 CSV-Ready</title>
|
|
347
|
-
<style>
|
|
348
|
-
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');
|
|
349
|
-
body { font-family: 'Outfit', sans-serif; background: #020617; color: #f8fafc; padding: 60px; line-height: 1.6; }
|
|
350
|
-
.container { max-width: 1000px; margin: 0 auto; }
|
|
351
|
-
.card { background: #0f172a; border: 1px solid #1e293b; border-radius: 20px; padding: 40px; margin-bottom: 30px; box-shadow: 0 20px 50px rgba(0,0,0,0.3); }
|
|
352
|
-
.hook-label { background: #3b82f6; color: white; font-size: 0.7em; font-weight: 800; padding: 5px 12px; border-radius: 50px; text-transform: uppercase; margin-bottom: 15px; display: inline-block; letter-spacing: 1px; }
|
|
353
|
-
h1 { font-size: 3.5em; font-weight: 800; background: linear-gradient(to right, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.2em; }
|
|
354
|
-
.step { border-left: 3px solid #3b82f6; padding-left: 30px; margin: 40px 0; }
|
|
355
|
-
h2 { font-size: 2em; margin-bottom: 1em; color: #f1f5f9; }
|
|
356
|
-
h3 { color: #94a3b8; font-weight: 400; text-transform: uppercase; font-size: 0.9em; letter-spacing: 2px; }
|
|
357
|
-
</style></head>
|
|
358
|
-
<body><div class="container">
|
|
359
|
-
<h1>SMS Dashboard</h1>
|
|
360
|
-
<p style="color: #64748b; font-size: 1.1em; margin-bottom: 40px;">Universal Data-Driven Framework • V11.2</p>
|
|
361
|
-
${data.hooks.beforeSuite.length ? `<div class="card"><span class="hook-label">Global Setup</span>${renderActions(data.hooks.beforeSuite)}</div>` : ""}
|
|
362
|
-
${data.cases.map(c => `
|
|
363
|
-
<div class="card">
|
|
364
|
-
<h3 style="color:#60a5fa; margin-bottom:10px;">TEST CASE EXECUTION</h3>
|
|
365
|
-
<h2>${c.name}</h2>
|
|
366
|
-
${c.hooks.beforeCase.length ? `<div><span class="hook-label">Pre-Condition</span>${renderActions(c.hooks.beforeCase)}</div>` : ""}
|
|
367
|
-
${c.steps.map(s => `<div class="step"><h3>STEP: ${s.name}</h3>${renderActions(s.actions)}</div>`).join("")}
|
|
368
|
-
${c.hooks.afterCase.length ? `<div><span class="hook-label">Post-Condition</span>${renderActions(c.hooks.afterCase)}</div>` : ""}
|
|
369
|
-
</div>
|
|
370
|
-
`).join("")}
|
|
371
|
-
${data.hooks.afterSuite.length ? `<div class="card"><span class="hook-label">Global Teardown</span>${renderActions(data.hooks.afterSuite)}</div>` : ""}
|
|
372
|
-
</div></body></html>`;
|
|
373
|
-
await fs.writeFile(path.join(dir, "index.html"), html);
|
|
374
|
-
await fs.writeFile(path.join(dir, "results.json"), JSON.stringify(data, null, 2));
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const maquina = new MaquinaDeEstados();
|
|
379
|
-
const server = new McpServer({ name: "demo-state-machine", version: "11.1.0" });
|
|
380
|
-
server.tool("execute_suite", "Ejecución de Suite Completa", {
|
|
381
|
-
name: z.string().describe("Nombre de la suite a ejecutar (ej: 'Suite_Login'). Buscará el archivo en /suites.")
|
|
382
|
-
}, async ({ name }) => {
|
|
383
|
-
await maquina.cargar();
|
|
384
|
-
const dir = await maquina.ejecutarSuite(name);
|
|
385
|
-
return { content: [{ type: "text", text: `Reporte: ${dir}` }] };
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados.", {
|
|
389
|
-
mapName: z.string().describe("Nombre del archivo del mapa (ej: 'home_map.json'). El servidor lo buscará en la carpeta /maps."),
|
|
390
|
-
nodeName: z.string().describe("Identificador único del nodo (ej: 'LOGIN_PAGE', 'HOME')."),
|
|
391
|
-
nodeData: z.object({
|
|
392
|
-
transiciones: z.any().optional().describe("Objeto que define las salidas del nodo. Ej: { 'IR_A_LOGIN': { 'destino': 'LOGIN_PAGE', 'accion': 'mcp:wdio-mcp/click_element ...' } }")
|
|
393
|
-
}).passthrough().describe("Datos completos del nodo, incluyendo transiciones y metadatos.")
|
|
394
|
-
}, async ({ mapName, nodeName, nodeData }) => {
|
|
395
|
-
const mapPath = path.join(__dirname, 'maps', mapName);
|
|
396
|
-
let map = { nodos: {} };
|
|
397
|
-
try {
|
|
398
|
-
const content = await fs.readFile(mapPath, 'utf8');
|
|
399
|
-
map = JSON.parse(content);
|
|
400
|
-
if (!map.nodos) map = { nodos: map };
|
|
401
|
-
} catch (e) { }
|
|
402
|
-
|
|
403
|
-
map.nodos[nodeName] = nodeData;
|
|
404
|
-
await fs.mkdir(path.dirname(mapPath), { recursive: true });
|
|
405
|
-
await fs.writeFile(mapPath, JSON.stringify(map, null, 2));
|
|
406
|
-
|
|
407
|
-
// Generar/Actualizar Diagrama Mermaid
|
|
408
|
-
try {
|
|
409
|
-
let mermaid = "graph TD\n";
|
|
410
|
-
for (const [name, node] of Object.entries(map.nodos)) {
|
|
411
|
-
if (node.transiciones) {
|
|
412
|
-
for (const [transName, trans] of Object.entries(node.transiciones)) {
|
|
413
|
-
mermaid += ` ${name} -- "${transName}" --> ${trans.destino}\n`;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const mermaidPath = mapPath.replace('.json', '.md');
|
|
418
|
-
await fs.writeFile(mermaidPath, `# 🗺️ Diagrama de Estados: ${mapName}\n\n\`\`\`mermaid\n${mermaid}\`\`\``);
|
|
419
|
-
} catch (me) {
|
|
420
|
-
process.stderr.write(`[WARN] Error generando Mermaid: ${me.message}\n`);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
return { content: [{ type: "text", text: `Nodo '${nodeName}' actualizado. Diagrama visual regenerado en maps/${mapName.replace('.json', '.md')}` }] };
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del framework", {
|
|
427
|
-
filter: z.optional(z.string())
|
|
428
|
-
}, async () => {
|
|
429
|
-
const entities = { maps: [], test_cases: [], suites: [], health: [] };
|
|
430
|
-
const getFiles = async (dir) => {
|
|
431
|
-
try { return (await fs.readdir(path.join(__dirname, dir))).filter(f => f.endsWith('.json')); }
|
|
432
|
-
catch (e) { return []; }
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
entities.maps = await getFiles('maps');
|
|
436
|
-
entities.test_cases = await getFiles('test_cases');
|
|
437
|
-
entities.suites = await getFiles('suites');
|
|
438
|
-
|
|
439
|
-
// Validación de integridad simple
|
|
440
|
-
for (const suiteFile of entities.suites) {
|
|
441
|
-
try {
|
|
442
|
-
const suite = JSON.parse(await fs.readFile(path.join(__dirname, 'suites', suiteFile), 'utf8'));
|
|
443
|
-
if (!entities.maps.includes(suite.state_map)) {
|
|
444
|
-
entities.health.push(`❌ Suite '${suiteFile}' referencia a mapa inexistente: ${suite.state_map}`);
|
|
445
|
-
}
|
|
446
|
-
for (const tc of suite.tests) {
|
|
447
|
-
if (!entities.test_cases.includes(`${tc}.json`) && !entities.test_cases.includes(tc)) {
|
|
448
|
-
entities.health.push(`❌ Suite '${suiteFile}' referencia a test case inexistente: ${tc}`);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
} catch (e) { entities.health.push(`❌ Error leyendo suite '${suiteFile}': ${e.message}`); }
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (entities.health.length === 0) entities.health.push("✅ Estructura íntegra. Todos los vínculos son correctos.");
|
|
455
|
-
|
|
456
|
-
// Validación de integridad de Grafos (Nodos Huérfanos)
|
|
457
|
-
for (const mapFile of entities.maps) {
|
|
458
|
-
try {
|
|
459
|
-
const mapData = JSON.parse(await fs.readFile(path.join(__dirname, 'maps', mapFile), 'utf8'));
|
|
460
|
-
const nodos = mapData.nodos || mapData;
|
|
461
|
-
if (nodos && nodos["HOME"]) {
|
|
462
|
-
const reachable = new Set(["HOME"]);
|
|
463
|
-
const stack = ["HOME"];
|
|
464
|
-
while (stack.length > 0) {
|
|
465
|
-
const current = stack.pop();
|
|
466
|
-
const node = nodos[current];
|
|
467
|
-
if (node && node.transiciones) {
|
|
468
|
-
for (const t of Object.values(node.transiciones)) {
|
|
469
|
-
if (t.destino && nodos[t.destino] && !reachable.has(t.destino)) {
|
|
470
|
-
reachable.add(t.destino);
|
|
471
|
-
stack.push(t.destino);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
const totalNodes = Object.keys(nodos);
|
|
477
|
-
const unreachable = totalNodes.filter(n => !reachable.has(n));
|
|
478
|
-
if (unreachable.length > 0) {
|
|
479
|
-
entities.health.push(`⚠️ Mapa '${mapFile}': Nodos inalcanzables desde HOME: ${unreachable.join(', ')}`);
|
|
480
|
-
}
|
|
481
|
-
} else if (nodos && !nodos["HOME"]) {
|
|
482
|
-
entities.health.push(`⚠️ Mapa '${mapFile}': No tiene un nodo 'HOME' (inicio), no se puede validar el grafo.`);
|
|
483
|
-
}
|
|
484
|
-
} catch (e) { entities.health.push(`❌ Error validando grafo de '${mapFile}': ${e.message}`); }
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return { content: [{ type: "text", text: JSON.stringify(entities, null, 2) }] };
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
|
|
491
|
-
name: z.string().describe("Nombre identificador del test (ej: 'TC_Login_Exitoso')."),
|
|
492
|
-
steps: z.array(z.object({
|
|
493
|
-
name: z.string().optional().describe("Descripción amigable del paso (ej: 'Ingresar Usuario')."),
|
|
494
|
-
action: z.string().optional().describe("Acción a realizar. Puede ser un nombre de transición del mapa o un comando mcp:wdio-mcp/...")
|
|
495
|
-
}).passthrough()).describe("Lista ordenada de pasos lógicos a ejecutar.")
|
|
496
|
-
}, async ({ name, steps }) => {
|
|
497
|
-
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
498
|
-
const filePath = path.join(__dirname, 'test_cases', fileName);
|
|
499
|
-
const testCase = { name: name.replace('.json', ''), steps };
|
|
500
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
501
|
-
await fs.writeFile(filePath, JSON.stringify(testCase, null, 2));
|
|
502
|
-
return { content: [{ type: "text", text: `Caso de prueba '${name}' guardado correctamente.` }] };
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
|
|
506
|
-
name: z.string().describe("Nombre de la suite (ej: 'Suite_E2E_Sanity')."),
|
|
507
|
-
state_map: z.string().describe("Archivo del mapa de estados a usar (ej: 'mob_perfecto_map.json')."),
|
|
508
|
-
tests: z.array(z.string()).describe("Lista de nombres de casos de prueba a incluir en la suite."),
|
|
509
|
-
beforeSuite: z.array(z.string()).optional().default([]).describe("Acciones globales antes de la suite (ej: iniciar sesión)."),
|
|
510
|
-
afterSuite: z.array(z.string()).optional().default([]).describe("Acciones globales tras la suite (ej: cerrar sesión).")
|
|
511
|
-
}, async ({ name, state_map, tests, beforeSuite, afterSuite }) => {
|
|
512
|
-
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
513
|
-
const filePath = path.join(__dirname, 'suites', fileName);
|
|
514
|
-
const suite = { name: name.replace('.json', ''), state_map, tests, beforeSuite, afterSuite };
|
|
515
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
516
|
-
await fs.writeFile(filePath, JSON.stringify(suite, null, 2));
|
|
517
|
-
return { content: [{ type: "text", text: `Suite '${name}' guardada correctamente.` }] };
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
server.tool("design_wizard", "Asistente paso a paso para diseñar la máquina de estados", {
|
|
521
|
-
action: z.enum(["start", "add_node", "add_transition", "save"]),
|
|
522
|
-
data: z.record(z.any()).optional().describe("Datos según la fase: { mapName }, { nodeName }, { label, destino, accion }")
|
|
523
|
-
}, async ({ action, data }) => {
|
|
524
|
-
const contextPath = path.join(__dirname, 'data', '.design_context.json');
|
|
525
|
-
let context = { step: "IDLE", mapName: "", nodeName: "", transitions: {} };
|
|
526
|
-
|
|
527
|
-
try { context = JSON.parse(await fs.readFile(contextPath, 'utf8')); } catch (e) { }
|
|
528
|
-
|
|
529
|
-
if (action === "start") {
|
|
530
|
-
context = { step: "SELECT_NODE", mapName: data.mapName || "default_map.json", nodeName: "", transitions: {} };
|
|
531
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
532
|
-
return { content: [{ type: "text", text: `🧙♂️ Wizard Iniciado: Trabajando en '${context.mapName}'.\n\nPASO 1: ¿Qué nombre le damos al nodo? (Usa action: 'add_node')` }] };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (action === "add_node" && context.step === "SELECT_NODE") {
|
|
536
|
-
context.nodeName = data.nodeName;
|
|
537
|
-
context.step = "DEFINE_FINGERPRINT";
|
|
538
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
539
|
-
return { content: [{ type: "text", text: `📍 Nodo '${context.nodeName}' identificado.\n\nPASO 2: ¿Cuál es la Huella Digital (Fingerprint) de esta pantalla? (Usa action: 'add_fingerprint' con { selector }).` }] };
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (action === "add_fingerprint" && context.step === "DEFINE_FINGERPRINT") {
|
|
543
|
-
if (!context.fingerprint) context.fingerprint = { selectors: [] };
|
|
544
|
-
context.fingerprint.selectors.push(data.selector);
|
|
545
|
-
context.fingerprint.timeout = data.timeout || 5000;
|
|
546
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
547
|
-
return { content: [{ type: "text", text: `🧬 Vector añadido: ${data.selector}.\n\n¿Quieres añadir otro detector para este nodo? (Usa 'add_fingerprint') o finaliza la identidad con 'done_fingerprint'.` }] };
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (action === "done_fingerprint" && context.step === "DEFINE_FINGERPRINT") {
|
|
551
|
-
context.step = "ADD_TRANSITIONS";
|
|
552
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
553
|
-
return { content: [{ type: "text", text: `✅ Identidad Multivectorial configurada.\n\nPASO 3: Define una transición. (Usa action: 'add_transition' con { label, destino, accion }).` }] };
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (action === "add_transition" && context.step === "ADD_TRANSITIONS") {
|
|
557
|
-
context.transitions[data.label] = { destino: data.destino, accion: data.accion };
|
|
558
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
559
|
-
return { content: [{ type: "text", text: `🔄 Transición '${data.label}' añadida.\n\n¿Quieres añadir otra o guardar? (Usa action: 'save' para finalizar)` }] };
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (action === "save" && context.step === "ADD_TRANSITIONS") {
|
|
563
|
-
const result = await server.callTool("upsert_node", {
|
|
564
|
-
mapName: context.mapName,
|
|
565
|
-
nodeName: context.nodeName,
|
|
566
|
-
nodeData: {
|
|
567
|
-
fingerprint: context.fingerprint,
|
|
568
|
-
transiciones: context.transitions
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
await fs.unlink(contextPath); // Reset wizard
|
|
572
|
-
return result;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return { content: [{ type: "text", text: `❌ Acción no permitida en el estado actual (${context.step}).` }] };
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
server.tool("begin_design", "Iniciar una sesión de diseño guiado para una aplicación", {
|
|
579
|
-
appName: z.string().describe("Nombre de la aplicación a diseñar (ej: 'AdvantageShopping').")
|
|
580
|
-
}, async ({ appName }) => {
|
|
581
|
-
const rules = `
|
|
582
|
-
🎭 HAS ENTRADO EN MODO DISEÑO (SMS ARCHITECT) 🎭
|
|
583
|
-
Estás diseñando la estructura para: ${appName}
|
|
584
|
-
|
|
585
|
-
REGLAS CRÍTICAS DE SUPERVIVENCIA:
|
|
586
|
-
1. ❌ PROHIBIDO editar archivos JSON manualmente.
|
|
587
|
-
2. ❌ PROHIBIDO usar los campos 'nodes' o 'edges'. NO SOMOS UNA LIBRERÍA DE GRÁFICOS.
|
|
588
|
-
3. ✅ USA EXCLUSIVAMENTE 'upsert_node' para crear o actualizar estados.
|
|
589
|
-
4. ✅ ESTRUCTURA OBLIGATORIA: { "nodos": { "NOMBRE_NODO": { "transiciones": {} } } }
|
|
590
|
-
5. 🔄 FLUJO: Primero propón el diseño en texto -> Pide confirmación -> Usa la herramienta.
|
|
591
|
-
|
|
592
|
-
Dime qué pantalla o flujo quieres empezar a mapear para ${appName}.
|
|
593
|
-
`;
|
|
594
|
-
return { content: [{ type: "text", text: rules }] };
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de plantilla", {
|
|
598
|
-
force: z.optional(z.boolean()).default(false)
|
|
599
|
-
}, async ({ force }) => {
|
|
600
|
-
await ensureDirectories();
|
|
601
|
-
|
|
602
|
-
const templates = {
|
|
603
|
-
'maps/template_map.json': {
|
|
604
|
-
nodos: {
|
|
605
|
-
HOME: {
|
|
606
|
-
fingerprint: { selector: "~Login", timeout: 5000 },
|
|
607
|
-
transiciones: {
|
|
608
|
-
IR_A_LOGIN: { destino: "LOGIN", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Login\" }" }
|
|
609
|
-
}
|
|
610
|
-
},
|
|
611
|
-
LOGIN: {
|
|
612
|
-
fingerprint: { selector: "~Back", timeout: 5000 },
|
|
613
|
-
transiciones: {
|
|
614
|
-
VOLVER: { destino: "HOME", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Back\" }" }
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
},
|
|
619
|
-
'test_cases/template_case.json': {
|
|
620
|
-
name: "Template Case",
|
|
621
|
-
steps: [
|
|
622
|
-
{ name: "Ir a Login", action: "IR_A_LOGIN" },
|
|
623
|
-
{ name: "Verificar Pantalla", action: "mcp:wdio-mcp/get_screenshot {}" }
|
|
624
|
-
]
|
|
625
|
-
},
|
|
626
|
-
'suites/template_suite.json': {
|
|
627
|
-
name: "Template Suite",
|
|
628
|
-
state_map: "template_map.json",
|
|
629
|
-
tests: ["template_case"],
|
|
630
|
-
beforeSuite: ["mcp:wdio-mcp/start_session { \"platform\": \"android\", \"provider\": \"perfecto\" }"],
|
|
631
|
-
afterSuite: ["mcp:wdio-mcp/close_session {}"]
|
|
632
|
-
}
|
|
633
|
-
};
|
|
634
|
-
|
|
635
|
-
for (const [relPath, content] of Object.entries(templates)) {
|
|
636
|
-
const fullPath = path.join(__dirname, relPath);
|
|
637
|
-
try {
|
|
638
|
-
if (!force) {
|
|
639
|
-
try {
|
|
640
|
-
await fs.access(fullPath);
|
|
641
|
-
continue; // Skip if exists
|
|
642
|
-
} catch (e) { }
|
|
643
|
-
}
|
|
644
|
-
await fs.writeFile(fullPath, JSON.stringify(content, null, 2));
|
|
645
|
-
} catch (e) {
|
|
646
|
-
process.stderr.write(`[WARN] Error creando template ${relPath}: ${e.message}\n`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
content: [{
|
|
652
|
-
type: "text",
|
|
653
|
-
text: "✅ Instalación completada con éxito.\n\nComo este es un proyecto nuevo, el siguiente paso es definir tu aplicación. Por favor, llama a la herramienta `begin_design` para empezar a mapear tus primeros estados."
|
|
654
|
-
}]
|
|
655
|
-
};
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
server.tool("framework_menu", "Panel de control para elegir entre Modo Diseño o Modo Ejecución", {}, async () => {
|
|
659
|
-
return {
|
|
660
|
-
content: [{
|
|
661
|
-
type: "text",
|
|
662
|
-
text: "🎮 **Framework Control Panel**\n\n¿Qué deseas hacer ahora?\n1. 🎨 **Modo Diseño**: Añadir o modificar nodos y mapas (Usa `begin_design`).\n2. 🚀 **Modo Ejecución**: Lanzar suites de pruebas existentes (Usa `execute_suite`).\n3. 🔍 **Auditoría**: Revisar la integridad del sistema (Usa `inspect_framework`)."
|
|
663
|
-
}]
|
|
664
|
-
};
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
async function ensureDirectories() {
|
|
668
|
-
const dirs = ['maps', 'suites', 'test_cases', 'reports', 'data'];
|
|
669
|
-
for (const dir of dirs) {
|
|
670
|
-
const dirPath = path.join(__dirname, dir);
|
|
671
|
-
try {
|
|
672
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
673
|
-
} catch (e) {
|
|
674
|
-
process.stderr.write(`[WARN] Error creando directorio ${dir}: ${e.message}\n`);
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async function test_wizard(action, data) {
|
|
680
|
-
const contextPath = path.join(__dirname, '.test_wizard_context.json');
|
|
681
|
-
let context = { step: "START", steps: [] };
|
|
682
|
-
|
|
683
|
-
try {
|
|
684
|
-
if (await fs.stat(contextPath)) {
|
|
685
|
-
context = JSON.parse(await fs.readFile(contextPath, 'utf8'));
|
|
686
|
-
}
|
|
687
|
-
} catch (e) {}
|
|
688
|
-
|
|
689
|
-
if (action === "start") {
|
|
690
|
-
context = { step: "STEP_BASIC", testName: data.testName, steps: [] };
|
|
691
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
692
|
-
return { content: [{ type: "text", text: `📝 Creando Test Case: '${data.testName}'.\n\nPASO 1: Define el nombre y acción del primer paso. (Usa action: 'add_step' con { name, action }).` }] };
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
if (action === "add_step" && context.step === "STEP_BASIC") {
|
|
696
|
-
context.currentStep = { name: data.name, action: data.action };
|
|
697
|
-
context.step = "STEP_ASSERT";
|
|
698
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
699
|
-
return { content: [{ type: "text", text: `✅ Paso '${data.name}' añadido.\n\nPASO 2 (Opcional): ¿Quieres añadir una validación (assert) técnica? (Usa action: 'add_assert' con { assert } o 'add_step' para el siguiente sin assert).` }] };
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
if (action === "add_assert" && context.step === "STEP_ASSERT") {
|
|
703
|
-
context.currentStep.assert = data.assert;
|
|
704
|
-
context.steps.push(context.currentStep);
|
|
705
|
-
context.step = "STEP_BASIC";
|
|
706
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
707
|
-
return { content: [{ type: "text", text: `🛡️ Assert añadido al paso.\n\n¿Siguiente paso? (Usa 'add_step') o finaliza con 'save'.` }] };
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (action === "save") {
|
|
711
|
-
if (context.currentStep && context.step === "STEP_ASSERT") context.steps.push(context.currentStep);
|
|
712
|
-
const result = await server.callTool("save_test_case", {
|
|
713
|
-
name: context.testName,
|
|
714
|
-
steps: context.steps
|
|
715
|
-
});
|
|
716
|
-
await fs.unlink(contextPath);
|
|
717
|
-
return result;
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
async function main() {
|
|
722
|
-
await ensureDirectories();
|
|
723
|
-
await maquina.cargar();
|
|
724
|
-
const transport = new StdioServerTransport();
|
|
725
|
-
await server.connect(transport);
|
|
726
|
-
}
|
|
727
|
-
main().catch(console.error);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import fs from "fs/promises";
|
|
8
|
+
import fsSync from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { exec } from "child_process";
|
|
12
|
+
import { promisify } from "util";
|
|
13
|
+
|
|
14
|
+
const execAsync = promisify(exec);
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = path.dirname(__filename);
|
|
17
|
+
|
|
18
|
+
const SUITES_DIR = path.join(__dirname, "suites");
|
|
19
|
+
const CASOS_DIR = path.join(__dirname, "test_cases");
|
|
20
|
+
const REPORTS_ROOT = path.join(__dirname, "reports");
|
|
21
|
+
const CONFIG_FILE = path.join(__dirname, "mcp_config.json");
|
|
22
|
+
|
|
23
|
+
export class MaquinaDeEstados {
|
|
24
|
+
constructor() {
|
|
25
|
+
this.nodos = new Map();
|
|
26
|
+
this.casosPrueba = new Map();
|
|
27
|
+
this.suites = new Map();
|
|
28
|
+
this.mcpClients = new Map();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async cargar() {
|
|
32
|
+
for (const dir of [CASOS_DIR, SUITES_DIR]) {
|
|
33
|
+
if (!fsSync.existsSync(dir)) await fs.mkdir(dir, { recursive: true });
|
|
34
|
+
const files = await fs.readdir(dir);
|
|
35
|
+
for (const f of files) {
|
|
36
|
+
if (f.endsWith(".json")) {
|
|
37
|
+
const content = JSON.parse(await fs.readFile(path.join(dir, f), "utf-8"));
|
|
38
|
+
if (dir === CASOS_DIR) this.casosPrueba.set(content.name, content);
|
|
39
|
+
else this.suites.set(content.name, content);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getMcpClient(serverName) {
|
|
46
|
+
if (this.mcpClients.has(serverName)) return this.mcpClients.get(serverName);
|
|
47
|
+
const configRaw = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
48
|
+
const config = JSON.parse(configRaw).mcpServers[serverName];
|
|
49
|
+
const transport = new StdioClientTransport({
|
|
50
|
+
command: config.command,
|
|
51
|
+
args: config.args || [],
|
|
52
|
+
env: { ...process.env, ...config.env }
|
|
53
|
+
});
|
|
54
|
+
const client = new Client({ name: "SMS-Client", version: "12.5.0" }, { capabilities: {} });
|
|
55
|
+
await client.connect(transport);
|
|
56
|
+
const data = { client, transport };
|
|
57
|
+
this.mcpClients.set(serverName, data);
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interpolate(text, data) {
|
|
62
|
+
if (!data) return text;
|
|
63
|
+
let result = text;
|
|
64
|
+
for (const key in data) { result = result.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); }
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async ejecutarSuite(suiteName) {
|
|
69
|
+
const suite = this.suites.get(suiteName);
|
|
70
|
+
if (!suite) throw new Error(`Suite '${suiteName}' no encontrada.`);
|
|
71
|
+
|
|
72
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
73
|
+
const reportDir = path.join(REPORTS_ROOT, `exec_${suiteName}_${timestamp}`);
|
|
74
|
+
await fs.mkdir(reportDir, { recursive: true });
|
|
75
|
+
const results = { suiteName, timestamp, cases: [], hooks: { beforeSuite: [], afterSuite: [] } };
|
|
76
|
+
|
|
77
|
+
const runActions = async (actions, targetRes, data) => {
|
|
78
|
+
if (!actions) return;
|
|
79
|
+
for (const action of actions) {
|
|
80
|
+
const actionRes = { action, status: "passed" };
|
|
81
|
+
targetRes.push(actionRes);
|
|
82
|
+
try {
|
|
83
|
+
const executeAction = async (act) => {
|
|
84
|
+
const subRes = (act === action) ? actionRes : { action: act, status: "passed" };
|
|
85
|
+
if (act !== action) targetRes.push(subRes);
|
|
86
|
+
|
|
87
|
+
if (act.startsWith("transicion:")) {
|
|
88
|
+
const transName = act.replace("transicion:", "").trim();
|
|
89
|
+
const mapPath = path.join(__dirname, 'maps', suite.state_map);
|
|
90
|
+
const mapData = JSON.parse(await fs.readFile(mapPath, 'utf8'));
|
|
91
|
+
const nodes = mapData.nodos || mapData;
|
|
92
|
+
|
|
93
|
+
let foundAction, destNodeName;
|
|
94
|
+
for (const [nodeName, node] of Object.entries(nodes)) {
|
|
95
|
+
if (node.transiciones && node.transiciones[transName]) {
|
|
96
|
+
foundAction = node.transiciones[transName].accion;
|
|
97
|
+
destNodeName = node.transiciones[transName].destino;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!foundAction) throw new Error(`Transición '${transName}' no encontrada.`);
|
|
102
|
+
await executeAction(foundAction);
|
|
103
|
+
|
|
104
|
+
// Identidad
|
|
105
|
+
const destNode = nodes[destNodeName];
|
|
106
|
+
if (destNode && destNode.fingerprint) {
|
|
107
|
+
const selectors = destNode.fingerprint.selectors || [destNode.fingerprint.selector];
|
|
108
|
+
const { client } = await this.getMcpClient("wdio-mcp");
|
|
109
|
+
for (const sel of selectors) {
|
|
110
|
+
const vRes = await client.callTool({ name: "wait_for_element", arguments: { selector: sel, timeout: 5000 } });
|
|
111
|
+
if (vRes.isError) throw new Error(`Identidad fallida: ${sel}`);
|
|
112
|
+
}
|
|
113
|
+
subRes.output = (subRes.output || "") + "\n✅ Identidad Multivectorial confirmada.";
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const interpolated = this.interpolate(act, data);
|
|
119
|
+
if (interpolated.startsWith("sh:")) {
|
|
120
|
+
const { stdout, stderr } = await execAsync(interpolated.replace("sh:", "").trim());
|
|
121
|
+
subRes.output = (subRes.output || "") + "\n" + stdout + (stderr ? "\nERR: " + stderr : "");
|
|
122
|
+
} else if (interpolated.startsWith("mcp:")) {
|
|
123
|
+
const raw = interpolated.replace("mcp:", "").trim();
|
|
124
|
+
const slash = raw.indexOf("/");
|
|
125
|
+
const space = raw.indexOf(" ", slash);
|
|
126
|
+
const srv = raw.substring(0, slash);
|
|
127
|
+
const tool = space === -1 ? raw.substring(slash + 1) : raw.substring(slash + 1, space);
|
|
128
|
+
const args = JSON.parse(space === -1 ? "{}" : raw.substring(space + 1));
|
|
129
|
+
const { client } = await this.getMcpClient(srv);
|
|
130
|
+
const res = await client.callTool({ name: tool, arguments: args });
|
|
131
|
+
if (res.content) {
|
|
132
|
+
for (const item of res.content) {
|
|
133
|
+
if (item.text) subRes.output = (subRes.output || "") + "\n" + item.text;
|
|
134
|
+
if (item.type === "image") subRes.image = item.data.startsWith("data:") ? item.data : `data:image/png;base64,${item.data}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
await executeAction(action);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
actionRes.status = "failed";
|
|
142
|
+
actionRes.error = e.message;
|
|
143
|
+
throw e;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
for (const caseName of suite.tests) {
|
|
150
|
+
const testCase = this.casosPrueba.get(caseName);
|
|
151
|
+
if (!testCase) continue;
|
|
152
|
+
const caseRes = { name: testCase.name, steps: [], status: "passed" };
|
|
153
|
+
results.cases.push(caseRes);
|
|
154
|
+
for (const step of testCase.steps) {
|
|
155
|
+
const stepRes = { name: step.name, actions: [], status: "passed" };
|
|
156
|
+
caseRes.steps.push(stepRes);
|
|
157
|
+
try {
|
|
158
|
+
await runActions(step.actions || [step.action], stepRes.actions);
|
|
159
|
+
if (step.assert) {
|
|
160
|
+
const aRes = { action: `[ASSERT] ${step.assert}`, status: "passed" };
|
|
161
|
+
stepRes.actions.push(aRes);
|
|
162
|
+
await runActions([step.assert], stepRes.actions);
|
|
163
|
+
}
|
|
164
|
+
} catch (e) { stepRes.status = "failed"; throw e; }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} finally {
|
|
168
|
+
for (const { transport } of this.mcpClients.values()) await transport.close();
|
|
169
|
+
this.mcpClients.clear();
|
|
170
|
+
await this.finalizarReporte(reportDir, results);
|
|
171
|
+
}
|
|
172
|
+
return reportDir;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async finalizarReporte(dir, data) {
|
|
176
|
+
const renderActions = (actions) => actions.map(a => {
|
|
177
|
+
const isAssert = a.action.startsWith("[ASSERT]");
|
|
178
|
+
const isIdentity = a.output && a.output.includes("Identidad");
|
|
179
|
+
return `
|
|
180
|
+
<div style="border-left: 4px solid ${a.status === 'passed' ? (isAssert ? '#10b981' : '#3b82f6') : '#ef4444'}; background: rgba(15, 23, 42, 0.8); padding: 20px; border-radius: 12px; margin-bottom: 15px; border: 1px solid #1e293b;">
|
|
181
|
+
<div style="display:flex; justify-content:space-between; margin-bottom: 10px;">
|
|
182
|
+
<span style="font-weight: 600; color: ${isAssert ? '#10b981' : '#94a3b8'};">${isAssert ? '🛡️ VALIDACIÓN' : '⚙️ ACCIÓN'}</span>
|
|
183
|
+
<span style="background:${a.status === 'passed' ? '#065f46' : '#991b1b'}; padding: 2px 8px; border-radius: 4px; font-size: 0.8em;">${a.status}</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div style="color: #f8fafc; margin-bottom: 10px;">${isAssert ? a.action.replace("[ASSERT]", "").trim() : a.action}</div>
|
|
186
|
+
${isIdentity ? `<div style="color: #34d399; font-size: 0.85em;">🔍 Identidad Multivectorial Confirmada</div>` : ""}
|
|
187
|
+
${a.image ? `<div style="margin-top:10px; text-align:center;"><img src="${a.image}" style="max-width:100%; border-radius:8px;"></div>` : ""}
|
|
188
|
+
${a.error ? `<pre style="color:#fca5a5; font-size:0.8em;">${a.error}</pre>` : ""}
|
|
189
|
+
</div>`;
|
|
190
|
+
}).join("");
|
|
191
|
+
|
|
192
|
+
const html = `<html><body style="background:#020617; color:#f8fafc; font-family:sans-serif; padding:50px;">
|
|
193
|
+
<h1 style="color:#60a5fa;">SMS Framework V12.5</h1>
|
|
194
|
+
${data.cases.map(c => `
|
|
195
|
+
<div style="background:#0f172a; padding:30px; border-radius:20px; margin-bottom:30px; border:1px solid #1e293b;">
|
|
196
|
+
<h2>CASE: ${c.name}</h2>
|
|
197
|
+
${c.steps.map(s => `<div><h3>STEP: ${s.name}</h3>${renderActions(s.actions)}</div>`).join("")}
|
|
198
|
+
</div>`).join("")}
|
|
199
|
+
</body></html>`;
|
|
200
|
+
await fs.writeFile(path.join(dir, "index.html"), html);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const maquina = new MaquinaDeEstados();
|
|
205
|
+
const server = new McpServer({ name: "demo-state-machine", version: "12.5.0" });
|
|
206
|
+
|
|
207
|
+
server.tool("init_project", "Instalación Inicial. Crea carpetas y archivos de plantilla.", {}, async () => {
|
|
208
|
+
const templates = {
|
|
209
|
+
'maps/template_map.json': { nodos: { HOME: { fingerprint: { selectors: ["~Login"] }, transiciones: { IR_A_LOGIN: { destino: "LOGIN", accion: "mcp:wdio-mcp/click_element {\"selector\":\"~Login\"}" } } } } },
|
|
210
|
+
'test_cases/template_case.json': { name: "TC_Ejemplo", steps: [{ name: "Ir a Login", action: "transicion:IR_A_LOGIN", assert: "sh:ls" }] },
|
|
211
|
+
'suites/template_suite.json': { name: "Suite_Ejemplo", state_map: "template_map.json", tests: ["TC_Ejemplo"] }
|
|
212
|
+
};
|
|
213
|
+
for (const [f, c] of Object.entries(templates)) {
|
|
214
|
+
const p = path.join(__dirname, f);
|
|
215
|
+
if (!fsSync.existsSync(p)) { await fs.mkdir(path.dirname(p), { recursive: true }); await fs.writeFile(p, JSON.stringify(c, null, 2)); }
|
|
216
|
+
}
|
|
217
|
+
return { content: [{ type: "text", text: "✨ Entorno Inicializado. Usa 'sms_builder' para empezar el diseño." }] };
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
server.tool("sms_builder", "Diseño Guiado: Nodos -> Tests -> Suite.", {
|
|
221
|
+
action: z.enum(["start", "add_node", "add_fingerprint", "done_node", "add_test", "add_step", "add_assert", "save_test", "assemble_suite"]),
|
|
222
|
+
data: z.record(z.any())
|
|
223
|
+
}, async ({ action, data }) => {
|
|
224
|
+
const ctxPath = path.join(__dirname, '.sms_builder_context.json');
|
|
225
|
+
let ctx = { step: "IDLE", nodes: {}, tests: [] };
|
|
226
|
+
try { ctx = JSON.parse(fsSync.readFileSync(ctxPath, 'utf8')); } catch (e) {}
|
|
227
|
+
|
|
228
|
+
switch (action) {
|
|
229
|
+
case "start":
|
|
230
|
+
ctx = { step: "DESIGN", mapName: data.mapName, nodes: {}, tests: [] };
|
|
231
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
232
|
+
return { content: [{ type: "text", text: `🏗️ Diseñando Mapa: ${data.mapName}. Añade un nodo (action: 'add_node' { nodeName }).` }] };
|
|
233
|
+
|
|
234
|
+
case "add_node":
|
|
235
|
+
ctx.currentNode = { name: data.nodeName, transiciones: {}, fingerprint: null };
|
|
236
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
237
|
+
return { content: [{ type: "text", text: `📍 Nodo '${data.nodeName}'. MANDATORIO: Añade huella (action: 'add_fingerprint' { selector }).` }] };
|
|
238
|
+
|
|
239
|
+
case "add_fingerprint":
|
|
240
|
+
if (!ctx.currentNode.fingerprint) ctx.currentNode.fingerprint = { selectors: [] };
|
|
241
|
+
ctx.currentNode.fingerprint.selectors.push(data.selector);
|
|
242
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
243
|
+
return { content: [{ type: "text", text: `🧬 Vector añadido. ¿Otro o 'done_node'?` }] };
|
|
244
|
+
|
|
245
|
+
case "done_node":
|
|
246
|
+
ctx.nodes[ctx.currentNode.name] = { fingerprint: ctx.currentNode.fingerprint, transiciones: ctx.currentNode.transiciones };
|
|
247
|
+
await fs.writeFile(path.join(__dirname, 'maps', `${ctx.mapName}.json`), JSON.stringify({ nodos: ctx.nodes }, null, 2));
|
|
248
|
+
delete ctx.currentNode;
|
|
249
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
250
|
+
return { content: [{ type: "text", text: `✅ Nodo guardado. ¿Otro ('add_node') o Tests ('add_test')?` }] };
|
|
251
|
+
|
|
252
|
+
case "add_test":
|
|
253
|
+
ctx.currentTest = { name: data.testName, steps: [] };
|
|
254
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
255
|
+
return { content: [{ type: "text", text: `🧪 Test '${data.testName}'. Añade pasos (action: 'add_step' { name, action }).` }] };
|
|
256
|
+
|
|
257
|
+
case "add_step":
|
|
258
|
+
ctx.currentStep = { name: data.name, action: data.action };
|
|
259
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
260
|
+
return { content: [{ type: "text", text: `✅ Paso añadido. ¿Assert? (action: 'add_assert' { assert }) o siguiente ('add_step').` }] };
|
|
261
|
+
|
|
262
|
+
case "add_assert":
|
|
263
|
+
ctx.currentStep.assert = data.assert;
|
|
264
|
+
ctx.currentTest.steps.push(ctx.currentStep);
|
|
265
|
+
delete ctx.currentStep;
|
|
266
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
267
|
+
return { content: [{ type: "text", text: `🛡️ Assert añadido. ¿Siguiente paso ('add_step') o guardar ('save_test')?` }] };
|
|
268
|
+
|
|
269
|
+
case "save_test":
|
|
270
|
+
if (ctx.currentStep) ctx.currentTest.steps.push(ctx.currentStep);
|
|
271
|
+
await fs.writeFile(path.join(__dirname, 'test_cases', `${ctx.currentTest.name}.json`), JSON.stringify(ctx.currentTest, null, 2));
|
|
272
|
+
ctx.tests.push(ctx.currentTest.name);
|
|
273
|
+
delete ctx.currentTest;
|
|
274
|
+
fsSync.writeFileSync(ctxPath, JSON.stringify(ctx, null, 2));
|
|
275
|
+
return { content: [{ type: "text", text: `✅ Test guardado. ¿Otro ('add_test') o Suite ('assemble_suite')?` }] };
|
|
276
|
+
|
|
277
|
+
case "assemble_suite":
|
|
278
|
+
const suite = { name: data.suiteName, state_map: `${ctx.mapName}.json`, tests: ctx.tests };
|
|
279
|
+
await fs.writeFile(path.join(__dirname, 'suites', `${data.suiteName}.json`), JSON.stringify(suite, null, 2));
|
|
280
|
+
fsSync.unlinkSync(ctxPath);
|
|
281
|
+
return { content: [{ type: "text", text: `🎊 Suite '${data.suiteName}' lista. Corre con 'sms_executor'.` }] };
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
server.tool("sms_executor", "Corre una suite y genera reporte.", { suiteName: z.string() }, async ({ suiteName }) => {
|
|
286
|
+
await maquina.cargar();
|
|
287
|
+
const dir = await maquina.ejecutarSuite(suiteName);
|
|
288
|
+
return { content: [{ type: "text", text: `🚀 Reporte: ${dir}` }] };
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
async function main() {
|
|
292
|
+
for (const d of ['maps', 'suites', 'test_cases', 'reports']) {
|
|
293
|
+
const p = path.join(__dirname, d);
|
|
294
|
+
if (!fsSync.existsSync(p)) await fs.mkdir(p, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
await maquina.cargar();
|
|
297
|
+
const transport = new StdioServerTransport();
|
|
298
|
+
await server.connect(transport);
|
|
299
|
+
}
|
|
300
|
+
main().catch(console.error);
|