mcp-state-machine-test-framework 1.0.7 → 1.0.8
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/README.md +7 -4
- package/index.js +91 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,10 +27,13 @@ Or run it directly via MCP in your configuration:
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## 🏁 Basic Workflow
|
|
30
|
-
1. **Initialize
|
|
31
|
-
2. **
|
|
32
|
-
3. **
|
|
33
|
-
4. **
|
|
30
|
+
1. **Initialize Project**: Run `init_project` to setup folders and templates.
|
|
31
|
+
2. **Guided Design**: Tell the agent to "Create a new node". The agent will ask for the mandatory fields (Map, Node Name, Transitions) before executing.
|
|
32
|
+
3. **Automatic Visualization**: Every map change regenerates a Mermaid diagram in `/maps`.
|
|
33
|
+
4. **Execution**: Run `execute_suite` to launch the automated flow.
|
|
34
|
+
|
|
35
|
+
## 🤖 AI Agent Interaction (Guided Design)
|
|
36
|
+
This framework enforces a "Strict Contract" via MCP tools. Agents are instructed NOT to edit JSON files directly, but to use the management tools (`upsert_node`, `save_test_case`, `save_suite`) which provide validation and structure.
|
|
34
37
|
|
|
35
38
|
## 📚 Technical Documentation
|
|
36
39
|
- [🧠 Technical Protocol & History](./SMS_Protocol.md)
|
package/index.js
CHANGED
|
@@ -326,18 +326,20 @@ export class MaquinaDeEstados {
|
|
|
326
326
|
|
|
327
327
|
const maquina = new MaquinaDeEstados();
|
|
328
328
|
const server = new McpServer({ name: "demo-state-machine", version: "11.1.0" });
|
|
329
|
-
server.tool("execute_suite", "Ejecución de Suite Completa", {
|
|
329
|
+
server.tool("execute_suite", "Ejecución de Suite Completa", {
|
|
330
|
+
name: z.string().describe("Nombre de la suite a ejecutar (ej: 'Suite_Login'). Buscará el archivo en /suites.")
|
|
331
|
+
}, async ({ name }) => {
|
|
330
332
|
await maquina.cargar();
|
|
331
333
|
const dir = await maquina.ejecutarSuite(name);
|
|
332
334
|
return { content: [{ type: "text", text: `Reporte: ${dir}` }] };
|
|
333
335
|
});
|
|
334
336
|
|
|
335
|
-
server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados", {
|
|
336
|
-
mapName: z.string(),
|
|
337
|
-
nodeName: z.string(),
|
|
337
|
+
server.tool("upsert_node", "Añadir o actualizar un nodo en el mapa de estados.", {
|
|
338
|
+
mapName: z.string().describe("Nombre del archivo del mapa (ej: 'home_map.json'). El servidor lo buscará en la carpeta /maps."),
|
|
339
|
+
nodeName: z.string().describe("Identificador único del nodo (ej: 'LOGIN_PAGE', 'HOME')."),
|
|
338
340
|
nodeData: z.object({
|
|
339
|
-
transiciones: z.any().optional()
|
|
340
|
-
}).passthrough()
|
|
341
|
+
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 ...' } }")
|
|
342
|
+
}).passthrough().describe("Datos completos del nodo, incluyendo transiciones y metadatos.")
|
|
341
343
|
}, async ({ mapName, nodeName, nodeData }) => {
|
|
342
344
|
const mapPath = path.join(__dirname, 'maps', mapName);
|
|
343
345
|
let map = { nodos: {} };
|
|
@@ -403,12 +405,12 @@ server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del
|
|
|
403
405
|
return { content: [{ type: "text", text: JSON.stringify(entities, null, 2) }] };
|
|
404
406
|
});
|
|
405
407
|
|
|
406
|
-
server.tool("save_test_case", "Crear o actualizar un caso de prueba", {
|
|
407
|
-
name: z.string(),
|
|
408
|
+
server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
|
|
409
|
+
name: z.string().describe("Nombre identificador del test (ej: 'TC_Login_Exitoso')."),
|
|
408
410
|
steps: z.array(z.object({
|
|
409
|
-
name: z.string().optional(),
|
|
410
|
-
action: z.string().optional()
|
|
411
|
-
}).passthrough())
|
|
411
|
+
name: z.string().optional().describe("Descripción amigable del paso (ej: 'Ingresar Usuario')."),
|
|
412
|
+
action: z.string().optional().describe("Acción a realizar. Puede ser un nombre de transición del mapa o un comando mcp:wdio-mcp/...")
|
|
413
|
+
}).passthrough()).describe("Lista ordenada de pasos lógicos a ejecutar.")
|
|
412
414
|
}, async ({ name, steps }) => {
|
|
413
415
|
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
414
416
|
const filePath = path.join(__dirname, 'test_cases', fileName);
|
|
@@ -418,12 +420,12 @@ server.tool("save_test_case", "Crear o actualizar un caso de prueba", {
|
|
|
418
420
|
return { content: [{ type: "text", text: `Caso de prueba '${name}' guardado correctamente.` }] };
|
|
419
421
|
});
|
|
420
422
|
|
|
421
|
-
server.tool("save_suite", "Crear o actualizar una suite de pruebas", {
|
|
422
|
-
name: z.string(),
|
|
423
|
-
state_map: z.string(),
|
|
424
|
-
tests: z.array(z.string()),
|
|
425
|
-
beforeSuite: z.array(z.string()).optional().default([]),
|
|
426
|
-
afterSuite: z.array(z.string()).optional().default([])
|
|
423
|
+
server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
|
|
424
|
+
name: z.string().describe("Nombre de la suite (ej: 'Suite_E2E_Sanity')."),
|
|
425
|
+
state_map: z.string().describe("Archivo del mapa de estados a usar (ej: 'mob_perfecto_map.json')."),
|
|
426
|
+
tests: z.array(z.string()).describe("Lista de nombres de casos de prueba a incluir en la suite."),
|
|
427
|
+
beforeSuite: z.array(z.string()).optional().default([]).describe("Acciones globales antes de la suite (ej: iniciar sesión)."),
|
|
428
|
+
afterSuite: z.array(z.string()).optional().default([]).describe("Acciones globales tras la suite (ej: cerrar sesión).")
|
|
427
429
|
}, async ({ name, state_map, tests, beforeSuite, afterSuite }) => {
|
|
428
430
|
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
429
431
|
const filePath = path.join(__dirname, 'suites', fileName);
|
|
@@ -433,5 +435,76 @@ server.tool("save_suite", "Crear o actualizar una suite de pruebas", {
|
|
|
433
435
|
return { content: [{ type: "text", text: `Suite '${name}' guardada correctamente.` }] };
|
|
434
436
|
});
|
|
435
437
|
|
|
436
|
-
|
|
438
|
+
server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de plantilla", {
|
|
439
|
+
force: z.optional(z.boolean()).default(false)
|
|
440
|
+
}, async ({ force }) => {
|
|
441
|
+
await ensureDirectories();
|
|
442
|
+
|
|
443
|
+
const templates = {
|
|
444
|
+
'maps/template_map.json': {
|
|
445
|
+
nodos: {
|
|
446
|
+
HOME: {
|
|
447
|
+
transiciones: {
|
|
448
|
+
IR_A_LOGIN: { destino: "LOGIN", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Login\" }" }
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
LOGIN: {
|
|
452
|
+
transiciones: {
|
|
453
|
+
VOLVER: { destino: "HOME", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Back\" }" }
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
'test_cases/template_case.json': {
|
|
459
|
+
name: "Template Case",
|
|
460
|
+
steps: [
|
|
461
|
+
{ name: "Ir a Login", action: "IR_A_LOGIN" },
|
|
462
|
+
{ name: "Verificar Pantalla", action: "mcp:wdio-mcp/get_screenshot {}" }
|
|
463
|
+
]
|
|
464
|
+
},
|
|
465
|
+
'suites/template_suite.json': {
|
|
466
|
+
name: "Template Suite",
|
|
467
|
+
state_map: "template_map.json",
|
|
468
|
+
tests: ["template_case"],
|
|
469
|
+
beforeSuite: ["mcp:wdio-mcp/start_session { \"platform\": \"android\", \"provider\": \"perfecto\" }"],
|
|
470
|
+
afterSuite: ["mcp:wdio-mcp/close_session {}"]
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
for (const [relPath, content] of Object.entries(templates)) {
|
|
475
|
+
const fullPath = path.join(__dirname, relPath);
|
|
476
|
+
try {
|
|
477
|
+
if (!force) {
|
|
478
|
+
try {
|
|
479
|
+
await fs.access(fullPath);
|
|
480
|
+
continue; // Skip if exists
|
|
481
|
+
} catch (e) {}
|
|
482
|
+
}
|
|
483
|
+
await fs.writeFile(fullPath, JSON.stringify(content, null, 2));
|
|
484
|
+
} catch (e) {
|
|
485
|
+
process.stderr.write(`[WARN] Error creando template ${relPath}: ${e.message}\n`);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return { content: [{ type: "text", text: "✅ Proyecto inicializado. Se han creado las carpetas y los archivos de plantilla (templates) para guiar al agente." }] };
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
async function ensureDirectories() {
|
|
493
|
+
const dirs = ['maps', 'suites', 'test_cases', 'reports', 'data'];
|
|
494
|
+
for (const dir of dirs) {
|
|
495
|
+
const dirPath = path.join(__dirname, dir);
|
|
496
|
+
try {
|
|
497
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
498
|
+
} catch (e) {
|
|
499
|
+
process.stderr.write(`[WARN] Error creando directorio ${dir}: ${e.message}\n`);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function main() {
|
|
505
|
+
await ensureDirectories();
|
|
506
|
+
await maquina.cargar();
|
|
507
|
+
const transport = new StdioServerTransport();
|
|
508
|
+
await server.connect(transport);
|
|
509
|
+
}
|
|
437
510
|
main().catch(console.error);
|