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 CHANGED
@@ -1,60 +1,31 @@
1
- # 🚀 MCP State Machine Test Framework (SMS)
2
-
3
- The high-fidelity, autonomous orchestration engine for Next-Generation E2E Testing.
4
-
5
- ## 🌟 Key Features
6
- - **Pure-MCP Architecture**: 100% compliant with the Model Context Protocol for seamless AI agent integration.
7
- - **Universal**: Supports Web, Mobile, and API testing through modular MCP tool aggregation.
8
- - **Secure by Design**: Automatic masking of sensitive data (tokens, JWTs) in reports and logs.
9
- - **Premium Reporting**: Generates high-fidelity HTML/JSON dashboards with glassmorphism aesthetics.
10
- - **Data-Driven Core**: Native support for complex testing scenarios via CSV and JSON data injection.
11
-
12
- ## 🛠️ Quick Installation
13
-
14
- You can install it as a dependency in your project:
15
-
16
- ```bash
17
- npm install mcp-state-machine-test-framework
18
- ```
19
-
20
- Or run it directly via MCP in your configuration:
21
-
22
- ```json
23
- "mcp-sms": {
24
- "command": "npx",
25
- "args": ["mcp-state-machine-test-framework"]
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
- *Developed with ❤️ for maximum automation efficiency.*
31
+ *Desarrollado para la era de la automatización autónoma.*
package/SMS_Protocol.md CHANGED
@@ -1,49 +1,47 @@
1
- # 🧠 Historial Técnico y Protocolo: SMS MCP Server
2
-
3
- Este documento registra el aprendizaje evolutivo sobre el uso del **State Machine Server (SMS)** y su integración con otros servidores MCP (ej. `wdio-mcp`). **No contiene lógica de negocio**, sino maestría técnica.
4
-
5
- ## 🛠️ Integración con WDIO-MCP (Aprendizajes Clave)
6
-
7
- ### 1. Gestión de Capacidades (Mobile Cloud)
8
- - **Timeouts**: En entornos cloud (Perfecto), las sesiones tardan en inicializarse. Es vital usar `timeout: 30000` en acciones críticas de navegación.
9
- - **Capabilities Estables**: Para Android en Perfecto, el set mínimo estable es:
10
- - `automationName: "UiAutomator2"`
11
- - `useVirtualDevice: true`
12
- - `app: "PRIVATE:NombreDeLaApp.apk"`
13
- - **Ciclo de Sesión**: Es preferible cerrar y abrir sesiones entre suites independientes para evitar colisiones de estado en el driver.
14
-
15
- ### 2. Estrategias de Selectores en MCP
16
- - **Prioridad de Accesibilidad**: Siempre intentar primero `~ID_Accesibilidad`. Es el método más rápido y compatible con MCP.
17
- - **XPaths Robustos**: Cuando no hay IDs, usar `//*[@resource-id='com.id.app:id/elemento']//android.widget.EditText`. La doble barra `//` ayuda a mitigar cambios menores en la jerarquía.
18
- - **Scroll Automático**: Para elementos fuera de vista, usar el selector nativo de Android: `android=new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().resourceId("..."))`.
19
-
20
- ### 3. Autenticación y Credenciales
21
- - **Estrategia de Descubrimiento**: Si las variables de entorno están vacías, rastrear archivos de "scratch" o scripts de verificación (`verify_*.js`) donde el usuario suele dejar tokens hardcodeados para pruebas rápidas.
22
- - **Formato Perfecto**: El token debe inyectarse en `perfecto:options` -> `securityToken`.
23
-
24
- ## ⚙️ Arquitectura del Servidor SMS
25
-
26
- ### 1. Protocolo de Comunicación "Pure MCP"
27
- - **STDOUT Protegido**: Nunca usar `console.log` para depuración. Cualquier salida no-JSON rompe la conexión. Usar `process.stderr.write` o volcar a un archivo de log externo.
28
- - **Resolución de Rutas**: Los servidores MCP se lanzan desde contextos variables. Siempre usar `path.join(__dirname, ...)` para localizar mapas, casos y suites.
29
-
30
- ### 2. Motor de Reportes y Evidencia
31
- - **Agregación de Resultados**: El servidor SMS itera sobre el array `content` de las respuestas MCP. Captura automáticamente:
32
- - `type: "image"` -> Se convierte en miniatura visual.
33
- - `type: "text"` -> Se guarda como log técnico expandible.
34
- - **Interpolación Dinámica**: Soporte para `{{variable}}` en las acciones, permitiendo inyectar datos de archivos CSV/JSON en tiempo de ejecución.
35
-
36
- ## 🔄 Formatos de Ejecución
37
- - **Tool Principal**: `execute_suite({ "name": "Nombre_Suite" })`.
38
- - **Estructura Modular**: Suite -> Test Cases -> Steps -> Actions. Esta jerarquía garantiza que los reportes sean legibles y la evidencia se asocie correctamente a cada paso lógico.
39
-
40
- ## 🛠️ Herramientas de Gestión (Management Tools)
41
-
42
- A partir de la versión 1.0.1, el servidor incluye herramientas para la construcción estructurada del framework:
43
-
44
- - **`upsert_node`**: Garantiza la integridad del mapa de estados. Valida que cada nodo tenga sus transiciones correctamente definidas.
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
- *Documento en evolución constante. Agregue nuevos aprendizajes técnicos aquí.*
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 cualquier agente que opere el MCP State Machine Test Framework.
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
- ## ⚖️ Reglas de Construcción de Pruebas
5
+ ## 🛠️ Herramientas de Gestión (MANDATORIO)
6
6
 
