mcp-state-machine-test-framework 1.2.3 → 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/README.md +29 -58
- package/SMS_Protocol.md +45 -47
- package/agent_protocol.md +27 -17
- package/index.js +161 -476
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,60 +1,31 @@
|
|
|
1
|
-
# 🚀
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
npm install
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## 🏁 Basic Workflow
|
|
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.
|
|
37
|
-
|
|
38
|
-
## 📚 Technical Documentation
|
|
39
|
-
- [🧠 Technical Protocol & History](./SMS_Protocol.md)
|
|
40
|
-
- [📱 Perfecto Mobile Cloud Setup](./DOCUMENTATION/Perfecto_Setup.md)
|
|
41
|
-
- [🤖 Agent Guidelines](./agent_protocol.md)
|
|
42
|
-
|
|
43
|
-
## 🤖 AI Agent Quickstart (For Copilot/Cursor/Antigravity)
|
|
44
|
-
If you are an AI agent helping with this project, follow these rules:
|
|
45
|
-
1. **Environment**: This is a Node.js project. Always use `npm`.
|
|
46
|
-
2. **Setup**: Install via `npm install mcp-state-machine-test-framework`.
|
|
47
|
-
3. **Configuration**: Use `npx mcp-sms` as the command in `mcp_config.json`.
|
|
48
|
-
4. **Initialization**: Your very first action after installation **MUST** be calling the `init_project` tool to create the directory structure and templates.
|
|
49
|
-
5. **Workflow**: Use `inspect_framework` to understand the current state before proposing changes.
|
|
50
|
-
|
|
51
|
-
## 🚀 Mission Bootstrap (Copy & Paste to any AI Agent)
|
|
52
|
-
> **YOUR ONLY MISSION**:
|
|
53
|
-
> 1. Run `npx -y -p mcp-state-machine-test-framework mcp-sms-init` to setup EVERYTHING.
|
|
54
|
-
> 2. Use `design_wizard` (action: 'start') to design nodes step-by-step.
|
|
55
|
-
> 3. **STRICT RULE**: Never edit JSON files manually.
|
|
56
|
-
>
|
|
57
|
-
> "Run `npx -y -p mcp-state-machine-test-framework mcp-sms-init`. Then start a design session using `design_wizard` (action: 'start'). Follow the wizard steps to create nodes and transitions."
|
|
1
|
+
# 🚀 SMS Framework: State Machine Testing V12.0
|
|
2
|
+
|
|
3
|
+
> **Orquestación de Pruebas E2E de Alta Fidelidad mediante Máquina de Estados Procedural.**
|
|
4
|
+
|
|
5
|
+
Este framework transforma la automatización tradicional en un sistema de orquestación autónomo donde la **Identidad de los Nodos** y la **Verificación Técnica** garantizan pruebas irrompibles en Web, Mobile y API.
|
|
6
|
+
|
|
7
|
+
## 🌟 Características Principales
|
|
8
|
+
|
|
9
|
+
* **🛡️ Identidad Multivectorial (Fingerprinting)**: Los nodos no son solo nombres; tienen una huella digital (múltiples selectores) que el motor valida automáticamente para asegurar la sincronización total entre el mapa y la realidad.
|
|
10
|
+
* **⚙️ Ejecución Híbrida (MCP + SH)**: Soporte nativo para comandos de cualquier servidor MCP y comandos de Shell (`sh:`) en un mismo flujo.
|
|
11
|
+
* **🧪 Asserts Técnicos por Paso**: Cada paso del test puede incluir validaciones profundas (DB, API, Logs) mediante el campo `assert`.
|
|
12
|
+
* **🧙♂️ Wizards Integrados**: Asistentes guiados (`design_wizard` y `test_wizard`) para construir mapas y pruebas sin errores de sintaxis.
|
|
13
|
+
* **📊 Dashboard Premium**: Reportes HTML con glassmorphism que distinguen visualmente entre acciones, validaciones de identidad y asserts.
|
|
14
|
+
|
|
15
|
+
## 🚀 Inicio Rápido
|
|
16
|
+
|
|
17
|
+
1. **Instalación**: `npm install`
|
|
18
|
+
2. **Inicialización**: Usa el comando `init_project` para generar la estructura de carpetas.
|
|
19
|
+
3. **Diseño**: Usa `design_wizard` para crear tu primer mapa con identidad protegida.
|
|
20
|
+
4. **Prueba**: Usa `test_wizard` para ensamblar tu flujo de prueba.
|
|
21
|
+
5. **Ejecución**: `ejecutarSuite(suiteName)`
|
|
22
|
+
|
|
23
|
+
## 📁 Estructura del Proyecto
|
|
24
|
+
|
|
25
|
+
* `/maps`: Definiciones de la Máquina de Estados (Nodos, Fingerprints, Transiciones).
|
|
26
|
+
* `/test_cases`: Pasos lógicos de prueba con acciones y asserts.
|
|
27
|
+
* `/suites`: El pegamento que une mapas, pruebas y hooks globales.
|
|
28
|
+
* `/reports`: Evidencia técnica y visual con alta fidelidad.
|
|
58
29
|
|
|
59
30
|
---
|
|
60
|
-
*
|
|
31
|
+
*Desarrollado para la era de la automatización autónoma.*
|
package/SMS_Protocol.md
CHANGED
|
@@ -1,49 +1,47 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
Este documento
|
|
4
|
-
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
###
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
- **`save_test_case`**: Normaliza la creación de pasos de prueba, asegurando que la jerarquía Suite -> Case -> Step se mantenga para el generador de reportes.
|
|
46
|
-
- **`save_suite`**: Orquesta el ensamblaje final, permitiendo la inyección de hooks (`before/afterSuite`) que son críticos para la gestión de sesiones en la nube.
|
|
1
|
+
# 📑 SMS Protocol: Referencia Técnica V12.0
|
|
2
|
+
|
|
3
|
+
Este documento detalla los esquemas de datos para la orquestación de la Máquina de Estados.
|
|
4
|
+
|
|
5
|
+
## 🗺️ Esquema del Mapa (`/maps/*.json`)
|
|
6
|
+
|
|
7
|
+
### Estructura de Nodo
|
|
8
|
+
```json
|
|
9
|
+
"NOMBRE_NODO": {
|
|
10
|
+
"fingerprint": {
|
|
11
|
+
"selectors": ["~logo", "id:main_container"],
|
|
12
|
+
"timeout": 5000
|
|
13
|
+
},
|
|
14
|
+
"transiciones": {
|
|
15
|
+
"NOMBRE_TRANSICION": {
|
|
16
|
+
"destino": "OTRO_NODO",
|
|
17
|
+
"accion": "mcp:wdio-mcp/click_element { \"selector\": \"~btn\" }"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 🧪 Esquema del Test Case (`/test_cases/*.json`)
|
|
24
|
+
|
|
25
|
+
### Estructura de Paso
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"name": "TC_Ejemplo",
|
|
29
|
+
"steps": [
|
|
30
|
+
{
|
|
31
|
+
"name": "Paso 1",
|
|
32
|
+
"action": "TRANSICION_O_COMANDO",
|
|
33
|
+
"assert": "mcp:wdio-mcp/wait_for_element { \"selector\": \"~success_icon\" }"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## ⚙️ Lógica del Motor de Ejecución
|
|
40
|
+
|
|
41
|
+
1. **Resolución de Acción**: Si la acción empieza por `transicion:`, el motor busca en el mapa cargado.
|
|
42
|
+
2. **Ejecución Principal**: Corre la acción (MCP o SH).
|
|
43
|
+
3. **Check de Identidad**: Si el destino tiene `fingerprint`, el motor itera por todos sus `selectors` y valida su presencia.
|
|
44
|
+
4. **Check de Assert**: Si el paso tiene `assert`, se ejecuta como una validación final antes de marcar el paso como "Passed".
|
|
47
45
|
|
|
48
46
|
---
|
|
49
|
-
*
|
|
47
|
+
*Nota: Todos los campos de texto dentro de las acciones y asserts soportan interpolación de datos dinámicos mediante `{{campo}}`.*
|
package/agent_protocol.md
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
# 🤖 Protocolo de Operación del Agente: SMS Framework
|
|
1
|
+
# 🤖 Protocolo de Operación del Agente: SMS Framework V12.0
|
|
2
2
|
|
|
3
|
-
Este protocolo es de **obligado cumplimiento** para
|
|
3
|
+
Este protocolo es de **obligado cumplimiento** para asegurar la integridad de la Máquina de Estados y la calidad de los reportes.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 🛠️ Herramientas de Gestión (MANDATORIO)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **PROHIBIDO**: Crear o editar archivos en `/maps` usando herramientas genéricas de escritura de archivos.
|
|
9
|
-
- **MANDATORIO**: Usar la herramienta `upsert_node` para añadir o modificar nodos. Esto asegura que la estructura de transiciones y acciones sea válida para el motor de ejecución.
|
|
7
|
+
Los agentes **NO** deben crear archivos JSON manualmente si existe un Wizard disponible:
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
1. **Diseño de Mapas**: Usar `design_wizard`.
|
|
10
|
+
* Cada nodo **DEBE** tener una `fingerprint` (huella digital).
|
|
11
|
+
* Se recomienda usar **Múltiples Selectores** (Multivectorial) para evitar ambigüedades entre pantallas similares.
|
|
12
|
+
2. **Creación de Pruebas**: Usar `test_wizard`.
|
|
13
|
+
* Definir un `assert` técnico en pasos críticos (Login, Submit, Delete).
|
|
14
|
+
* Usar el prefijo `sh:` para validaciones de sistema/archivos y `mcp:` para validaciones de UI.
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
- **MANDATORIO**: Usar `save_suite`. El agente debe verificar que el `state_map` referenciado exista antes de guardar la suite.
|
|
16
|
+
## ⚖️ Reglas de Oro
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
2. **Registrar**: Inmediatamente usar `upsert_node` para añadir el hallazgo al mapa de estados.
|
|
20
|
-
3. **Validar**: Ejecutar un `save_test_case` rápido para confirmar que la nueva transición funciona.
|
|
21
|
-
4. **Reportar**: Usar `execute_suite` para generar la evidencia visual del nuevo flujo descubierto.
|
|
18
|
+
### 1. Sincronización de Identidad
|
|
19
|
+
El agente debe asegurar que el `selector` usado en la `fingerprint` de un nodo sea visible de forma persistente mientras se está en esa pantalla. Si una pantalla es dinámica, usar el elemento más estable como "ancla".
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
*
|
|
21
|
+
### 2. Validaciones (Asserts)
|
|
22
|
+
* **Acción**: Lo que el agente hace para mover la app.
|
|
23
|
+
* **Assert**: Lo que el agente verifica para confirmar el éxito del negocio.
|
|
24
|
+
* *Nota*: El motor fallará el paso si el `assert` devuelve un error, capturando automáticamente la evidencia en ese punto.
|
|
25
|
+
|
|
26
|
+
### 3. Prefijos de Acción
|
|
27
|
+
* `transicion:NOMBRE`: Ejecuta una ruta definida en el mapa.
|
|
28
|
+
* `mcp:SERVIDOR/HERRAMIENTA {args}`: Ejecución directa de herramienta.
|
|
29
|
+
* `sh:COMANDO`: Ejecución directa en consola del host.
|
|
30
|
+
|
|
31
|
+
## 📊 Reportes y Diagnóstico
|
|
32
|
+
Si una suite falla, el agente debe revisar el Dashboard:
|
|
33
|
+
* Si el error es de **Identidad**: El mapa o el selector de la huella están desactualizados.
|
|
34
|
+
* Si el error es de **Assert**: La funcionalidad de la aplicación bajo prueba ha fallado.
|
package/index.js
CHANGED
|
@@ -12,16 +12,9 @@ import { exec } from "child_process";
|
|
|
12
12
|
import { promisify } from "util";
|
|
13
13
|
|
|
14
14
|
const execAsync = promisify(exec);
|
|
15
|
-
|
|
16
|
-
async function log(msg) {
|
|
17
|
-
const time = new Date().toISOString();
|
|
18
|
-
const line = `[${time}] ${msg}\n`;
|
|
19
|
-
await fs.appendFile("sms_server.log", line);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
16
|
const __dirname = path.dirname(__filename);
|
|
24
|
-
|
|
17
|
+
|
|
25
18
|
const SUITES_DIR = path.join(__dirname, "suites");
|
|
26
19
|
const CASOS_DIR = path.join(__dirname, "test_cases");
|
|
27
20
|
const REPORTS_ROOT = path.join(__dirname, "reports");
|
|
@@ -53,13 +46,12 @@ export class MaquinaDeEstados {
|
|
|
53
46
|
if (this.mcpClients.has(serverName)) return this.mcpClients.get(serverName);
|
|
54
47
|
const configRaw = await fs.readFile(CONFIG_FILE, "utf-8");
|
|
55
48
|
const config = JSON.parse(configRaw).mcpServers[serverName];
|
|
56
|
-
if (!config) throw new Error(`Configuración no encontrada para ${serverName}`);
|
|
57
49
|
const transport = new StdioClientTransport({
|
|
58
50
|
command: config.command,
|
|
59
51
|
args: config.args || [],
|
|
60
52
|
env: { ...process.env, ...config.env }
|
|
61
53
|
});
|
|
62
|
-
const client = new Client({ name: "SMS-Client", version: "
|
|
54
|
+
const client = new Client({ name: "SMS-Client", version: "12.5.0" }, { capabilities: {} });
|
|
63
55
|
await client.connect(transport);
|
|
64
56
|
const data = { client, transport };
|
|
65
57
|
this.mcpClients.set(serverName, data);
|
|
@@ -69,156 +61,81 @@ export class MaquinaDeEstados {
|
|
|
69
61
|
interpolate(text, data) {
|
|
70
62
|
if (!data) return text;
|
|
71
63
|
let result = text;
|
|
72
|
-
for (const key in data) {
|
|
73
|
-
result = result.replace(new RegExp(`{{${key}}}`, 'g'), data[key]);
|
|
74
|
-
}
|
|
64
|
+
for (const key in data) { result = result.replace(new RegExp(`{{${key}}}`, 'g'), data[key]); }
|
|
75
65
|
return result;
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
async loadExternalData(dataPath) {
|
|
79
|
-
const fullPath = path.isAbsolute(dataPath) ? dataPath : path.join(__dirname, dataPath);
|
|
80
|
-
if (!fsSync.existsSync(fullPath)) return null;
|
|
81
|
-
|
|
82
|
-
const content = await fs.readFile(fullPath, "utf-8");
|
|
83
|
-
if (fullPath.endsWith(".json")) return JSON.parse(content);
|
|
84
|
-
|
|
85
|
-
if (fullPath.endsWith(".csv")) {
|
|
86
|
-
const lines = content.split("\n").map(l => l.trim()).filter(l => l.length > 0);
|
|
87
|
-
if (lines.length < 2) return [];
|
|
88
|
-
const headers = lines[0].split(",").map(h => h.trim().replace(/^["']|["']$/g, ""));
|
|
89
|
-
return lines.slice(1).map(line => {
|
|
90
|
-
const values = line.split(",");
|
|
91
|
-
const obj = {};
|
|
92
|
-
headers.forEach((h, i) => {
|
|
93
|
-
let val = values[i] ? values[i].trim() : "";
|
|
94
|
-
// Quitar comillas si existen
|
|
95
|
-
val = val.replace(/^["']|["']$/g, "");
|
|
96
|
-
obj[h] = val;
|
|
97
|
-
});
|
|
98
|
-
return obj;
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
68
|
async ejecutarSuite(suiteName) {
|
|
105
69
|
const suite = this.suites.get(suiteName);
|
|
106
70
|
if (!suite) throw new Error(`Suite '${suiteName}' no encontrada.`);
|
|
107
|
-
|
|
108
|
-
if (suite.state_map) {
|
|
109
|
-
process.stderr.write(`\n📍 [V12.3] Pre-flight Integrity Check for: ${suite.state_map}\n`);
|
|
110
|
-
// Simplified validation: Ensure the file exists and has the required structure
|
|
111
|
-
const mapPath = path.join(__dirname, 'maps', suite.state_map);
|
|
112
|
-
try {
|
|
113
|
-
await fs.access(mapPath);
|
|
114
|
-
const mapData = JSON.parse(await fs.readFile(mapPath, 'utf8'));
|
|
115
|
-
const nodes = mapData.nodos || mapData;
|
|
116
|
-
process.stderr.write(`✅ Map validated (${Object.keys(nodes).length} nodes found).\n`);
|
|
117
|
-
} catch (e) {
|
|
118
|
-
process.stderr.write(`❌ CRITICAL: State Map '${suite.state_map}' not found or invalid!\n`);
|
|
119
|
-
throw new Error(`Integrity Check Failed: ${e.message}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
71
|
+
|
|
123
72
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
124
73
|
const reportDir = path.join(REPORTS_ROOT, `exec_${suiteName}_${timestamp}`);
|
|
125
74
|
await fs.mkdir(reportDir, { recursive: true });
|
|
126
75
|
const results = { suiteName, timestamp, cases: [], hooks: { beforeSuite: [], afterSuite: [] } };
|
|
127
76
|
|
|
128
|
-
const maskSecrets = (text) => {
|
|
129
|
-
if (typeof text !== "string") return text;
|
|
130
|
-
// Enmascarar tokens de seguridad y JWTs
|
|
131
|
-
return text.replace(/("securityToken"\s*:\s*")[^"]+(")/g, '$1********$2')
|
|
132
|
-
.replace(/("token"\s*:\s*")[^"]+(")/g, '$1********$2')
|
|
133
|
-
.replace(/eyJhbGciOi[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*/g, '********[JWT_MASKED]********');
|
|
134
|
-
};
|
|
135
|
-
|
|
136
77
|
const runActions = async (actions, targetRes, data) => {
|
|
137
78
|
if (!actions) return;
|
|
138
79
|
for (const action of actions) {
|
|
139
|
-
const
|
|
140
|
-
process.stderr.write(`[EXEC] Running action: ${maskedAction}\n`);
|
|
141
|
-
const actionRes = { action: maskedAction, status: "passed" };
|
|
80
|
+
const actionRes = { action, status: "passed" };
|
|
142
81
|
targetRes.push(actionRes);
|
|
143
82
|
try {
|
|
144
|
-
let lastResult = null;
|
|
145
83
|
const executeAction = async (act) => {
|
|
146
84
|
const subRes = (act === action) ? actionRes : { action: act, status: "passed" };
|
|
147
85
|
if (act !== action) targetRes.push(subRes);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
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;
|
|
163
99
|
}
|
|
164
|
-
if (!foundAction) throw new Error(`Transición '${transName}' no encontrada en el mapa '${suite.state_map}'`);
|
|
165
|
-
finalAction = foundAction;
|
|
166
100
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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}`);
|
|
171
112
|
}
|
|
172
|
-
|
|
113
|
+
subRes.output = (subRes.output || "") + "\n✅ Identidad Multivectorial confirmada.";
|
|
173
114
|
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
174
117
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw new Error(lastResult.content?.[0]?.text || "Error desconocido en herramienta MCP");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (lastResult.content) {
|
|
196
|
-
for (const item of lastResult.content) {
|
|
197
|
-
if (item.text) {
|
|
198
|
-
subRes.output = (subRes.output || "") + "\n" + maskSecrets(item.text);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (lastResult.content) {
|
|
204
|
-
let base64 = "";
|
|
205
|
-
for (const item of lastResult.content) {
|
|
206
|
-
if (item.type === "image") base64 = item.data;
|
|
207
|
-
else if (item.text && (item.text.startsWith("iVBOR") || item.text.length > 5000)) base64 = item.text;
|
|
208
|
-
}
|
|
209
|
-
if (base64) {
|
|
210
|
-
subRes.image = base64.startsWith("data:") ? base64 : `data:image/png;base64,${base64}`;
|
|
211
|
-
subRes.output = (subRes.output || "") + "\nEvidencia visual capturada.";
|
|
212
|
-
}
|
|
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}`;
|
|
213
135
|
}
|
|
214
136
|
}
|
|
215
|
-
} catch (e) {
|
|
216
|
-
subRes.status = "failed";
|
|
217
|
-
subRes.error = e.message;
|
|
218
|
-
throw e;
|
|
219
137
|
}
|
|
220
138
|
};
|
|
221
|
-
|
|
222
139
|
await executeAction(action);
|
|
223
140
|
} catch (e) {
|
|
224
141
|
actionRes.status = "failed";
|
|
@@ -229,44 +146,24 @@ export class MaquinaDeEstados {
|
|
|
229
146
|
};
|
|
230
147
|
|
|
231
148
|
try {
|
|
232
|
-
await runActions(suite.beforeSuite, results.hooks.beforeSuite);
|
|
233
149
|
for (const caseName of suite.tests) {
|
|
234
150
|
const testCase = this.casosPrueba.get(caseName);
|
|
235
151
|
if (!testCase) continue;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
for (let i = 0; i < dataRows.length; i++) {
|
|
244
|
-
const row = dataRows[i];
|
|
245
|
-
const suffix = row ? ` [Iteration ${i + 1}]` : "";
|
|
246
|
-
const caseRes = { name: testCase.name + suffix, steps: [], status: "passed", hooks: { beforeCase: [], afterCase: [] } };
|
|
247
|
-
results.cases.push(caseRes);
|
|
248
|
-
|
|
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);
|
|
249
157
|
try {
|
|
250
|
-
await runActions(
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
await runActions(suite.beforeStep, stepRes.actions, row);
|
|
256
|
-
await runActions(step.actions, stepRes.actions, row);
|
|
257
|
-
} catch (e) {
|
|
258
|
-
stepRes.status = "failed";
|
|
259
|
-
stepRes.error = e.message;
|
|
260
|
-
throw e;
|
|
261
|
-
} finally {
|
|
262
|
-
await runActions(suite.afterStep, stepRes.actions, row);
|
|
263
|
-
}
|
|
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);
|
|
264
163
|
}
|
|
265
|
-
|
|
266
|
-
} catch (e) { caseRes.status = "failed"; caseRes.error = e.message; }
|
|
164
|
+
} catch (e) { stepRes.status = "failed"; throw e; }
|
|
267
165
|
}
|
|
268
166
|
}
|
|
269
|
-
await runActions(suite.afterSuite, results.hooks.afterSuite);
|
|
270
167
|
} finally {
|
|
271
168
|
for (const { transport } of this.mcpClients.values()) await transport.close();
|
|
272
169
|
this.mcpClients.clear();
|
|
@@ -276,338 +173,126 @@ export class MaquinaDeEstados {
|
|
|
276
173
|
}
|
|
277
174
|
|
|
278
175
|
async finalizarReporte(dir, data) {
|
|
279
|
-
const renderActions = (actions) => actions.map(a =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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>
|
|
284
184
|
</div>
|
|
285
|
-
|
|
286
|
-
${
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const html = `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"><title>SMS Dashboard V11.1 CSV-Ready</title>
|
|
296
|
-
<style>
|
|
297
|
-
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&display=swap');
|
|
298
|
-
body { font-family: 'Outfit', sans-serif; background: #020617; color: #f8fafc; padding: 60px; line-height: 1.6; }
|
|
299
|
-
.container { max-width: 1000px; margin: 0 auto; }
|
|
300
|
-
.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); }
|
|
301
|
-
.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; }
|
|
302
|
-
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; }
|
|
303
|
-
.step { border-left: 3px solid #3b82f6; padding-left: 30px; margin: 40px 0; }
|
|
304
|
-
h2 { font-size: 2em; margin-bottom: 1em; color: #f1f5f9; }
|
|
305
|
-
h3 { color: #94a3b8; font-weight: 400; text-transform: uppercase; font-size: 0.9em; letter-spacing: 2px; }
|
|
306
|
-
</style></head>
|
|
307
|
-
<body><div class="container">
|
|
308
|
-
<h1>SMS Dashboard</h1>
|
|
309
|
-
<p style="color: #64748b; font-size: 1.1em; margin-bottom: 40px;">Universal Data-Driven Framework • V11.2</p>
|
|
310
|
-
${data.hooks.beforeSuite.length ? `<div class="card"><span class="hook-label">Global Setup</span>${renderActions(data.hooks.beforeSuite)}</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>
|
|
311
194
|
${data.cases.map(c => `
|
|
312
|
-
<div
|
|
313
|
-
<
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
${c.hooks.afterCase.length ? `<div><span class="hook-label">Post-Condition</span>${renderActions(c.hooks.afterCase)}</div>` : ""}
|
|
318
|
-
</div>
|
|
319
|
-
`).join("")}
|
|
320
|
-
${data.hooks.afterSuite.length ? `<div class="card"><span class="hook-label">Global Teardown</span>${renderActions(data.hooks.afterSuite)}</div>` : ""}
|
|
321
|
-
</div></body></html>`;
|
|
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>`;
|
|
322
200
|
await fs.writeFile(path.join(dir, "index.html"), html);
|
|
323
|
-
await fs.writeFile(path.join(dir, "results.json"), JSON.stringify(data, null, 2));
|
|
324
201
|
}
|
|
325
202
|
}
|
|
326
203
|
|
|
327
204
|
const maquina = new MaquinaDeEstados();
|
|
328
|
-
const server = new McpServer({ name: "demo-state-machine", version: "
|
|
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 }) => {
|
|
332
|
-
await maquina.cargar();
|
|
333
|
-
const dir = await maquina.ejecutarSuite(name);
|
|
334
|
-
return { content: [{ type: "text", text: `Reporte: ${dir}` }] };
|
|
335
|
-
});
|
|
336
|
-
|
|
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')."),
|
|
340
|
-
nodeData: z.object({
|
|
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.")
|
|
343
|
-
}, async ({ mapName, nodeName, nodeData }) => {
|
|
344
|
-
const mapPath = path.join(__dirname, 'maps', mapName);
|
|
345
|
-
let map = { nodos: {} };
|
|
346
|
-
try {
|
|
347
|
-
const content = await fs.readFile(mapPath, 'utf8');
|
|
348
|
-
map = JSON.parse(content);
|
|
349
|
-
if (!map.nodos) map = { nodos: map };
|
|
350
|
-
} catch (e) { }
|
|
351
|
-
|
|
352
|
-
map.nodos[nodeName] = nodeData;
|
|
353
|
-
await fs.mkdir(path.dirname(mapPath), { recursive: true });
|
|
354
|
-
await fs.writeFile(mapPath, JSON.stringify(map, null, 2));
|
|
355
|
-
|
|
356
|
-
// Generar/Actualizar Diagrama Mermaid
|
|
357
|
-
try {
|
|
358
|
-
let mermaid = "graph TD\n";
|
|
359
|
-
for (const [name, node] of Object.entries(map.nodos)) {
|
|
360
|
-
if (node.transiciones) {
|
|
361
|
-
for (const [transName, trans] of Object.entries(node.transiciones)) {
|
|
362
|
-
mermaid += ` ${name} -- "${transName}" --> ${trans.destino}\n`;
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const mermaidPath = mapPath.replace('.json', '.md');
|
|
367
|
-
await fs.writeFile(mermaidPath, `# 🗺️ Diagrama de Estados: ${mapName}\n\n\`\`\`mermaid\n${mermaid}\`\`\``);
|
|
368
|
-
} catch (me) {
|
|
369
|
-
process.stderr.write(`[WARN] Error generando Mermaid: ${me.message}\n`);
|
|
370
|
-
}
|
|
205
|
+
const server = new McpServer({ name: "demo-state-machine", version: "12.5.0" });
|
|
371
206
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}, async () => {
|
|
378
|
-
const entities = { maps: [], test_cases: [], suites: [], health: [] };
|
|
379
|
-
const getFiles = async (dir) => {
|
|
380
|
-
try { return (await fs.readdir(path.join(__dirname, dir))).filter(f => f.endsWith('.json')); }
|
|
381
|
-
catch (e) { return []; }
|
|
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"] }
|
|
382
212
|
};
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
entities.suites = await getFiles('suites');
|
|
387
|
-
|
|
388
|
-
// Validación de integridad simple
|
|
389
|
-
for (const suiteFile of entities.suites) {
|
|
390
|
-
try {
|
|
391
|
-
const suite = JSON.parse(await fs.readFile(path.join(__dirname, 'suites', suiteFile), 'utf8'));
|
|
392
|
-
if (!entities.maps.includes(suite.state_map)) {
|
|
393
|
-
entities.health.push(`❌ Suite '${suiteFile}' referencia a mapa inexistente: ${suite.state_map}`);
|
|
394
|
-
}
|
|
395
|
-
for (const tc of suite.tests) {
|
|
396
|
-
if (!entities.test_cases.includes(`${tc}.json`) && !entities.test_cases.includes(tc)) {
|
|
397
|
-
entities.health.push(`❌ Suite '${suiteFile}' referencia a test case inexistente: ${tc}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
} catch (e) { entities.health.push(`❌ Error leyendo suite '${suiteFile}': ${e.message}`); }
|
|
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)); }
|
|
401
216
|
}
|
|
402
|
-
|
|
403
|
-
if (entities.health.length === 0) entities.health.push("✅ Estructura íntegra. Todos los vínculos son correctos.");
|
|
404
|
-
|
|
405
|
-
// Validación de integridad de Grafos (Nodos Huérfanos)
|
|
406
|
-
for (const mapFile of entities.maps) {
|
|
407
|
-
try {
|
|
408
|
-
const mapData = JSON.parse(await fs.readFile(path.join(__dirname, 'maps', mapFile), 'utf8'));
|
|
409
|
-
const nodos = mapData.nodos || mapData;
|
|
410
|
-
if (nodos && nodos["HOME"]) {
|
|
411
|
-
const reachable = new Set(["HOME"]);
|
|
412
|
-
const stack = ["HOME"];
|
|
413
|
-
while (stack.length > 0) {
|
|
414
|
-
const current = stack.pop();
|
|
415
|
-
const node = nodos[current];
|
|
416
|
-
if (node && node.transiciones) {
|
|
417
|
-
for (const t of Object.values(node.transiciones)) {
|
|
418
|
-
if (t.destino && nodos[t.destino] && !reachable.has(t.destino)) {
|
|
419
|
-
reachable.add(t.destino);
|
|
420
|
-
stack.push(t.destino);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
const totalNodes = Object.keys(nodos);
|
|
426
|
-
const unreachable = totalNodes.filter(n => !reachable.has(n));
|
|
427
|
-
if (unreachable.length > 0) {
|
|
428
|
-
entities.health.push(`⚠️ Mapa '${mapFile}': Nodos inalcanzables desde HOME: ${unreachable.join(', ')}`);
|
|
429
|
-
}
|
|
430
|
-
} else if (nodos && !nodos["HOME"]) {
|
|
431
|
-
entities.health.push(`⚠️ Mapa '${mapFile}': No tiene un nodo 'HOME' (inicio), no se puede validar el grafo.`);
|
|
432
|
-
}
|
|
433
|
-
} catch (e) { entities.health.push(`❌ Error validando grafo de '${mapFile}': ${e.message}`); }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return { content: [{ type: "text", text: JSON.stringify(entities, null, 2) }] };
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
server.tool("save_test_case", "Crear o actualizar un caso de prueba.", {
|
|
440
|
-
name: z.string().describe("Nombre identificador del test (ej: 'TC_Login_Exitoso')."),
|
|
441
|
-
steps: z.array(z.object({
|
|
442
|
-
name: z.string().optional().describe("Descripción amigable del paso (ej: 'Ingresar Usuario')."),
|
|
443
|
-
action: z.string().optional().describe("Acción a realizar. Puede ser un nombre de transición del mapa o un comando mcp:wdio-mcp/...")
|
|
444
|
-
}).passthrough()).describe("Lista ordenada de pasos lógicos a ejecutar.")
|
|
445
|
-
}, async ({ name, steps }) => {
|
|
446
|
-
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
447
|
-
const filePath = path.join(__dirname, 'test_cases', fileName);
|
|
448
|
-
const testCase = { name: name.replace('.json', ''), steps };
|
|
449
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
450
|
-
await fs.writeFile(filePath, JSON.stringify(testCase, null, 2));
|
|
451
|
-
return { content: [{ type: "text", text: `Caso de prueba '${name}' guardado correctamente.` }] };
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
server.tool("save_suite", "Crear o actualizar una suite de pruebas.", {
|
|
455
|
-
name: z.string().describe("Nombre de la suite (ej: 'Suite_E2E_Sanity')."),
|
|
456
|
-
state_map: z.string().describe("Archivo del mapa de estados a usar (ej: 'mob_perfecto_map.json')."),
|
|
457
|
-
tests: z.array(z.string()).describe("Lista de nombres de casos de prueba a incluir en la suite."),
|
|
458
|
-
beforeSuite: z.array(z.string()).optional().default([]).describe("Acciones globales antes de la suite (ej: iniciar sesión)."),
|
|
459
|
-
afterSuite: z.array(z.string()).optional().default([]).describe("Acciones globales tras la suite (ej: cerrar sesión).")
|
|
460
|
-
}, async ({ name, state_map, tests, beforeSuite, afterSuite }) => {
|
|
461
|
-
const fileName = name.endsWith('.json') ? name : `${name}.json`;
|
|
462
|
-
const filePath = path.join(__dirname, 'suites', fileName);
|
|
463
|
-
const suite = { name: name.replace('.json', ''), state_map, tests, beforeSuite, afterSuite };
|
|
464
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
465
|
-
await fs.writeFile(filePath, JSON.stringify(suite, null, 2));
|
|
466
|
-
return { content: [{ type: "text", text: `Suite '${name}' guardada correctamente.` }] };
|
|
217
|
+
return { content: [{ type: "text", text: "✨ Entorno Inicializado. Usa 'sms_builder' para empezar el diseño." }] };
|
|
467
218
|
});
|
|
468
219
|
|
|
469
|
-
server.tool("
|
|
470
|
-
action: z.enum(["start", "add_node", "
|
|
471
|
-
data: z.record(z.any())
|
|
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())
|
|
472
223
|
}, async ({ action, data }) => {
|
|
473
|
-
const
|
|
474
|
-
let
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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'.` }] };
|
|
489
282
|
}
|
|
490
|
-
|
|
491
|
-
if (action === "add_transition" && context.step === "ADD_TRANSITIONS") {
|
|
492
|
-
context.transitions[data.label] = { destino: data.destino, accion: data.accion };
|
|
493
|
-
await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
|
|
494
|
-
return { content: [{ type: "text", text: `🔄 Transición '${data.label}' añadida.\n\n¿Quieres añadir otra o guardar? (Usa action: 'save' para finalizar)` }] };
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
if (action === "save" && context.step === "ADD_TRANSITIONS") {
|
|
498
|
-
const result = await server.callTool("upsert_node", {
|
|
499
|
-
mapName: context.mapName,
|
|
500
|
-
nodeName: context.nodeName,
|
|
501
|
-
nodeData: { transiciones: context.transitions }
|
|
502
|
-
});
|
|
503
|
-
await fs.unlink(contextPath); // Reset wizard
|
|
504
|
-
return result;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return { content: [{ type: "text", text: `❌ Acción no permitida en el estado actual (${context.step}).` }] };
|
|
508
283
|
});
|
|
509
284
|
|
|
510
|
-
server.tool("
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
🎭 HAS ENTRADO EN MODO DISEÑO (SMS ARCHITECT) 🎭
|
|
515
|
-
Estás diseñando la estructura para: ${appName}
|
|
516
|
-
|
|
517
|
-
REGLAS CRÍTICAS DE SUPERVIVENCIA:
|
|
518
|
-
1. ❌ PROHIBIDO editar archivos JSON manualmente.
|
|
519
|
-
2. ❌ PROHIBIDO usar los campos 'nodes' o 'edges'. NO SOMOS UNA LIBRERÍA DE GRÁFICOS.
|
|
520
|
-
3. ✅ USA EXCLUSIVAMENTE 'upsert_node' para crear o actualizar estados.
|
|
521
|
-
4. ✅ ESTRUCTURA OBLIGATORIA: { "nodos": { "NOMBRE_NODO": { "transiciones": {} } } }
|
|
522
|
-
5. 🔄 FLUJO: Primero propón el diseño en texto -> Pide confirmación -> Usa la herramienta.
|
|
523
|
-
|
|
524
|
-
Dime qué pantalla o flujo quieres empezar a mapear para ${appName}.
|
|
525
|
-
`;
|
|
526
|
-
return { content: [{ type: "text", text: rules }] };
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
server.tool("init_project", "Inicializa el proyecto con carpetas y archivos de plantilla", {
|
|
530
|
-
force: z.optional(z.boolean()).default(false)
|
|
531
|
-
}, async ({ force }) => {
|
|
532
|
-
await ensureDirectories();
|
|
533
|
-
|
|
534
|
-
const templates = {
|
|
535
|
-
'maps/template_map.json': {
|
|
536
|
-
nodos: {
|
|
537
|
-
HOME: {
|
|
538
|
-
transiciones: {
|
|
539
|
-
IR_A_LOGIN: { destino: "LOGIN", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Login\" }" }
|
|
540
|
-
}
|
|
541
|
-
},
|
|
542
|
-
LOGIN: {
|
|
543
|
-
transiciones: {
|
|
544
|
-
VOLVER: { destino: "HOME", accion: "mcp:wdio-mcp/click_element { \"selector\": \"~Back\" }" }
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
},
|
|
549
|
-
'test_cases/template_case.json': {
|
|
550
|
-
name: "Template Case",
|
|
551
|
-
steps: [
|
|
552
|
-
{ name: "Ir a Login", action: "IR_A_LOGIN" },
|
|
553
|
-
{ name: "Verificar Pantalla", action: "mcp:wdio-mcp/get_screenshot {}" }
|
|
554
|
-
]
|
|
555
|
-
},
|
|
556
|
-
'suites/template_suite.json': {
|
|
557
|
-
name: "Template Suite",
|
|
558
|
-
state_map: "template_map.json",
|
|
559
|
-
tests: ["template_case"],
|
|
560
|
-
beforeSuite: ["mcp:wdio-mcp/start_session { \"platform\": \"android\", \"provider\": \"perfecto\" }"],
|
|
561
|
-
afterSuite: ["mcp:wdio-mcp/close_session {}"]
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
for (const [relPath, content] of Object.entries(templates)) {
|
|
566
|
-
const fullPath = path.join(__dirname, relPath);
|
|
567
|
-
try {
|
|
568
|
-
if (!force) {
|
|
569
|
-
try {
|
|
570
|
-
await fs.access(fullPath);
|
|
571
|
-
continue; // Skip if exists
|
|
572
|
-
} catch (e) { }
|
|
573
|
-
}
|
|
574
|
-
await fs.writeFile(fullPath, JSON.stringify(content, null, 2));
|
|
575
|
-
} catch (e) {
|
|
576
|
-
process.stderr.write(`[WARN] Error creando template ${relPath}: ${e.message}\n`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return {
|
|
581
|
-
content: [{
|
|
582
|
-
type: "text",
|
|
583
|
-
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."
|
|
584
|
-
}]
|
|
585
|
-
};
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
server.tool("framework_menu", "Panel de control para elegir entre Modo Diseño o Modo Ejecución", {}, async () => {
|
|
589
|
-
return {
|
|
590
|
-
content: [{
|
|
591
|
-
type: "text",
|
|
592
|
-
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`)."
|
|
593
|
-
}]
|
|
594
|
-
};
|
|
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}` }] };
|
|
595
289
|
});
|
|
596
290
|
|
|
597
|
-
async function ensureDirectories() {
|
|
598
|
-
const dirs = ['maps', 'suites', 'test_cases', 'reports', 'data'];
|
|
599
|
-
for (const dir of dirs) {
|
|
600
|
-
const dirPath = path.join(__dirname, dir);
|
|
601
|
-
try {
|
|
602
|
-
await fs.mkdir(dirPath, { recursive: true });
|
|
603
|
-
} catch (e) {
|
|
604
|
-
process.stderr.write(`[WARN] Error creando directorio ${dir}: ${e.message}\n`);
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
291
|
async function main() {
|
|
610
|
-
|
|
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
|
+
}
|
|
611
296
|
await maquina.cargar();
|
|
612
297
|
const transport = new StdioServerTransport();
|
|
613
298
|
await server.connect(transport);
|