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