7
- ### 1. Gestión de Mapas y Estados
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
- ### 2. Definición de Casos de Prueba
12
- - **MANDATORIO**: Usar `save_test_case`. El agente debe definir los `steps` como un array de objetos, asegurando que cada paso tenga un nombre descriptivo para el reporte final.
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
- ### 3. Ensamblaje de Suites
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
- ## 🔄 Flujo de Trabajo Autónomo (Discovery-to-Code)
18
- 1. **Explorar**: Usar `wdio-mcp/get_elements` para identificar selectores.
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
- *V1.1 - Protocolo de Integridad Estructural*
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
- const DB_FILE = path.join(__dirname, "maquina_db.json");
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: "11.1.0" }, { capabilities: {} });
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 maskedAction = maskSecrets(action);
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
- let finalAction = act;
150
- try {
151
- if (act.startsWith("transicion:")) {
152
- const transName = act.replace("transicion:", "").trim();
153
- const mapPath = path.join(__dirname, 'maps', suite.state_map);
154
- const mapData = JSON.parse(await fs.readFile(mapPath, 'utf8'));
155
- const nodes = mapData.nodos || mapData;
156
-
157
- let foundAction = null;
158
- for (const node of Object.values(nodes)) {
159
- if (node.transiciones && node.transiciones[transName]) {
160
- foundAction = node.transiciones[transName].accion;
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
- if (Array.isArray(finalAction)) {
169
- for (const subAct of finalAction) {
170
- await executeAction(subAct);
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
- return;
113
+ subRes.output = (subRes.output || "") + "\n✅ Identidad Multivectorial confirmada.";
173
114
  }
115
+ return;
116
+ }
174
117
 
175
- const interpolatedAction = this.interpolate(finalAction, data);
176
-
177
- if (interpolatedAction.startsWith("sh:")) {
178
- subRes.output = (await execAsync(interpolatedAction.replace("sh:", ""))).stdout;
179
- } else if (interpolatedAction.startsWith("mcp:")) {
180
- const raw = interpolatedAction.replace("mcp:", "").trim();
181
- const slashIndex = raw.indexOf("/");
182
- const serverName = raw.substring(0, slashIndex);
183
- const rest = raw.substring(slashIndex + 1);
184
- const spaceIndex = rest.indexOf(" ");
185
- const toolName = spaceIndex === -1 ? rest : rest.substring(0, spaceIndex);
186
- const toolArgs = JSON.parse(spaceIndex === -1 ? "{}" : rest.substring(spaceIndex + 1));
187
-
188
- const { client } = await this.getMcpClient(serverName);
189
- lastResult = await client.callTool({ name: toolName, arguments: toolArgs }, undefined, { timeout: 600000 });
190
-
191
- if (lastResult.isError && toolName !== "close_session") {
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
- let dataRows = [null];
238
- if (testCase.data) {
239
- if (Array.isArray(testCase.data)) dataRows = testCase.data;
240
- else if (typeof testCase.data === "string") dataRows = await this.loadExternalData(testCase.data) || [null];
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(suite.beforeCase, caseRes.hooks.beforeCase, row);
251
- for (const step of testCase.steps) {
252
- const stepRes = { name: step.name, actions: [], status: "passed" };
253
- caseRes.steps.push(stepRes);
254
- try {
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
- await runActions(suite.afterCase, caseRes.hooks.afterCase, row);
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
- <div style="margin-top:15px; border-bottom: 1px solid #1e293b; padding-bottom: 15px;">
281
- <div style="display:flex; justify-content:space-between; align-items:center;">
282
- <code style="color: #60a5fa; font-weight:bold;">${a.action}</code>
283
- <span style="font-size:0.7em; padding:2px 8px; border-radius:10px; background:${a.status === 'passed' ? '#065f46' : '#991b1b'}; color:white;">${a.status}</span>
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
- ${a.image ? `<div style="margin-top:15px; text-align:center;"><img src="${a.image}" style="max-width:100%; border-radius:12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); border: 1px solid #334155;"></div>` : ""}
286
- ${a.output ? `
287
- <details style="margin-top:10px;">
288
- <summary style="cursor:pointer; color:#64748b; font-size:0.8em; outline:none;">View technical logs</summary>
289
- <pre style="background:#020617; padding:15px; border-radius:8px; font-size:0.85em; color:#10b981; margin-top:10px; border:1px solid #1e293b; overflow-x:auto;">${a.output}</pre>
290
- </details>` : ""}
291
- ${a.error ? `<pre style="background:#450a0a; padding:15px; border-radius:8px; font-size:0.85em; color:#fca5a5; margin-top:10px;">${a.error}</pre>` : ""}
292
- </div>
293
- `).join("");
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 class="card">
313
- <h3 style="color:#60a5fa; margin-bottom:10px;">TEST CASE EXECUTION</h3>
314
- <h2>${c.name}</h2>
315
- ${c.hooks.beforeCase.length ? `<div><span class="hook-label">Pre-Condition</span>${renderActions(c.hooks.beforeCase)}</div>` : ""}
316
- ${c.steps.map(s => `<div class="step"><h3>STEP: ${s.name}</h3>${renderActions(s.actions)}</div>`).join("")}
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: "11.1.0" });
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
- return { content: [{ type: "text", text: `Nodo '${nodeName}' actualizado. Diagrama visual regenerado en maps/${mapName.replace('.json', '.md')}` }] };
373
- });
374
-
375
- server.tool("inspect_framework", "Inspeccionar integridad y listar entidades del framework", {
376
- filter: z.optional(z.string())
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
- entities.maps = await getFiles('maps');
385
- entities.test_cases = await getFiles('test_cases');
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("design_wizard", "Asistente paso a paso para diseñar la máquina de estados", {
470
- action: z.enum(["start", "add_node", "add_transition", "save"]),
471
- data: z.record(z.any()).optional().describe("Datos según la fase: { mapName }, { nodeName }, { label, destino, accion }")
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 contextPath = path.join(__dirname, 'data', '.design_context.json');
474
- let context = { step: "IDLE", mapName: "", nodeName: "", transitions: {} };
475
-
476
- try { context = JSON.parse(await fs.readFile(contextPath, 'utf8')); } catch (e) { }
477
-
478
- if (action === "start") {
479
- context = { step: "SELECT_NODE", mapName: data.mapName || "default_map.json", nodeName: "", transitions: {} };
480
- await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
481
- return { content: [{ type: "text", text: `🧙‍♂️ Wizard Iniciado: Trabajando en '${context.mapName}'.\n\nPASO 1: ¿Qué nombre le damos al nodo? (Usa action: 'add_node')` }] };
482
- }
483
-
484
- if (action === "add_node" && context.step === "SELECT_NODE") {
485
- context.nodeName = data.nodeName;
486
- context.step = "ADD_TRANSITIONS";
487
- await fs.writeFile(contextPath, JSON.stringify(context, null, 2));
488
- return { content: [{ type: "text", text: `📍 Nodo '${context.nodeName}' identificado.\n\nPASO 2: Define una transición. (Usa action: 'add_transition' con { label, destino, accion }). Puedes llamar a esto varias veces.` }] };
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("begin_design", "Iniciar una sesión de diseño guiado para una aplicación", {
511
- appName: z.string().describe("Nombre de la aplicación a diseñar (ej: 'AdvantageShopping').")
512
- }, async ({ appName }) => {
513
- const rules = `
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
- await ensureDirectories();
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-state-machine-test-framework",
3
- "version": "1.2.3",
3
+ "version": "1.2.7",
4
4
  "description": "High-fidelity State Machine MCP Server for autonomous E2E testing orchestration.",
5
5
  "main": "index.js",
6
6
  "type": "module",